redesigning mass attribute assignment (activerecord 3.0?)

Hello!

I’d like to refactor the code for mass assignment to support a new security paradigm, but figured that I might save some time getting feedback from the gatekeepers of the code. :slight_smile:

We’re all familiar with attr_accessible and attr_protected. These methods provide rudimentary support from malicious mass assignment … when people remember to use them. I usually remember to set them myself, except that I really wouldn’t be surprised if someone auditing my code found one or two spots that I’ve forgotten. But this isn’t about just changing the default to a hardcore blacklist instead of an all-encompassing whitelist. This is about when attr_accessible and attr_protected aren’t enough.

They’re not enough when user roles enter into the picture. Just to take the simplest possible example, let’s suppose that an application has normal registered users and it has staff. The staff, very trustable sorts (crossed fingers), are allowed to change attributes that normal users are not. It’s a problem that attr_accessible and attr_protected, being very static lists, are not prepared to handle. But let’s not stop there. We’ve all coded spots where we wanted to directly assign a couple of attributes ourselves to trusted values, but we couldn’t use mass assignment because we (the devs) have essentially told the code to not even trust ourselves.

And yet … it’s such a pretty syntax. Who wants to give that up?

I do! Well, just a little.

My idea (most likely not original) is to let the controller specify which attributes may be assigned. In its briefest form, my proposal is be to remove attr_accessible/attr_protected and change the mass assignment API as follows:

  • User.new(params[:user]) becomes User.new(params[:user], whitelist)
  • @user.attributes = params[:user] becomes @user.assign(params[:user], whitelist)

Overall I think that this makes things a whole lot cleaner! Which may or may not be the same thing as fewer LOC. Consider:

  • The whitelist would be optional, and if absent, would default to most every column. Which makes it usable for normal coding, and doesn’t get in the way when you’re just getting started.
  • The whitelist makes it very obvious how the code works. Obvious is better than hidden. I normally don’t realize that my attr_accessible isn’t configured right until I happen to scan logs and see that “can’t mass assign” message.
  • The whitelist can be a nested hash of any depth, offering excellent support for the nascent nested forms functionality.
  • The whitelist can be different in a controller that enforces broader security and requires admin users in the first place.
  • It moves security where it belongs, into the context of a user.
    Security is business logic, in my mind, and I do like that in my
    (still-skinny) controllers.
  • The whitelist can be generated on-demand according to the current user. Which would offer an actual extension point for permission-management plugins and their User.current class/thread variable hacks!
  • I like the assign() syntax better than the attributes= syntax. But that could be just me.

I think that Rails 3.0 would be the right time for an API change like this. It would provide a much better extension point for permission management plugins, and, well, there are no holy cows, right? So … should I put in the time and code it up? What do you all think?

-Lance

I like it.

FWIW, Sequel has had something similar to what you propose for about a
year. You can set accessible/protected attributes at a class level
and you can use the set_(only|except) instance methods to only
allow/reject specific attributes per method call. I added support
for it for pretty much the same reasons you gave. The all-or-nothing
approach of ActiveRecord's mass assignment is not flexible enough to
handle different security contexts.

I'm -1 on this being added to ActiveRecord, so Sequel keeps its
advantage in this area. ;p

Jeremy

We're all familiar with attr_accessible and attr_protected. These methods
provide rudimentary support from malicious mass assignment ... when people
remember to use them. I usually remember to set them myself, except that I
really wouldn't be surprised if someone auditing my code found one or two
spots that I've forgotten. But this isn't about just changing the default to
a hardcore blacklist instead of an all-encompassing whitelist. This is about
when attr_accessible and attr_protected aren't enough.

They're not enough when user roles enter into the picture. Just to take the
simplest possible example, let's suppose that an application has normal
registered users and it has staff. The staff, very trustable sorts (crossed
fingers), are allowed to change attributes that normal users are not. It's a
problem that attr_accessible and attr_protected, being very static lists,
are not prepared to handle. But let's not stop there. We've all coded spots
where we wanted to directly assign a couple of attributes ourselves to
trusted values, but we couldn't use mass assignment because we (the devs)
have essentially told the code to not even trust ourselves.

I completely agree with you. Which attributes are allowed to be
mass-assigned is almost always context-sensitive, and thus the
whitelist doesn't belong in the class level.

And yet ... it's such a pretty syntax. Who wants to give that up?

I do! Well, just a little.

My idea (most likely not original) is to let the controller specify which
attributes may be assigned. In its briefest form, my proposal is be to
remove attr_accessible/attr_protected and change the mass assignment API as
follows:

* User.new(params[:user]) becomes User.new(params[:user], whitelist)
* @user.attributes = params[:user] becomes @user.assign(params[:user],
whitelist)

Agreed.

How about a simple extension to Hash (somebody let me know if this
exists already) that allows you to do:

user.attributes = params[:user].pick(:first_name, :last_name)

(Naive implementation here: http://gist.github.com/116880)

The RHS could be extracted to a separate method in the relevant
controller, which would do the context checking.

Regardless of the syntax I think the use of attr_accessible at the
class level should not be encouraged. I'm +1 on this.

How about a simple extension to Hash (somebody let me know if this exists already) that allows you to do:

user.attributes = params[:user].pick(:first_name, :last_name)

This is what active support #slice and #slice! hash extensions do and I have used this in my controllers already for mass attribute white lists per controller action "when needed" for a few months now. I think since 2.3.2 (perhaps earlier) the #slice method even do as expected for indifferent access hashes.

>> {'a' => 'fff', 'b' => 'ccc'}.with_indifferent_access.slice(:a)
=> {"a"=>"fff"}

  - Ken

Which points to an interesting question -- should the model or the
controller be responsible for filtering the attributes? That is,
should the burden be on the model to only assign allowed parameters,
or the controller to only pass allowed parameters? It certainly seems
simple to do it from the controller using something like your
Hash#pick method, but I think it's safer to do it from the model. For
example, if the model is responsible for filtering assignable
attributes, it may create an intelligent default blacklist for cases
where the developer has paid no attention.

I've just about finished a patch to implement AR::Base#assign
(attributes, allowed_attributes). In the process I've realized that
allowed_attributes can simply be an override to attr_accessible/
attr_protected, which makes for an easily backwards compatible API
update. So that'll be my first ticket.

I'd really prefer to remove attr_accessible/attr_protected altogether
as I believe they are in all ways inferior to the new approach and
would only serve to clutter the API in the name of backwards
compatibility. But that's a secondary concern, and will be in a second
ticket that may be evaluated independently.

Ok, primary ticket:
https://rails.lighthouseapp.com/projects/8994/tickets/2704-allow-calltime-list-of-allowed-attributes-for-mass-assignment

And the bonus round:
https://rails.lighthouseapp.com/projects/8994/tickets/2705-deprecate-attr_accessible-attr_protected

The second one seems like a really, really bad idea. Specifying the allowed attributes at the call is great, but what about the simpler use cases? Sometimes you just want to prohibit (or allow) mass assignment, and requiring users to specify the list every time is the opposite of DRY. Not to mention the maintenance headache when adding a new field...

--Matt Jones

cainlevy wrote:

Which points to an interesting question -- should the model or the
controller be responsible for filtering the attributes? That is,
should the burden be on the model to only assign allowed parameters,
or the controller to only pass allowed parameters? It certainly seems
simple to do it from the controller using something like your
Hash#pick method, but I think it's safer to do it from the model. For
example, if the model is responsible for filtering assignable
attributes, it may create an intelligent default blacklist for cases
where the developer has paid no attention.

I've just about finished a patch to implement AR::Base#assign
(attributes, allowed_attributes). In the process I've realized that
allowed_attributes can simply be an override to attr_accessible/
attr_protected, which makes for an easily backwards compatible API
update. So that'll be my first ticket.

I don't think this massive change to the api is justified. You're
introducing complexity for all users to support a few cases which, while
hardly rare, aren't 100% of user's requirements.

It should be trivial for you to implement this as a plugin to see if
people prefer this approach to specifying assignable attributes. If
that picks up momentum we can look at pulling it in to rails.

In the meantime users can already do:

@user.attributes = params[:user].slice(:email, :password,
:password_confirmation)

or

@user.attributes = params[:user].except(:admin)

I'd really prefer to remove attr_accessible/attr_protected altogether
as I believe they are in all ways inferior to the new approach and
would only serve to clutter the API in the name of backwards
compatibility. But that's a secondary concern, and will be in a second
ticket that may be evaluated independently.

attr_accessible and friends are a great simple solution for a really
common case. We shouldn't lose sight of that just because there are
some cases where they're not perfect.

Hey Matt, I did some thinkng about this and I'm not sure if the
simpler use cases are actually simpler with attr_accessible, or if
they're just different? For any given model I generally only have one
resource controller, so in that case I'd simply be moving the
attribute list from the model to the controller. And when a model is
represented by more than one resource controller, it's probably
because of an entirely different context with different permissions.
And of course there are easy ways to DRY up a list of attributes that
are commonly used without needing official API support.

Do you have any examples where this would be a bad idea? Perhaps I'm
missing something obvious here.

But yes, I split #2705 into a separate ticket because it's more about
opinion than functionality.

I've put the core assign() method into a plugin. Feedback is always
desired.

http://github.com/cainlevy/mass_assignment

class UsersController < ApplicationController

def create

@user = User.new

during signup a user may pick a username

@user.assign(params[:user], [:username, :email, :password, :password_confirmation])

@user.save!

end

I still don’t see why #splice or #except wouldn’t be preferred here?

@user.attributes = params[:user].splice(:username, :email, :password, :password_confirmation)

-Brennan

I agree. Lance raises very valid points, but what would what's wrong
with using Hash#slice and Hash#except to solve these problems?
Especially if the documentation for attr_accessible/attr_protected can
be updated to refer to the usage of Hash#slice and Hash#except for
more complex use cases?

I'm still struggling a bit for clear reasons why it might be better to
filter the attributes controller-side or model-side. The only
functional advantage I can think of either way is that if you want a
system with overridable defaults, you want to build the filtering into
the model. For example, if you had a baseline attr_accessible
declaration and overrode it at calltime with a different set. But eh,
combining declarations like that sounds messy and prone to failure.

Or perhaps some security plugin for people who want to start with
fewer (or no) assignable fields could create default blacklists for
any /is_.*/ and /.*_id/ fields, to be overridden as needed. Hmm, that
actually sounds interesting. Perhaps I'll add that optional module
into my plugin. :slight_smile:

Other than that, is the difference entirely aesthetic? I think the
"assign(params[:user], ...)" syntax reads better than "attributes =
params[:user].slice(...)", but that alone isn't enough reason to
change an API.

Based on the example given for #assign, it does appear to be just a syntactical difference, and I wouldn’t think anything about AR core should be changed.

When mass assigning attributes, one could always do: @user.attributes = params[:user].slice(*@user.writeable_attributes), where User#writeable_attributes would tie in to your ACL system and return an array of settable attributes for that user type.

-Brennan

To me it sounds like a responsibility of the controller. It is the one receiving the parameters, selecting the relevant ones and passing them down to models.

On the other hand, having attribute filtering in the controllers would require a bit of sync between model and controller code. I like to avoid controller having too much knowledge of model attributes, myself.

Yeah I've added it

   http://github.com/lifo/docrails/commit/6197606588674bd16e17899e0df15adf2a482ba0

I think a concrete application will probably need some controller
macro to be able to express those tweaks at a higher level. But the
basic technique to build upon is documented there anyway.

One other thought - going back to the original example (admin user can mass-assign fields that are normally protected), what about an extra parameter to update_attributes (and possibly create)? ie:

@model.update_attributes(params[:whatever], [:stuff_non_admins_cant_change])

So essentially a, "no, really, you can mass-assign these attributes just this once" parameter. That would still allow regular code to work correctly while permitting the context-sensitive stuff you're looking for.

--Matt Jones

Certainly the Hash#except idiom requires you whitelist (or
not-blacklist) sensitive data, because attr_(accessible|protected) are
of course applied to whatever sanitized hash you pass. So in
particular you can only narrow accessible aattributes (or extend
protected attributes)

Going the other way around sounds better to me, not sure about the API
though. I think it requires the same amount of configuration and
exceptions, but looks like a safer default.

After playing with a reimagined version of attr_accessible/
attr_protected in my plugin, I'm much happier with the model-side
filtering approach. I think it allows for more interesting and useful
defaults.

Since this API is to live only as a plugin for a bit, I'm unsure
whether this thread is the place to continue discussion? I think that
Xavier's improved documentation is probably all that can/should be
done to core at this time. Anyone interested in playing with the mass
assignment API is welcome to contact me directly or through GitHub.