AccessibleFor: key-based hash sanitizer for Ruby
This is a simple mass-assignment security module loosely based on
ActiveModel::MassAssignmentSecurity. It attempts to steal the good ideas
and some of the API while being compatible with Rails 2.3-based applications.
Only attr_accessible (or its equivalent, keep reading) is implemented, because
attr_protected is just a bad ActiveRecord API that hung around for some reason,
and we don't want it stinking up the place.
There are actually two available APIs, the ActiveModel-workalike and a new one
called accessible_for. They provide identical functionality.
Installation
$ gem install accessible_for
Usage
This is primarily intended for use in controller code. It should be possible
to use this with an ActiveRecord model as well, provided you use the
accessible_for API (to avoid name conflicts).
accessible_for API
require 'accessible_for'
class TacoShop < Controller
include AccessibleFor
# there are no implicit roles and
# you can declare only one role for each set of attributes
accessible_for :customer => [ :filling, :topping, :rating ]
accessible_for :manager => [ :filling, :topping, :price ]
# you can declare a role multiple times to add attributes,
# and specify a single value instead of an array
accessible_for :manager => :promotion
# If that's not DRY enough you can compose access lists from other roles
# using the class method accessible_by
accessible_for :common => [ :filling, :topping ]
accessible_for :customer => accessible_by(:common) + [ :rating ]
accessible_for :manager => accessible_by(:common) + [ :price, :promotion ]
def update
Taco.find(params[:id]).update_attributes!(taco_params)
end
protected
def taco_params
# use sanitize_for(role, params) to build a safe hash
# again, there is no implicit role
if current_user.manager?
sanitize_for :manager, params[:taco]
else
sanitize_for :customer, params[:taco]
end
end
end
It's also possible to call sanitize_for with a block to loop over the
accessible name/value pairs:
sanitize_for(:default, params[:taco]) do |name, value|
puts "#{name}: #{value}"
end
ActiveModel-workalike API
require 'mass_assignment_backport'
class TacoShop < Controller
include MassAssignmentBackport
# when no role is specified, :default is used
attr_accessible :rating
# you can specify multiple roles
attr_accessible :filling, :topping, :as => [:default, :manager]
# and add to existing roles
attr_accessible :price, :as => :manager
def update
Taco.find(params[:id]).update_attributes!(taco_params)
end
protected
def taco_params
# use sanitize_for_mass_assignment to build a safe hash given a role.
# when nothing/nil is passed for the role, :default is used
sanitize_for_mass_assignment params[:taco], current_user.manager? ? :manager : nil
end
end
Rationale
There are two things I've never liked about ActiveRecord's attr_* API:
It's model-level when the resources I am trying to protect are controller-level.
This actually gets in our way when we're just trying to test/manipulate our own
models outside of a controller context, making it harder to work with
our own data for no good reason. I feel this phenomenon could have the effect of
discouraging developers from using it.
Another problem with ActiveRecord is that it provides attr_protected.
Blacklisting instead of whitelisting is just a bad idea, and I see no reason
to allow/support it when security is the primary goal.
So once we address those two things we have something that looks a bit like
ActiveModel's implementation minus attr_protected, which is the purpose of the
ActiveModel-workalike API. However there are problems with this API as well:
The role is optional, leading to lack of clarity. Sometimes you need to
specify :default, sometimes it's implicit. I think an API designed for
hardening should be more transparent.
The way the role is specified is also suboptimal. It's at the end of the
declaration so you have to hunt for it. It uses the key :as implying a
user-based access role, but the fact is this value is really just a scope and
can mean anything.
Author
Zack Hobson (zack@zackhobson.com)