Understanding Strong Parameters in Rails 4

November 14, 2014

This article was originally published in the blog of ActBlue Technical Services.

Understanding Strong Parameters in Rails 4

Earlier this year, in ActBlue, we completed the upgrade of our main application from Rails 3.2 to 4.1. The last step in the process was incorporating strong parameters.

Several of our models are simple and we did not have a problem following the documentation, RailsCast and other articles. But a good number of models (and its controllers) are more complex or were using our own protection mechanism, and we were dragging our feet on them.

The main issue was that we did not really understand how strong parameters worked. Correction: I did not understand, which is the big realization that took place when I had to explain what I was doing to the rest of the team. The part that is missing from all the articles I read is what happens when you do not use strong parameters in the correct way. For this reason, the best solution was a series of tests, which is what we present here.

In order to minimize the dependencies, the tests were written in MiniTest::Spec, and the only additional gem required is mocha. Ruby version is 2.1.4 and Rails 4.1.6.

The full list of tests can be found in this github repo, following we go through the most important ones:

If you change the version of Rails in your Gemfile from 3.2 to 4.1, without making any other changes, parameters in the controllers will be passed untouched to methods new and update_attributes on your models, raising this error:

describe 'completely unhandled params (i.e. no call to permit)' do
  before do
    @hash = { 'title' => 'Test', 'body' => 'test body' }
    @params = ActionController::Parameters.new @hash
  end
  it 'should raise error' do
    -> { Article.new @params }.must_raise ActiveModel::ForbiddenAttributesError
  end
end

The next step is to start whitelisting some parameters by calling the permit method. For sure, you will miss some, which will trigger this notification:

describe 'permit some elements from params and miss others' do
  before do
    @params = ActionController::Parameters.new @hash
  end
  it 'should log non-permitted attributes' do
    ActiveSupport::Notifications.
      expects(:instrument).
      with('unpermitted_parameters.action_controller', keys: ['body']).
      once
    @params.permit(:title).must_equal 'title' => 'Test'
  end
end

In Rails 3.2 when you create or update objects on a model and include attributes that do not exist they willl just be ignored. Version 4.1 raises an exception, which is a better way of handling it, no one would ever rely on the fact they are ignored, right?

describe 'call permit on attributes that do not exist in the model' do
  before do
    params = ActionController::Parameters.new @hash.
      merge('non_attr' => 'test')
    @permitted_params = params.permit(:title, :non_attr)
  end
  it 'ActiveRecord should raise exception on non-attribute' do
    ex = -> { Article.new @permitted_params }.
      must_raise ActiveRecord::UnknownAttributeError
    ex.message.must_equal 'unknown attribute: non_attr'
  end
end

The require method is useful when you want to make sure that certain parameter is present, if not it will raise an exception:

describe 'require something that is not present in params' do
  before do
    hash = {
      'article_attributes' => {'title' => 'Test', 'body' =>'test body'},
      'other_attributes' => {'amount' => '12', 'color' => 'blue' }
    }
    @params = ActionController::Parameters.new hash
  end
  it 'should raise exception' do
    ex = -> { @params.require(:category_attributes) }.
      must_raise ActionController::ParameterMissing
    ex.message.must_equal 'param is missing or the value is empty: ' +
                          'category_attributes'
  end
end

Note that if there are other keys in the params hash they will be removed:

it 'should filter out everything but the required key' do
  @params.require(:article_attributes).must_equal 'title' => 'Test',
                                                  'body' =>'test body'
end

Additionally, requiring without calling permit will raise an exception:

it 'should raise error if we try to use it as is (without permit)' do
  -> { Article.new @params.require(:article_attributes) }.
    must_raise ActiveModel::ForbiddenAttributesError
end

Finally the “standard” way to use strong parameters, this is the form that you will see presented in other blog posts.

describe 'proper use' do
  before do
    @attributes = @params.require(:article_attributes).
      permit(:title, :body)
  end
  it 'should be fine if we use require and permit combined' do
    @article = Article.new @attributes
    @article.title.must_equal 'Test'
    @article.body.must_equal 'test body'
  end
end

Nested attributes on a model that has a one-to-one relation. In the example a comment belongs_to an article:

it 'shows how to permit nested params' do
  attributes = @params.
                  require(:comment).
                  permit(:author, article_attributes: [:id, :title,
                                                       :body])
  attributes.must_equal "author" => "John Smith",
                        "article_attributes" => {
                          "title" => "Test",
                          "body" => "test body"}
  comment = Comment.new attributes
  comment.author.must_equal 'John Smith'
  comment.content.must_be_nil
  comment.article.title.must_equal 'Test'
  comment.article.body.must_equal 'test body'
end

Nested attributes on a model with a one-to-many relation. The example shows an article that has_many comments. Note that the controller will receive an extra level of keys, the integers 0, 1, etc. Rails internally ignores these numbers so we don’t have to include them in the call to permit:

describe 'nested attributes on articles (which has_many comments)' do
  before do
    @hash = {
      "article" => {
        "title" => "Test",
        "body" => "test body",
        "comments_attributes" => {
          '0'=>{"author" => "John", "content" => "great"},
          '1'=>{"author" => "Mary", "content" => "awful"}
        }
      }
    }
    @params = ActionController::Parameters.new @hash
  end
  it 'shows how to permit nested parameters' do
    attributes = @params.
      require(:article).
      permit(:title, comments_attributes: [:author,
                                :content])[:comments_attributes].
        must_equal @hash['article']['comments_attributes']
  end
end

The repo has additional cases and also shows how the code looks when you are doing updates.

In the next blog post will show the specific steps we took during the upgrade of our application.