#371 Strong Parameters pro
- Download:
- source codeProject Files in Zip (74.9 KB)
- mp4Full Size H.264 Video (30.1 MB)
- m4vSmaller H.264 Video (15.2 MB)
- webmFull Size VP8 Video (17.7 MB)
- ogvFull Size Theora Video (35.5 MB)
If you’ve worked with Rails for a while you probably know about the importance of using attr_accessible
in your models in regard to security. (If you’re not then episode 26 has details about it.) One issue that is often encountered when using it is that it isn’t very flexible. Sometimes we want this behaviour to be dependent on the current user’s permissions but this isn’t straightforward to implement. We’ll explain this with a simple forum application.
This forum has many topics and users can add or edit topics. If we edit one we can change its name or mark it as sticky so that it stays at the top of the list. We only want admin users to be able to mark a topic as sticky but currently any user can do this.
The attr_accessible
line in the Topic
model allows the sticky
attribute to be set through mass assignment but this needs to be dependent on the current user which we don’t have access to in the model. The current solution is to pass an as option to define the roles that can be set by each type of user.
class Topic < ActiveRecord::Base has_many :posts accepts_nested_attributes_for :posts attr_accessible :name, as: :user attr_accessible :name, :sticky, as: :admin end
Doing this means that when we create or update a Topic
we need to pass in that as option, like this:
def create @topic = Topic.new(params[:topic], as: current_user.try(:admin?) ? :admin : :user) if @topic.save redirect_to @topic, notice: "Created topic." else render :new end end
We’d need to do this as well when we update a topic. If we try marking a topic as sticky while logged in as a non-admin user now an exception will be raised. When we sign in as an admin and make the same change, however, we can update the topic.
There are a couple of problems with this approach. One is that the authorization logic could potentially become rather complicated and placing it inline in each model doesn’t feel like the right place for it. Also it’s still not very dynamic: if we want to change this behaviour depending on the user’s attributes or the topic’s attributes this is difficult to do.
Strong Parameters
Several months ago Yehuda Katz wrote up a couple of ideas for improving how Rails handles mass assignment. One of his suggestions was to use a signed token in the form that would be sent along with the other fields when the form was submitted which would mean that access to update the fields would only be granted to those form fields. This was an interesting idea but it didn’t gain much traction. There are security concerns with it and it doesn’t really address how to accept user parameters outside a form such as when using an application’s API. The second idea is more practical and involves moving the protection into the controller layer from the model layer. This idea raised a lot of interesting discussion and its worth reading through the gist’s comments to see the ideas that people have had surrounding this.
Shortly afterwards David Heinemeier Hannson released a gem called Strong Parameters that provides an elegant solution to this problem. This gem allows us to call permit
on the request parameters and restrict the attributes that are passed in. The current plan is to add this gem’s functionality to Rails 4 so for a sneak peek of how this functionality will work we’ll use this gem in our application.
We’ll apply Strong Parameters to our Forum application to see how it works. First we’ll need to add the gem to the gemfile then run bundle
to install it.
gem 'strong_parameters'
There’s an option in our app’s config file that forces us to use attr_accessible
in our models. We’ll comment this out as we’ll be using strong_parameters
instead.
# Enforce whitelist mode for mass assignment. # This will create an empty whitelist of attributes available for mass-assignment for all models # in your app. As such, your models will need to explicitly whitelist or blacklist accessible # parameters by using an attr_accessible or attr_protected declaration. # config.active_record.whitelist_attributes = true
We can now remove the calls to attr_accessible
from our Topic
model.
class Topic < ActiveRecord::Base has_many :posts accepts_nested_attributes_for :posts end
Doing this will make the model insecure so we’ll enable Strong Parameters. To do this we need to include the ActiveModel::ForbiddenAttributesProtection
. We could do this in the Topic
model but the problem with doing this is that we need to remember to do it for every model in our application. It’s better to have to models secure by default so we’ll add it to ActiveRecord::Base
instead. We can do this is a new initializer.
ActiveRecord::Base.send(:include, ActiveModel::ForbiddenAttributesProtection)
When we restart our app now we have security by default. If we try to update a topic in the browser we’ll see an ActiveModel::ForbiddenAttributes
error message as we can’t update any of our application’s models by default. To fix this we need to call permit
on the parameters that are passed in to mass assignment such as in the TopicController
’s update
action. We’ll replace the as
option that we set earlier and, for now, set the attributes statically.
def update @topic = Topic.find(params[:id]) if @topic.update_attributes(params[:topic].permit(:name, :sticky) redirect_to topics_url, notice: "Updated topic." else render :edit end end
We can now update a topic in the browser. To keep things clean and DRY we’ll move the code that defines the parameters into its own method.
def update @topic = Topic.find(params[:id]) if @topic.update_attributes(topic_params) redirect_to topics_url, notice: "Updated topic." else render :edit end end private def topic_params params[:topic].permit(:name, :sticky) end
As this code is in a controller we can easily change the parameters based on the current user and so restrict the ability to update the sticky
attribute to admins.
def topic_params if current_user && current_user.admin? params[:topic].permit(:name, :sticky) else params[:topic].permit(:name) end end
This code will do much the same thing we did with attr_accessible
in the model earlier. There are a couple of changes we can make to this method. Instead of accessing the topic parameters directly we’ll use require
to ensure that they exist so that we don’t get a nil
exception when we call permit
. If we want all of a model’s parameters to be updatable we can call permit
with an exclamation mark so we’ll so that for the admin users.
private def topic_params if current_user && current_user.admin? params.require(:topic).permit! else params.require(:topic).permit(:name) end end
How Strong Parameters Work
At this point you might be wondering how Strong Parameters works. It looks like it defines methods on the params
hash so we’ll do some experimenting in the console to work this out. It’s important to understand that passing in a simple normal hash to mass assignment will not protect the model and we can demonstrate this by creating a new Topic
.
>> Topic.new(sticky: true) => #<Topic id: nil, name: nil, sticky: true, created_at: nil, updated_at: nil>
Strong Parameters overrides the params
method in the controllers and passes it through an instance of ActionController::Parameters
. If we create an instance of this class and pass it a hash the object we get back behaves like a hash but it has extra methods such as permit
and require
. If we pass this object to a new Topic
an exception will be raised as those attributes can’t be set through mass assignment.
>> params = ActionController::Parameters.new(sticky: true) => {"sticky"=>true} >> Topic.new(params) ActiveModel::ForbiddenAttributes: ActiveModel::ForbiddenAttributes
This is very useful. If we ever pass in attributes through a trusted source such as our application’s tests we can pass them straight in. For untrusted sources we can go through ActionController::Parameters
but we need to watch out for those cases where the params
hash is converted back into a normal hash which would go through unprotected, although this is unlikely to happen.
There are some concerns about defining the attribute protection in the controller layer, but this is a flexible solution and it can easily be moved elsewhere. As an example we have another TopicsController
in our application that handles API requests. We’d like to have similar permissions on the attributes here to those in the other TopicsController
but we don’t want to duplicate our topic_params
method as any changes to the authorization logic will then need to be made in two places. A common solution for cases where code needs to be included in two classes is to move it into a module so let’s try that. There’s no obvious place to put this module so we’ll put it in the /lib
directory.
module TopicParams private def topic_params if current_user && current_user.admin? params.require(:topic).permit! else params.require(:topic).permit(:name) end end end
We could now include this module in both of our controllers.
While this approach works, extracting modules isn’t always a good idea as the interface and the dependencies aren’t really defined. We access the current_user
and params
methods in this module so anything that needs in include this module needs to have the same interface. This makes this solution inflexible and doesn’t allow us to easily access this functionality outside a controller. So, instead of doing this we’ll delete this module and move its functionality into a new class under /app/models
. Even though this class won’t inherit from ActiveRecord::Base
it’s still a model, just not one with a database table behind it.
class PermittedParams < Struct.new(:params, :user) def topic if user && user.admin? params.require(:topic).permit! else params.require(:topic).permit(:name) end end end
Note that the class inherits from Struct.new
as this provides a quick way to define accessors and the initializer. The topic_params
is very similar to the code we had in our module, except that we’ve replaced current_user
with user and renamed the method to topic
. Now the dependencies for this logic are clearly defined and we can easily instantiate this class from within a controller. We’ll use it now in the ApplicationController
so that it’s available everywhere.
def permitted_params @permitted_params ||= PermittedParams.new(params, current_user) end
We can now call this method in our TopicsController
.
def update @topic = Topic.find(params[:id]) if @topic.update_attributes(permitted_params.topic) redirect_to topics_url, notice: "Updated topic." else render :edit end end
For our other controllers we can define similar methods in our new PermittedParameters class and use them in the same way. We can use our new class in our API’s TopicsController
, too, without duplicating any logic.
We can try our new logic out now. If we sign in as an admin we can update a topic and change its stickyness. If we log in as a non-admin user and try to update a topic, however, although no exception is raised the sticky
attribute for that topic isn’t updated. Ideally the checkbox for setting a topic’s stickiness wouldn’t be displayed to non-admin users. It would be good if we could use the PermittedParams
logic in our views as well like this:
<% if permitted_params.topic_attributes.include? :sticky %> <div class="field"> <%= f.check_box :sticky %> <%= f.label :sticky %> </div> <% end %>
To get this to work we’ll write a topic_attributes
method in our PermittedParams
class.
class PermittedParams < Struct.new(:params, :user) def topic params.require(:topic).permit(*topic_attributes) end def topic_attributes if user && user.admin? [:name, :sticky] else [:name] end end end
Our topic
method now calls topic_attributes
and this new method now contains the logic that defines the attributes that are permitted for each type of user. We need to make a change in the ApplicationController
, too, and make this method a helper method so that we can use it in the views.
def permitted_params @permitted_params ||= PermittedParams.new(params, current_user) end helper_method :permitted_params
When we reload the edit page for a topic now while logged in as a non-admin the “Sticky” checkbox no longer appears.
Strong Parameters appears to be a big step up from attr_accessible
and it’s certainly flexible. We can define authorization logic in the controllers or in a separate class as we’ve done here.