scoped_attr_accessible

scoped_attr_accessible is a plugin that makes it easy to scope the attr_accessible and attr_protected methods on any library using ActiveModel's MassAssignmentSecurity module. For those unfamiliar with it, read here to get a bit of back story about how it works in ActiveRecord - thanks to ActiveModel, you can now get the joy of scoped access restrictions across any ORM built on ActiveModel, including mongoid!

Installation

To use, just add to any application using ActiveModel. In Rails 3, this is a simple job of adding:

gem 'scoped_attr_accessible'

To our Gemfile and running bundle install.

If you encounter issues, please make sure you add it right after activerecord or rails - Otherwise, you might encounter issues where the process it uses to hook into ActiveModel fails.

Usage

With it enabled, your application should continue to work as usual with classic attr_accessible and attr_protected. When in use, you can simply pass the :scope option in your declaration to declare a scope in which it should be accessible.

For example,

class User < ActiveRecord::Base

  # All attributes are accessible for the admin scope.
  attr_accessible :all, :scope => :admin

  # The default scope can only access a and b.
  attr_accessible :a, :b

  # Make both :c and :d accessible for owners and the default scope
  attr_accessible :c, :d, :scope => [:owner, :default]

  # Also, it works the same with attr_protected!
  attr_protected :n, :scope => :default

end

If both attr_accessible and attr_protected are used on a given scope, attributes declared in attr_protected take precedence. Also, If attr_accessible isn't called for a scope at all, it will allow all variables except those marked as protected.

When declaring the scopes in the accessible / protected part, please note that they need to be symbol names for simplicity's sake.

When you want to mark an attribute as accessible / protected in all scopes, you can use the :all scope. For example:

class User < ActiveRecord::Base
  attr_accessible :a, :scope => :all
  attr_accessible :c, :scope => :admin
  attr_accessible :b, :scope => :owner
end

Will allow :admin to access :a and :c, but not :b. Along the same lines, :owner can access :a and :b, but not :c.

Setting the Scope

Next, when you call methods that use mass assignment (e.g. ActiveRecord::Base#attributes=), it will use your current scope to sanitize mass-assigned variables. By default, with no user intervention this scope is simply :default.

To set the scope, you can do so on a class an instance level with instance-level taking precedence.

To set it on a class level, simply do:

User.current_sanitizer_scope = :admin
# Or, dynamically:
User.current_sanitizer_scope = @user.role.name.to_sym

This will be set Thread local. Also note you can get the current class-level scope:

p User.current_sanitizer_scope # => nil by default

Or, temporarily switch it out, resetting it afterwards:

p User.current_sanitizer_scope
User.with_sanitizer_scope :admin do
  p User.current_sanitizer_scope
end
p User.current_sanitizer_scope

You can also declare this on the instance level, e.g:

user = User.find(params[:id])
user.current_sanitizer_scope = :admin
# Or, more complex:
user.current_sanitizer_scope = "something-else"

Complex Scoping

Although the scope on a given accessible / protected declaration must be a symbol, scoped_attr_accessible provides a way to deal with non-symbol scopes when assigning them - Namely, you can set the current_sanitizer_scope value on classes or instances to an arbitrary object and let scoped_attr_accessible dynamically convert it for you.

This is done using two seperate processes - Recognizers and Converters, each run when a given scope is not a symbol.

The first of these (and the highest priority) are recognizers - they are simply blocks you can declare (like below) that have a scope name and return a value denoting whether or not they match. e.g:

# Reeopen the class
class User < ActiveRecord::Base

  sanitizer_scope_recognizer :admin do |record, scope_value|
    scope_value.is_a?(User) && scope_value.admin?
  end

  sanitizer_scope_recognizer :owner do |record, scope_value|
    scope_value.is_a?(User) && scope_value == record
  end

end

In this example, we could simply do:

user = User.find(params[:id])
user.current_sanitizer_scope = current_user
user.update_attributes params[:user]

And it would automatically set the scope to :owner / :admin when sanitizing the attributes.

The second and more flexible option is scope convertors - they're given the same information (e.g. a record and scope value) and they are responsible for returning nil or a reduced form of the scope. If they return a reduced form (e.g. they may return their creating user, or a plain symbol) it is smart enough to reduce it until it does have a symbol.

As an example, we could implement the following:

# Reeopen the class
class User < ActiveRecord::Base

  sanitizer_scope_converter do |record, scope_value|
    return user.role.name.to_sym if scope_value.is_a?(User)
    return scope_value.user if scope_value.is_a?(UserSession)
  end

end

When combined, these all form a very flexible way to dynamically scope attribute accessible.

Note on Patches/Pull Requests

  • Fork the project.
  • Make your feature addition or bug fix.
  • Add tests for it. This is important so I don't break it in a future version unintentionally.
  • Commit, do not mess with rakefile, version, or history. (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
  • Send me a pull request. Bonus points for topic branches.

Contributors

  • Darcy Laycock
  • Mario Visic

Copyright (c) 2010 The Frontier Group. See LICENSE for details.