mirror of
https://github.com/rails/rails.git
synced 2022-11-09 12:12:34 -05:00
93c1f11c0a
The problem: We need to be able to specify configuration in a way that can be inherited to models that include ActiveRecord::Model. So it is no longer sufficient to put 'top level' config on ActiveRecord::Base, but we do want configuration specified on ActiveRecord::Base and descendants to continue to work. So we need something like class_attribute that can be defined on a module but that is inherited when ActiveRecord::Model is included. The solution: added ActiveModel::Configuration module which provides a config_attribute macro. It's a bit specific hence I am not putting this in Active Support or making it a 'public API' at present.
238 lines
8.4 KiB
Ruby
238 lines
8.4 KiB
Ruby
require 'active_support/core_ext/class/attribute'
|
|
require 'active_support/core_ext/string/inflections'
|
|
require 'active_support/core_ext/array/wrap'
|
|
require 'active_model/mass_assignment_security/permission_set'
|
|
require 'active_model/mass_assignment_security/sanitizer'
|
|
|
|
module ActiveModel
|
|
# = Active Model Mass-Assignment Security
|
|
module MassAssignmentSecurity
|
|
extend ActiveSupport::Concern
|
|
|
|
included do
|
|
extend ActiveModel::Configuration
|
|
|
|
config_attribute :_accessible_attributes
|
|
config_attribute :_protected_attributes
|
|
config_attribute :_active_authorizer
|
|
|
|
config_attribute :_mass_assignment_sanitizer
|
|
self.mass_assignment_sanitizer = :logger
|
|
end
|
|
|
|
# Mass assignment security provides an interface for protecting attributes
|
|
# from end-user assignment. For more complex permissions, mass assignment security
|
|
# may be handled outside the model by extending a non-ActiveRecord class,
|
|
# such as a controller, with this behavior.
|
|
#
|
|
# For example, a logged in user may need to assign additional attributes depending
|
|
# on their role:
|
|
#
|
|
# class AccountsController < ApplicationController
|
|
# include ActiveModel::MassAssignmentSecurity
|
|
#
|
|
# attr_accessible :first_name, :last_name
|
|
# attr_accessible :first_name, :last_name, :plan_id, :as => :admin
|
|
#
|
|
# def update
|
|
# ...
|
|
# @account.update_attributes(account_params)
|
|
# ...
|
|
# end
|
|
#
|
|
# protected
|
|
#
|
|
# def account_params
|
|
# role = admin ? :admin : :default
|
|
# sanitize_for_mass_assignment(params[:account], role)
|
|
# end
|
|
#
|
|
# end
|
|
#
|
|
# = Configuration options
|
|
#
|
|
# * <tt>mass_assignment_sanitizer</tt> - Defines sanitize method. Possible values are:
|
|
# * <tt>:logger</tt> (default) - writes filtered attributes to logger
|
|
# * <tt>:strict</tt> - raise <tt>ActiveModel::MassAssignmentSecurity::Error</tt> on any protected attribute update
|
|
#
|
|
# You can specify your own sanitizer object eg. MySanitizer.new.
|
|
# See <tt>ActiveModel::MassAssignmentSecurity::LoggerSanitizer</tt> for example implementation.
|
|
#
|
|
#
|
|
module ClassMethods
|
|
# Attributes named in this macro are protected from mass-assignment
|
|
# whenever attributes are sanitized before assignment. A role for the
|
|
# attributes is optional, if no role is provided then :default is used.
|
|
# A role 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
|
|
# sensitive attributes from being overwritten by malicious users
|
|
# tampering with URLs or forms. Example:
|
|
#
|
|
# class Customer
|
|
# include ActiveModel::MassAssignmentSecurity
|
|
#
|
|
# attr_accessor :name, :email, :logins_count
|
|
#
|
|
# attr_protected :logins_count
|
|
# # Suppose that admin can not change email for customer
|
|
# attr_protected :logins_count, :email, :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 the :default role :
|
|
#
|
|
# customer = Customer.new
|
|
# customer.assign_attributes({ "name" => "David", "email" => "a@b.com", :logins_count => 5 }, :as => :default)
|
|
# customer.name # => "David"
|
|
# customer.email # => "a@b.com"
|
|
# customer.logins_count # => nil
|
|
#
|
|
# And using the :admin role :
|
|
#
|
|
# customer = Customer.new
|
|
# customer.assign_attributes({ "name" => "David", "email" => "a@b.com", :logins_count => 5}, :as => :admin)
|
|
# customer.name # => "David"
|
|
# customer.email # => nil
|
|
# customer.logins_count # => nil
|
|
#
|
|
# customer.email = "c@d.com"
|
|
# customer.email # => "c@d.com"
|
|
#
|
|
# 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(*args)
|
|
options = args.extract_options!
|
|
role = options[:as] || :default
|
|
|
|
self._protected_attributes = protected_attributes_configs.dup
|
|
|
|
Array.wrap(role).each do |name|
|
|
self._protected_attributes[name] = self.protected_attributes(name) + args
|
|
end
|
|
|
|
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 role for the attributes is optional,
|
|
# if no role is provided then :default is used. A role 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
|
|
# sensitive attributes from being overwritten by malicious users
|
|
# tampering with URLs or forms. If you'd rather start from an all-open
|
|
# default and restrict attributes as needed, have a look at
|
|
# +attr_protected+.
|
|
#
|
|
# class Customer
|
|
# include ActiveModel::MassAssignmentSecurity
|
|
#
|
|
# attr_accessor :name, :credit_rating
|
|
#
|
|
# 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 the :default role :
|
|
#
|
|
# customer = Customer.new
|
|
# 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 role :
|
|
#
|
|
# 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(*args)
|
|
options = args.extract_options!
|
|
role = options[:as] || :default
|
|
|
|
self._accessible_attributes = accessible_attributes_configs.dup
|
|
|
|
Array.wrap(role).each do |name|
|
|
self._accessible_attributes[name] = self.accessible_attributes(name) + args
|
|
end
|
|
|
|
self._active_authorizer = self._accessible_attributes
|
|
end
|
|
|
|
def protected_attributes(role = :default)
|
|
protected_attributes_configs[role]
|
|
end
|
|
|
|
def accessible_attributes(role = :default)
|
|
accessible_attributes_configs[role]
|
|
end
|
|
|
|
def active_authorizers
|
|
self._active_authorizer ||= protected_attributes_configs
|
|
end
|
|
alias active_authorizer active_authorizers
|
|
|
|
def attributes_protected_by_default
|
|
[]
|
|
end
|
|
|
|
def mass_assignment_sanitizer=(value)
|
|
self._mass_assignment_sanitizer = if value.is_a?(Symbol)
|
|
const_get(:"#{value.to_s.camelize}Sanitizer").new(self)
|
|
else
|
|
value
|
|
end
|
|
end
|
|
|
|
private
|
|
|
|
def protected_attributes_configs
|
|
self._protected_attributes ||= begin
|
|
Hash.new { |h,k| h[k] = BlackList.new(attributes_protected_by_default) }
|
|
end
|
|
end
|
|
|
|
def accessible_attributes_configs
|
|
self._accessible_attributes ||= begin
|
|
Hash.new { |h,k| h[k] = WhiteList.new }
|
|
end
|
|
end
|
|
end
|
|
|
|
protected
|
|
|
|
def sanitize_for_mass_assignment(attributes, role = :default)
|
|
_mass_assignment_sanitizer.sanitize(attributes, mass_assignment_authorizer(role))
|
|
end
|
|
|
|
def mass_assignment_authorizer(role = :default)
|
|
self.class.active_authorizer[role]
|
|
end
|
|
end
|
|
end
|