mirror of
https://github.com/github/rails.git
synced 2026-01-10 23:27:56 -05:00
AM mass assignment security attr_accessible and attr_protected now allow for scopes using :as => scope eg.
attr_accessible :name
attr_accessible :name, :admin, :as => :admin
This commit is contained in:
@@ -24,10 +24,7 @@ module ActiveModel
|
||||
# include ActiveModel::MassAssignmentSecurity
|
||||
#
|
||||
# attr_accessible :first_name, :last_name
|
||||
#
|
||||
# def self.admin_accessible_attributes
|
||||
# accessible_attributes + [ :plan_id ]
|
||||
# end
|
||||
# attr_accessible :first_name, :last_name, :plan_id, :as => :admin
|
||||
#
|
||||
# def update
|
||||
# ...
|
||||
@@ -37,19 +34,18 @@ module ActiveModel
|
||||
#
|
||||
# protected
|
||||
#
|
||||
# def account_params
|
||||
# sanitize_for_mass_assignment(params[:account])
|
||||
# end
|
||||
#
|
||||
# def mass_assignment_authorizer
|
||||
# admin ? admin_accessible_attributes : super
|
||||
# def scope
|
||||
# scope = admin ? :admin : :default
|
||||
# sanitize_for_mass_assignment(params[:account], scope)
|
||||
# end
|
||||
#
|
||||
# end
|
||||
#
|
||||
module ClassMethods
|
||||
# Attributes named in this macro are protected from mass-assignment
|
||||
# whenever attributes are sanitized before assignment.
|
||||
# whenever attributes are sanitized before assignment. A scope for the
|
||||
# attributes is optional, if no scope is provided then :default is used.
|
||||
# A scope can be defined by using the :as option.
|
||||
#
|
||||
# Mass-assignment to these attributes will simply be ignored, to assign
|
||||
# to them you can use direct writer methods. This is meant to protect
|
||||
@@ -60,36 +56,58 @@ module ActiveModel
|
||||
# include ActiveModel::MassAssignmentSecurity
|
||||
#
|
||||
# attr_accessor :name, :credit_rating
|
||||
# attr_protected :credit_rating
|
||||
#
|
||||
# def attributes=(values)
|
||||
# sanitize_for_mass_assignment(values).each do |k, v|
|
||||
# attr_protected :credit_rating, :last_login
|
||||
# attr_protected :last_login, :as => :admin
|
||||
#
|
||||
# def assign_attributes(values, options = {})
|
||||
# sanitize_for_mass_assignment(values, options[:as]).each do |k, v|
|
||||
# send("#{k}=", v)
|
||||
# end
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# When using a :default scope :
|
||||
#
|
||||
# customer = Customer.new
|
||||
# customer.attributes = { "name" => "David", "credit_rating" => "Excellent" }
|
||||
# customer.assign_attributes({ "name" => "David", "credit_rating" => "Excellent", :last_login => 1.day.ago }, :as => :default)
|
||||
# customer.name # => "David"
|
||||
# customer.credit_rating # => nil
|
||||
# customer.last_login # => nil
|
||||
#
|
||||
# customer.credit_rating = "Average"
|
||||
# customer.credit_rating # => "Average"
|
||||
#
|
||||
# And using the :admin scope :
|
||||
#
|
||||
# customer = Customer.new
|
||||
# customer.assign_attributes({ "name" => "David", "credit_rating" => "Excellent", :last_login => 1.day.ago }, :as => :admin)
|
||||
# customer.name # => "David"
|
||||
# customer.credit_rating # => "Excellent"
|
||||
# customer.last_login # => nil
|
||||
#
|
||||
# To start from an all-closed default and enable attributes as needed,
|
||||
# have a look at +attr_accessible+.
|
||||
#
|
||||
# Note that using <tt>Hash#except</tt> or <tt>Hash#slice</tt> in place of +attr_protected+
|
||||
# to sanitize attributes won't provide sufficient protection.
|
||||
def attr_protected(*names)
|
||||
self._protected_attributes = self.protected_attributes + names
|
||||
def attr_protected(*args)
|
||||
options = args.extract_options!
|
||||
scope = options[:as] || :default
|
||||
|
||||
self._protected_attributes = protected_attributes_configs.dup
|
||||
self._protected_attributes[scope] = self.protected_attributes(scope) + args
|
||||
|
||||
self._active_authorizer = self._protected_attributes
|
||||
end
|
||||
|
||||
# Specifies a white list of model attributes that can be set via
|
||||
# mass-assignment.
|
||||
#
|
||||
# Like +attr_protected+, a scope for the attributes is optional,
|
||||
# if no scope is provided then :default is used. A scope can be defined by
|
||||
# using the :as option.
|
||||
#
|
||||
# This is the opposite of the +attr_protected+ macro: Mass-assignment
|
||||
# will only set attributes in this list, to assign to the rest of
|
||||
# attributes you can use direct writer methods. This is meant to protect
|
||||
@@ -102,57 +120,90 @@ module ActiveModel
|
||||
# include ActiveModel::MassAssignmentSecurity
|
||||
#
|
||||
# attr_accessor :name, :credit_rating
|
||||
# attr_accessible :name
|
||||
#
|
||||
# def attributes=(values)
|
||||
# sanitize_for_mass_assignment(values).each do |k, v|
|
||||
# attr_accessible :name
|
||||
# attr_accessible :name, :credit_rating, :as => :admin
|
||||
#
|
||||
# def assign_attributes(values, options = {})
|
||||
# sanitize_for_mass_assignment(values, options[:as]).each do |k, v|
|
||||
# send("#{k}=", v)
|
||||
# end
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# When using a :default scope :
|
||||
#
|
||||
# customer = Customer.new
|
||||
# customer.attributes = { :name => "David", :credit_rating => "Excellent" }
|
||||
# customer.assign_attributes({ "name" => "David", "credit_rating" => "Excellent", :last_login => 1.day.ago }, :as => :default)
|
||||
# customer.name # => "David"
|
||||
# customer.credit_rating # => nil
|
||||
#
|
||||
# customer.credit_rating = "Average"
|
||||
# customer.credit_rating # => "Average"
|
||||
#
|
||||
# And using the :admin scope :
|
||||
#
|
||||
# customer = Customer.new
|
||||
# customer.assign_attributes({ "name" => "David", "credit_rating" => "Excellent", :last_login => 1.day.ago }, :as => :admin)
|
||||
# customer.name # => "David"
|
||||
# customer.credit_rating # => "Excellent"
|
||||
#
|
||||
# Note that using <tt>Hash#except</tt> or <tt>Hash#slice</tt> in place of +attr_accessible+
|
||||
# to sanitize attributes won't provide sufficient protection.
|
||||
def attr_accessible(*names)
|
||||
self._accessible_attributes = self.accessible_attributes + names
|
||||
def attr_accessible(*args)
|
||||
options = args.extract_options!
|
||||
scope = options[:as] || :default
|
||||
|
||||
self._accessible_attributes = accessible_attributes_configs.dup
|
||||
self._accessible_attributes[scope] = self.accessible_attributes(scope) + args
|
||||
|
||||
self._active_authorizer = self._accessible_attributes
|
||||
end
|
||||
|
||||
def protected_attributes
|
||||
self._protected_attributes ||= BlackList.new(attributes_protected_by_default).tap do |w|
|
||||
w.logger = self.logger if self.respond_to?(:logger)
|
||||
end
|
||||
def protected_attributes(scope = :default)
|
||||
protected_attributes_configs[scope]
|
||||
end
|
||||
|
||||
def accessible_attributes
|
||||
self._accessible_attributes ||= WhiteList.new.tap { |w| w.logger = self.logger if self.respond_to?(:logger) }
|
||||
def accessible_attributes(scope = :default)
|
||||
accessible_attributes_configs[scope]
|
||||
end
|
||||
|
||||
def active_authorizer
|
||||
self._active_authorizer ||= protected_attributes
|
||||
def active_authorizers
|
||||
self._active_authorizer ||= protected_attributes_configs
|
||||
end
|
||||
alias active_authorizer active_authorizers
|
||||
|
||||
def attributes_protected_by_default
|
||||
[]
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def protected_attributes_configs
|
||||
self._protected_attributes ||= begin
|
||||
default_black_list = BlackList.new(attributes_protected_by_default).tap do |w|
|
||||
w.logger = self.logger if self.respond_to?(:logger)
|
||||
end
|
||||
Hash.new(default_black_list)
|
||||
end
|
||||
end
|
||||
|
||||
def accessible_attributes_configs
|
||||
self._accessible_attributes ||= begin
|
||||
default_white_list = WhiteList.new.tap { |w| w.logger = self.logger if self.respond_to?(:logger) }
|
||||
Hash.new(default_white_list)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def sanitize_for_mass_assignment(attributes)
|
||||
mass_assignment_authorizer.sanitize(attributes)
|
||||
def sanitize_for_mass_assignment(attributes, scope = :default)
|
||||
mass_assignment_authorizer(scope).sanitize(attributes)
|
||||
end
|
||||
|
||||
def mass_assignment_authorizer
|
||||
self.class.active_authorizer
|
||||
def mass_assignment_authorizer(scope = :default)
|
||||
self.class.active_authorizer[scope]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -10,10 +10,27 @@ class MassAssignmentSecurityTest < ActiveModel::TestCase
|
||||
assert_equal expected, sanitized
|
||||
end
|
||||
|
||||
def test_only_moderator_scope_attribute_accessible
|
||||
user = SpecialUser.new
|
||||
expected = { "name" => "John Smith", "email" => "john@smith.com" }
|
||||
sanitized = user.sanitize_for_mass_assignment(expected.merge("admin" => true), :moderator)
|
||||
assert_equal expected, sanitized
|
||||
|
||||
sanitized = user.sanitize_for_mass_assignment({ "name" => "John Smith", "email" => "john@smith.com", "admin" => true })
|
||||
assert_equal({}, sanitized)
|
||||
end
|
||||
|
||||
def test_attributes_accessible
|
||||
user = Person.new
|
||||
expected = { "name" => "John Smith", "email" => "john@smith.com" }
|
||||
sanitized = user.sanitize_for_mass_assignment(expected.merge("super_powers" => true))
|
||||
sanitized = user.sanitize_for_mass_assignment(expected.merge("admin" => true))
|
||||
assert_equal expected, sanitized
|
||||
end
|
||||
|
||||
def test_admin_scoped_attributes_accessible
|
||||
user = Person.new
|
||||
expected = { "name" => "John Smith", "email" => "john@smith.com", "admin" => true }
|
||||
sanitized = user.sanitize_for_mass_assignment(expected.merge("super_powers" => true), :admin)
|
||||
assert_equal expected, sanitized
|
||||
end
|
||||
|
||||
@@ -26,20 +43,30 @@ class MassAssignmentSecurityTest < ActiveModel::TestCase
|
||||
|
||||
def test_mass_assignment_protection_inheritance
|
||||
assert_blank LoosePerson.accessible_attributes
|
||||
assert_equal Set.new([ 'credit_rating', 'administrator']), LoosePerson.protected_attributes
|
||||
assert_equal Set.new(['credit_rating', 'administrator']), LoosePerson.protected_attributes
|
||||
|
||||
assert_blank LoosePerson.accessible_attributes
|
||||
assert_equal Set.new(['credit_rating']), LoosePerson.protected_attributes(:admin)
|
||||
|
||||
assert_blank LooseDescendant.accessible_attributes
|
||||
assert_equal Set.new([ 'credit_rating', 'administrator', 'phone_number']), LooseDescendant.protected_attributes
|
||||
assert_equal Set.new(['credit_rating', 'administrator', 'phone_number']), LooseDescendant.protected_attributes
|
||||
|
||||
assert_blank LooseDescendantSecond.accessible_attributes
|
||||
assert_equal Set.new([ 'credit_rating', 'administrator', 'phone_number', 'name']), LooseDescendantSecond.protected_attributes,
|
||||
assert_equal Set.new(['credit_rating', 'administrator', 'phone_number', 'name']), LooseDescendantSecond.protected_attributes,
|
||||
'Running attr_protected twice in one class should merge the protections'
|
||||
|
||||
assert_blank TightPerson.protected_attributes - TightPerson.attributes_protected_by_default
|
||||
assert_equal Set.new([ 'name', 'address' ]), TightPerson.accessible_attributes
|
||||
assert_equal Set.new(['name', 'address']), TightPerson.accessible_attributes
|
||||
|
||||
assert_blank TightPerson.protected_attributes(:admin) - TightPerson.attributes_protected_by_default
|
||||
assert_equal Set.new(['name', 'address', 'admin']), TightPerson.accessible_attributes(:admin)
|
||||
|
||||
assert_blank TightDescendant.protected_attributes - TightDescendant.attributes_protected_by_default
|
||||
assert_equal Set.new([ 'name', 'address', 'phone_number' ]), TightDescendant.accessible_attributes
|
||||
assert_equal Set.new(['name', 'address', 'phone_number']), TightDescendant.accessible_attributes
|
||||
|
||||
assert_blank TightDescendant.protected_attributes(:admin) - TightDescendant.attributes_protected_by_default
|
||||
assert_equal Set.new(['name', 'address', 'admin', 'super_powers']), TightDescendant.accessible_attributes(:admin)
|
||||
|
||||
end
|
||||
|
||||
def test_mass_assignment_multiparameter_protector
|
||||
|
||||
@@ -45,13 +45,14 @@ class SecurePasswordTest < ActiveModel::TestCase
|
||||
end
|
||||
|
||||
test "visitor#password_digest should be protected against mass assignment" do
|
||||
assert Visitor.active_authorizer.kind_of?(ActiveModel::MassAssignmentSecurity::BlackList)
|
||||
assert Visitor.active_authorizer.include?(:password_digest)
|
||||
assert Visitor.active_authorizers[:default].kind_of?(ActiveModel::MassAssignmentSecurity::BlackList)
|
||||
assert Visitor.active_authorizers[:default].include?(:password_digest)
|
||||
end
|
||||
|
||||
test "Administrator's mass_assignment_authorizer should be WhiteList" do
|
||||
assert Administrator.active_authorizer.kind_of?(ActiveModel::MassAssignmentSecurity::WhiteList)
|
||||
assert !Administrator.active_authorizer.include?(:password_digest)
|
||||
assert Administrator.active_authorizer.include?(:name)
|
||||
active_authorizer = Administrator.active_authorizers[:default]
|
||||
assert active_authorizer.kind_of?(ActiveModel::MassAssignmentSecurity::WhiteList)
|
||||
assert !active_authorizer.include?(:password_digest)
|
||||
assert active_authorizer.include?(:name)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -5,9 +5,17 @@ class User
|
||||
public :sanitize_for_mass_assignment
|
||||
end
|
||||
|
||||
class SpecialUser
|
||||
include ActiveModel::MassAssignmentSecurity
|
||||
attr_accessible :name, :email, :as => :moderator
|
||||
|
||||
public :sanitize_for_mass_assignment
|
||||
end
|
||||
|
||||
class Person
|
||||
include ActiveModel::MassAssignmentSecurity
|
||||
attr_accessible :name, :email
|
||||
attr_accessible :name, :email, :admin, :as => :admin
|
||||
|
||||
public :sanitize_for_mass_assignment
|
||||
end
|
||||
@@ -32,6 +40,7 @@ end
|
||||
class LoosePerson
|
||||
include ActiveModel::MassAssignmentSecurity
|
||||
attr_protected :credit_rating, :administrator
|
||||
attr_protected :credit_rating, :as => :admin
|
||||
end
|
||||
|
||||
class LooseDescendant < LoosePerson
|
||||
@@ -46,6 +55,7 @@ end
|
||||
class TightPerson
|
||||
include ActiveModel::MassAssignmentSecurity
|
||||
attr_accessible :name, :address
|
||||
attr_accessible :name, :address, :admin, :as => :admin
|
||||
|
||||
def self.attributes_protected_by_default
|
||||
["mobile_number"]
|
||||
@@ -54,4 +64,5 @@ end
|
||||
|
||||
class TightDescendant < TightPerson
|
||||
attr_accessible :phone_number
|
||||
attr_accessible :super_powers, :as => :admin
|
||||
end
|
||||
Reference in New Issue
Block a user