diff options
Diffstat (limited to 'activemodel')
17 files changed, 142 insertions, 70 deletions
diff --git a/activemodel/CHANGELOG b/activemodel/CHANGELOG index be4de2e53c..b1ad315c46 100644 --- a/activemodel/CHANGELOG +++ b/activemodel/CHANGELOG @@ -1,3 +1,5 @@ +* Provide mass_assignment_sanitizer as an easy API to replace the sanitizer behavior. Also support both :logger (default) and :strict sanitizer behavior [Bogdan Gusiev] + *Rails 3.1.0 (unreleased)* * attr_accessible and friends now accepts :as as option to specify a role [Josh Kalderimis] diff --git a/activemodel/lib/active_model/errors.rb b/activemodel/lib/active_model/errors.rb index 0e2a2fd5e2..71ece15b71 100644 --- a/activemodel/lib/active_model/errors.rb +++ b/activemodel/lib/active_model/errors.rb @@ -248,7 +248,7 @@ module ActiveModel # # company = Company.create(:address => '123 First St.') # company.errors.full_messages # => - # ["Name is too short (minimum is 5 characters)", "Name can't be blank", "Address can't be blank"] + # ["Name is too short (minimum is 5 characters)", "Name can't be blank", "Email can't be blank"] def full_messages map { |attribute, message| if attribute == :base diff --git a/activemodel/lib/active_model/mass_assignment_security.rb b/activemodel/lib/active_model/mass_assignment_security.rb index 483b577681..a7b4706906 100644 --- a/activemodel/lib/active_model/mass_assignment_security.rb +++ b/activemodel/lib/active_model/mass_assignment_security.rb @@ -1,5 +1,7 @@ -require 'active_support/core_ext/class/attribute.rb' +require 'active_support/core_ext/class/attribute' +require 'active_support/core_ext/string/inflections' require 'active_model/mass_assignment_security/permission_set' +require 'active_model/mass_assignment_security/sanitizer' module ActiveModel # = Active Model Mass-Assignment Security @@ -10,6 +12,9 @@ module ActiveModel class_attribute :_accessible_attributes class_attribute :_protected_attributes class_attribute :_active_authorizer + + class_attribute :_mass_assignment_sanitizer + self.mass_assignment_sanitizer = :logger end # Mass assignment security provides an interface for protecting attributes @@ -41,6 +46,16 @@ module ActiveModel # # 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 @@ -154,7 +169,7 @@ module ActiveModel options = args.extract_options! role = options[:as] || :default - self._accessible_attributes = accessible_attributes_configs.dup + self._accessible_attributes = accessible_attributes_configs.dup self._accessible_attributes[role] = self.accessible_attributes(role) + args self._active_authorizer = self._accessible_attributes @@ -177,21 +192,25 @@ module ActiveModel [] 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 - 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) + Hash.new { |h,k| h[k] = BlackList.new(attributes_protected_by_default) } 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) + Hash.new { |h,k| h[k] = WhiteList.new } end end end @@ -199,7 +218,7 @@ module ActiveModel protected def sanitize_for_mass_assignment(attributes, role = :default) - mass_assignment_authorizer(role).sanitize(attributes) + _mass_assignment_sanitizer.sanitize(attributes, mass_assignment_authorizer(role)) end def mass_assignment_authorizer(role = :default) diff --git a/activemodel/lib/active_model/mass_assignment_security/permission_set.rb b/activemodel/lib/active_model/mass_assignment_security/permission_set.rb index 9fcb94d48a..a1fcdf1a38 100644 --- a/activemodel/lib/active_model/mass_assignment_security/permission_set.rb +++ b/activemodel/lib/active_model/mass_assignment_security/permission_set.rb @@ -1,10 +1,8 @@ require 'set' -require 'active_model/mass_assignment_security/sanitizer' module ActiveModel module MassAssignmentSecurity class PermissionSet < Set - attr_accessor :logger def +(values) super(values.map(&:to_s)) @@ -14,6 +12,10 @@ module ActiveModel super(remove_multiparameter_id(key)) end + def deny?(key) + raise NotImplementedError, "#deny?(key) suppose to be overwritten" + end + protected def remove_multiparameter_id(key) @@ -22,7 +24,6 @@ module ActiveModel end class WhiteList < PermissionSet - include Sanitizer def deny?(key) !include?(key) @@ -30,7 +31,6 @@ module ActiveModel end class BlackList < PermissionSet - include Sanitizer def deny?(key) include?(key) diff --git a/activemodel/lib/active_model/mass_assignment_security/sanitizer.rb b/activemodel/lib/active_model/mass_assignment_security/sanitizer.rb index 150beb1ff2..ee43a6694f 100644 --- a/activemodel/lib/active_model/mass_assignment_security/sanitizer.rb +++ b/activemodel/lib/active_model/mass_assignment_security/sanitizer.rb @@ -1,9 +1,14 @@ +require 'active_support/core_ext/module/delegation' + module ActiveModel module MassAssignmentSecurity - module Sanitizer + class Sanitizer + def initialize(target=nil) + end + # Returns all attributes not denied by the authorizer. - def sanitize(attributes) - sanitized_attributes = attributes.reject { |key, value| deny?(key) } + def sanitize(attributes, authorizer) + sanitized_attributes = attributes.reject { |key, value| authorizer.deny?(key) } debug_protected_attribute_removal(attributes, sanitized_attributes) sanitized_attributes end @@ -12,12 +17,38 @@ module ActiveModel def debug_protected_attribute_removal(attributes, sanitized_attributes) removed_keys = attributes.keys - sanitized_attributes.keys - warn!(removed_keys) if removed_keys.any? + process_removed_attributes(removed_keys) if removed_keys.any? + end + + def process_removed_attributes(attrs) + raise NotImplementedError, "#process_removed_attributes(attrs) suppose to be overwritten" + end + end + + class LoggerSanitizer < Sanitizer + delegate :logger, :to => :@target + + def initialize(target) + @target = target + super end - def warn!(attrs) - self.logger.debug "WARNING: Can't mass-assign protected attributes: #{attrs.join(', ')}" if self.logger + def logger? + @target.respond_to?(:logger) && @target.logger end + + def process_removed_attributes(attrs) + logger.debug "WARNING: Can't mass-assign protected attributes: #{attrs.join(', ')}" if logger? + end + end + + class StrictSanitizer < Sanitizer + def process_removed_attributes(attrs) + raise ActiveModel::MassAssignmentSecurity::Error, "Can't mass-assign protected attributes: #{attrs.join(', ')}" + end + end + + class Error < StandardError end end end diff --git a/activemodel/lib/active_model/naming.rb b/activemodel/lib/active_model/naming.rb index 4c1a82f413..c7b9c41f46 100644 --- a/activemodel/lib/active_model/naming.rb +++ b/activemodel/lib/active_model/naming.rb @@ -21,7 +21,7 @@ module ActiveModel @partial_path = "#{@collection}/#{@element}".freeze @param_key = (namespace ? _singularize(@unnamespaced) : @singular).freeze @route_key = (namespace ? ActiveSupport::Inflector.pluralize(@param_key) : @plural).freeze - @i18n_key = self.underscore.to_sym + @i18n_key = self.underscore.tr('/', '.').to_sym end # Transform the model name into a more humane format, using I18n. By default, @@ -35,8 +35,9 @@ module ActiveModel @klass.respond_to?(:i18n_scope) defaults = @klass.lookup_ancestors.map do |klass| - klass.model_name.i18n_key - end + [klass.model_name.i18n_key, + klass.model_name.i18n_key.to_s.tr('.', '/').to_sym] + end.flatten defaults << options[:default] if options[:default] defaults << @human diff --git a/activemodel/lib/active_model/serializers/json.rb b/activemodel/lib/active_model/serializers/json.rb index 0bfbf2aa06..0405705b35 100644 --- a/activemodel/lib/active_model/serializers/json.rb +++ b/activemodel/lib/active_model/serializers/json.rb @@ -22,13 +22,13 @@ module ActiveModel # of +as_json+. If true (the default) +as_json+ will emit a single root # node named after the object's type. For example: # - # konata = User.find(1) - # konata.as_json + # user = User.find(1) + # user.as_json # # => { "user": {"id": 1, "name": "Konata Izumi", "age": 16, # "created_at": "2006/08/01", "awesome": true} } # # ActiveRecord::Base.include_root_in_json = false - # konata.as_json + # user.as_json # # => {"id": 1, "name": "Konata Izumi", "age": 16, # "created_at": "2006/08/01", "awesome": true} # @@ -38,30 +38,30 @@ module ActiveModel # Without any +options+, the returned JSON string will include all the model's # attributes. For example: # - # konata = User.find(1) - # konata.as_json + # user = User.find(1) + # user.as_json # # => {"id": 1, "name": "Konata Izumi", "age": 16, # "created_at": "2006/08/01", "awesome": true} # # The <tt>:only</tt> and <tt>:except</tt> options can be used to limit the attributes # included, and work similar to the +attributes+ method. For example: # - # konata.as_json(:only => [ :id, :name ]) + # user.as_json(:only => [ :id, :name ]) # # => {"id": 1, "name": "Konata Izumi"} # - # konata.as_json(:except => [ :id, :created_at, :age ]) + # user.as_json(:except => [ :id, :created_at, :age ]) # # => {"name": "Konata Izumi", "awesome": true} # # To include the result of some method calls on the model use <tt>:methods</tt>: # - # konata.as_json(:methods => :permalink) + # user.as_json(:methods => :permalink) # # => {"id": 1, "name": "Konata Izumi", "age": 16, # "created_at": "2006/08/01", "awesome": true, # "permalink": "1-konata-izumi"} # # To include associations use <tt>:include</tt>: # - # konata.as_json(:include => :posts) + # user.as_json(:include => :posts) # # => {"id": 1, "name": "Konata Izumi", "age": 16, # "created_at": "2006/08/01", "awesome": true, # "posts": [{"id": 1, "author_id": 1, "title": "Welcome to the weblog"}, @@ -69,7 +69,7 @@ module ActiveModel # # Second level and higher order associations work as well: # - # konata.as_json(:include => { :posts => { + # user.as_json(:include => { :posts => { # :include => { :comments => { # :only => :body } }, # :only => :title } }) diff --git a/activemodel/lib/active_model/serializers/xml.rb b/activemodel/lib/active_model/serializers/xml.rb index f6aae24324..9812af43d6 100644 --- a/activemodel/lib/active_model/serializers/xml.rb +++ b/activemodel/lib/active_model/serializers/xml.rb @@ -139,8 +139,8 @@ module ActiveModel # Without any +options+, the returned XML string will include all the model's # attributes. For example: # - # konata = User.find(1) - # konata.to_xml + # user = User.find(1) + # user.to_xml # # <?xml version="1.0" encoding="UTF-8"?> # <user> diff --git a/activemodel/lib/active_model/translation.rb b/activemodel/lib/active_model/translation.rb index 6d64c81b5f..c615311692 100644 --- a/activemodel/lib/active_model/translation.rb +++ b/activemodel/lib/active_model/translation.rb @@ -44,8 +44,9 @@ module ActiveModel # Specify +options+ with additional translating options. def human_attribute_name(attribute, options = {}) defaults = lookup_ancestors.map do |klass| - :"#{self.i18n_scope}.attributes.#{klass.model_name.i18n_key}.#{attribute}" - end + [:"#{self.i18n_scope}.attributes.#{klass.model_name.i18n_key}.#{attribute}", + :"#{self.i18n_scope}.attributes.#{klass.model_name.i18n_key.to_s.tr('.', '/')}.#{attribute}"] + end.flatten defaults << :"attributes.#{attribute}" defaults << options.delete(:default) if options[:default] diff --git a/activemodel/lib/active_model/validations/exclusion.rb b/activemodel/lib/active_model/validations/exclusion.rb index ab4de3c459..d3b8d31502 100644 --- a/activemodel/lib/active_model/validations/exclusion.rb +++ b/activemodel/lib/active_model/validations/exclusion.rb @@ -1,4 +1,4 @@ -require 'active_support/core_ext/range.rb' +require 'active_support/core_ext/range' module ActiveModel diff --git a/activemodel/lib/active_model/validations/inclusion.rb b/activemodel/lib/active_model/validations/inclusion.rb index bfb65d2f3f..9a9270d615 100644 --- a/activemodel/lib/active_model/validations/inclusion.rb +++ b/activemodel/lib/active_model/validations/inclusion.rb @@ -1,4 +1,4 @@ -require 'active_support/core_ext/range.rb' +require 'active_support/core_ext/range' module ActiveModel diff --git a/activemodel/lib/active_model/validations/length.rb b/activemodel/lib/active_model/validations/length.rb index 48d14978fa..144e73904e 100644 --- a/activemodel/lib/active_model/validations/length.rb +++ b/activemodel/lib/active_model/validations/length.rb @@ -62,14 +62,14 @@ module ActiveModel # Validates that the specified attribute matches the length restrictions supplied. Only one option can be used at a time: # # class Person < ActiveRecord::Base - # validates_length_of :first_name, :maximum=>30 - # validates_length_of :last_name, :maximum=>30, :message=>"less than 30 if you don't mind" + # validates_length_of :first_name, :maximum => 30 + # validates_length_of :last_name, :maximum => 30, :message => "less than 30 if you don't mind" # validates_length_of :fax, :in => 7..32, :allow_nil => true # validates_length_of :phone, :in => 7..32, :allow_blank => true # validates_length_of :user_name, :within => 6..20, :too_long => "pick a shorter name", :too_short => "pick a longer name" # validates_length_of :zip_code, :minimum => 5, :too_short => "please enter at least 5 characters" # validates_length_of :smurf_leader, :is => 4, :message => "papa is spelled with 4 characters... don't play me." - # validates_length_of :essay, :minimum => 100, :too_short => "Your essay must be at least 100 words."), :tokenizer => lambda {|str| str.scan(/\w+/) } + # validates_length_of :essay, :minimum => 100, :too_short => "Your essay must be at least 100 words.", :tokenizer => lambda { |str| str.scan(/\w+/) } # end # # Configuration options: diff --git a/activemodel/test/cases/mass_assignment_security/black_list_test.rb b/activemodel/test/cases/mass_assignment_security/black_list_test.rb index ed168bc016..0ec7f8719c 100644 --- a/activemodel/test/cases/mass_assignment_security/black_list_test.rb +++ b/activemodel/test/cases/mass_assignment_security/black_list_test.rb @@ -16,13 +16,5 @@ class BlackListTest < ActiveModel::TestCase assert_equal false, @black_list.deny?('first_name') end - test "sanitize attributes" do - original_attributes = { 'first_name' => 'allowed', 'admin' => 'denied', 'admin(1)' => 'denied' } - attributes = @black_list.sanitize(original_attributes) - - assert attributes.key?('first_name'), "Allowed key shouldn't be rejected" - assert !attributes.key?('admin'), "Denied key should be rejected" - assert !attributes.key?('admin(1)'), "Multi-parameter key should be detected" - end end diff --git a/activemodel/test/cases/mass_assignment_security/sanitizer_test.rb b/activemodel/test/cases/mass_assignment_security/sanitizer_test.rb index 9a73a5ad91..62a6ec9c9b 100644 --- a/activemodel/test/cases/mass_assignment_security/sanitizer_test.rb +++ b/activemodel/test/cases/mass_assignment_security/sanitizer_test.rb @@ -3,36 +3,41 @@ require 'logger' require 'active_support/core_ext/object/inclusion' class SanitizerTest < ActiveModel::TestCase + attr_accessor :logger - class SanitizingAuthorizer - include ActiveModel::MassAssignmentSecurity::Sanitizer - - attr_accessor :logger - + class Authorizer < ActiveModel::MassAssignmentSecurity::PermissionSet def deny?(key) key.in?(['admin']) end - end def setup - @sanitizer = SanitizingAuthorizer.new + @logger_sanitizer = ActiveModel::MassAssignmentSecurity::LoggerSanitizer.new(self) + @strict_sanitizer = ActiveModel::MassAssignmentSecurity::StrictSanitizer.new(self) + @authorizer = Authorizer.new end test "sanitize attributes" do original_attributes = { 'first_name' => 'allowed', 'admin' => 'denied' } - attributes = @sanitizer.sanitize(original_attributes) + attributes = @logger_sanitizer.sanitize(original_attributes, @authorizer) assert attributes.key?('first_name'), "Allowed key shouldn't be rejected" assert !attributes.key?('admin'), "Denied key should be rejected" end - test "debug mass assignment removal" do + test "debug mass assignment removal with LoggerSanitizer" do original_attributes = { 'first_name' => 'allowed', 'admin' => 'denied' } log = StringIO.new - @sanitizer.logger = Logger.new(log) - @sanitizer.sanitize(original_attributes) + self.logger = Logger.new(log) + @logger_sanitizer.sanitize(original_attributes, @authorizer) assert_match(/admin/, log.string, "Should log removed attributes: #{log.string}") end + test "debug mass assignment removal with StrictSanitizer" do + original_attributes = { 'first_name' => 'allowed', 'admin' => 'denied' } + assert_raise ActiveModel::MassAssignmentSecurity::Error do + @strict_sanitizer.sanitize(original_attributes, @authorizer) + end + end + end diff --git a/activemodel/test/cases/mass_assignment_security/white_list_test.rb b/activemodel/test/cases/mass_assignment_security/white_list_test.rb index aa3596ad2a..737b55492a 100644 --- a/activemodel/test/cases/mass_assignment_security/white_list_test.rb +++ b/activemodel/test/cases/mass_assignment_security/white_list_test.rb @@ -16,13 +16,4 @@ class WhiteListTest < ActiveModel::TestCase assert_equal true, @white_list.deny?('admin') end - test "sanitize attributes" do - original_attributes = { 'first_name' => 'allowed', 'admin' => 'denied', 'admin(1)' => 'denied' } - attributes = @white_list.sanitize(original_attributes) - - assert attributes.key?('first_name'), "Allowed key shouldn't be rejected" - assert !attributes.key?('admin'), "Denied key should be rejected" - assert !attributes.key?('admin(1)'), "Multi-parameter key should be detected" - end - end diff --git a/activemodel/test/cases/mass_assignment_security_test.rb b/activemodel/test/cases/mass_assignment_security_test.rb index 43a12eed61..a778240827 100644 --- a/activemodel/test/cases/mass_assignment_security_test.rb +++ b/activemodel/test/cases/mass_assignment_security_test.rb @@ -1,6 +1,15 @@ require "cases/helper" require 'models/mass_assignment_specific' + +class CustomSanitizer < ActiveModel::MassAssignmentSecurity::Sanitizer + + def process_removed_attributes(attrs) + raise StandardError + end + +end + class MassAssignmentSecurityTest < ActiveModel::TestCase def test_attribute_protection @@ -76,4 +85,15 @@ class MassAssignmentSecurityTest < ActiveModel::TestCase assert_equal sanitized, { } end + def test_custom_sanitizer + user = User.new + User.mass_assignment_sanitizer = CustomSanitizer.new + assert_raise StandardError do + user.sanitize_for_mass_assignment("admin" => true) + end + ensure + User.mass_assignment_sanitizer = nil + + end + end diff --git a/activemodel/test/cases/translation_test.rb b/activemodel/test/cases/translation_test.rb index 1b1d972d5c..838956bc98 100644 --- a/activemodel/test/cases/translation_test.rb +++ b/activemodel/test/cases/translation_test.rb @@ -76,5 +76,15 @@ class ActiveModelI18nTests < ActiveModel::TestCase Person.model_name.human(options) assert_equal({:default => 'person model'}, options) end + + def test_alternate_namespaced_model_attribute_translation + I18n.backend.store_translations 'en', :activemodel => {:attributes => {:person => {:gender => {:attribute => 'person gender attribute'}}}} + assert_equal 'person gender attribute', Person::Gender.human_attribute_name('attribute') + end + + def test_alternate_namespaced_model_translation + I18n.backend.store_translations 'en', :activemodel => {:models => {:person => {:gender => 'person gender model'}}} + assert_equal 'person gender model', Person::Gender.model_name.human + end end |