For a while, I have felt that the following is the correct way to improve the mass assignment problem without increasing the burden on new users. Now that the problem with the Rails default has been brought up again, it's a good time to revisit it.
When creating a form with form_for
, include a signed token including all of the fields that were created at form creation time. Only these fields are allowed.
To allow new known fields to be added via JS, we could add:
<%= f.allowed_fields "foo", "bar", "baz" %>
The first strategy will not full satisfy apps that support a lot of HTTP requests that do not come from forms generated by Rails.
Because accessible fields is usually a function of authorization, and is not global, we should move allowed fields into the controller. The basic idea is:
class PostsController < ApplicationController
# attributes can be marked accessible for the entire controller
attr_accessible :foo, :bar
def create
# mass assignment can also be done on an instance basis
# this can be used to override the class defaults
attr_accessible(true) if user.admin?
...
end
end
I would imagine that Rails authorization frameworks like CanCan could add sugar to make this even easier in common cases.
The core problem with Rails mass assignment is that attribute protection is an authorization concern. By moving it to the controller, we can have smart defaults (like signed fields in form_for
) and in more advanced cases, make it easier to decide what fields are allowed on a per-user basis.
By moving it into the correct place, we will probably find other nice abstractions that we can use over time to make nice defaults for users without compromising security.
@tulockmike I think the conversation about attributes is missing the abstraction. We really only have three things going on
Authentication happens in the middleware. It is a global state.
Authorization happens on specific controller actions. CANCAN makes this clear
Validation happens after a state change has been affected. It assumes the state change is authorized and is a sanity
check on the object fields.
However it is clear that in many cases it is not enough to just authorize! update, @some_model_instance and
then blindly apply update_attributes. An example makes it clear. Say we have the following model.
We have two contexts in which we may wish to update an image object.
Both of these actions can be done through update_attributes passing in the params
hash. Easy to do. However it is evil. What you really should have on the object
are methods to perform specific state changes.
Now our controller would have to look like
So now we have correct separation of concerns. The model is fully responsible for defining it's state changes and the api for those state changes. The controller is fully responsible for deciding who is allowed to invoke the state change.
Of couse this results in our controllers slipping away from the standard
suite of methods. In theory you could create a set of nested controllers to implement the above image controller and in fact it probably is the right thing to do. Imagine a web form to change the ownership of the image
Again there are no attribute authorizations made in the controller only authorizations made on specific state changes which invoke methods on the model. To make this kind of thing easier on the developer I would suggest method generation helpers on the models to change
to
which could be compressed in default as
but then again too much magic got us where we are today.