Understanding Strong Parameters in Rails 4
This article was originally published in the blog of ActBlue Technical Services.
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.