diff options
author | Gonçalo Silva <goncalossilva@gmail.com> | 2010-07-10 17:36:10 +0100 |
---|---|---|
committer | Gonçalo Silva <goncalossilva@gmail.com> | 2010-07-10 17:36:10 +0100 |
commit | cd2bbed9846d84a1230a1b9e52843eedca17b28d (patch) | |
tree | 5214b7855f3d102e4c22239b9d62bc5717cb3547 /activemodel/lib | |
parent | d2c633ba0bfb7baacdee89a46d7d036d24c68817 (diff) | |
parent | 80e47d7b88dcc732ebeb5290faab6e529829dac6 (diff) | |
download | rails-cd2bbed9846d84a1230a1b9e52843eedca17b28d.tar.gz rails-cd2bbed9846d84a1230a1b9e52843eedca17b28d.tar.bz2 rails-cd2bbed9846d84a1230a1b9e52843eedca17b28d.zip |
Merge branch 'master' of http://github.com/rails/rails
Diffstat (limited to 'activemodel/lib')
6 files changed, 226 insertions, 0 deletions
diff --git a/activemodel/lib/active_model.rb b/activemodel/lib/active_model.rb index 026430fee3..5ed21a39c2 100644 --- a/activemodel/lib/active_model.rb +++ b/activemodel/lib/active_model.rb @@ -38,6 +38,7 @@ module ActiveModel autoload :EachValidator, 'active_model/validator' autoload :Errors autoload :Lint + autoload :MassAssignmentSecurity autoload :Name, 'active_model/naming' autoload :Naming autoload :Observer, 'active_model/observing' diff --git a/activemodel/lib/active_model/errors.rb b/activemodel/lib/active_model/errors.rb index d42fc5291d..482b3dac47 100644 --- a/activemodel/lib/active_model/errors.rb +++ b/activemodel/lib/active_model/errors.rb @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- require 'active_support/core_ext/array/wrap' +require 'active_support/core_ext/array/conversions' require 'active_support/core_ext/string/inflections' require 'active_support/core_ext/object/blank' require 'active_support/core_ext/hash/reverse_merge' diff --git a/activemodel/lib/active_model/mass_assignment_security.rb b/activemodel/lib/active_model/mass_assignment_security.rb new file mode 100644 index 0000000000..66cd9fdde6 --- /dev/null +++ b/activemodel/lib/active_model/mass_assignment_security.rb @@ -0,0 +1,160 @@ +require 'active_support/core_ext/class/attribute.rb' +require 'active_model/mass_assignment_security/permission_set' + +module ActiveModel + # = Active Model Mass-Assignment Security + module MassAssignmentSecurity + extend ActiveSupport::Concern + + included do + class_attribute :_accessible_attributes + class_attribute :_protected_attributes + class_attribute :_active_authorizer + 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 + # + # def self.admin_accessible_attributes + # accessible_attributes + [ :plan_id ] + # end + # + # def update + # ... + # @account.update_attributes(account_params) + # ... + # end + # + # protected + # + # def account_params + # sanitize_for_mass_assignment(params[:account]) + # end + # + # def mass_assignment_authorizer + # admin ? admin_accessible_attributes : super + # end + # + # end + # + module ClassMethods + # Attributes named in this macro are protected from mass-assignment + # whenever attributes are sanitized before assignment. + # + # 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, :credit_rating + # attr_protected :credit_rating + # + # def attributes=(values) + # sanitize_for_mass_assignment(values).each do |k, v| + # send("#{k}=", v) + # end + # end + # end + # + # customer = Customer.new + # customer.attributes = { "name" => "David", "credit_rating" => "Excellent" } + # customer.name # => "David" + # customer.credit_rating # => nil + # + # customer.credit_rating = "Average" + # customer.credit_rating # => "Average" + # + # 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 + self._active_authorizer = self._protected_attributes + end + + # Specifies a white list of model attributes that can be set via + # mass-assignment. + # + # 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 + # + # def attributes=(values) + # sanitize_for_mass_assignment(values).each do |k, v| + # send("#{k}=", v) + # end + # end + # end + # + # customer = Customer.new + # customer.attributes = { :name => "David", :credit_rating => "Excellent" } + # customer.name # => "David" + # customer.credit_rating # => nil + # + # customer.credit_rating = "Average" + # customer.credit_rating # => "Average" + # + # 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 + 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 + end + + def accessible_attributes + self._accessible_attributes ||= WhiteList.new.tap { |w| w.logger = self.logger if self.respond_to?(:logger) } + end + + def active_authorizer + self._active_authorizer ||= protected_attributes + end + + def attributes_protected_by_default + [] + end + end + + protected + + def sanitize_for_mass_assignment(attributes) + mass_assignment_authorizer.sanitize(attributes) + end + + def mass_assignment_authorizer + self.class.active_authorizer + end + end +end diff --git a/activemodel/lib/active_model/mass_assignment_security/permission_set.rb b/activemodel/lib/active_model/mass_assignment_security/permission_set.rb new file mode 100644 index 0000000000..7c48472799 --- /dev/null +++ b/activemodel/lib/active_model/mass_assignment_security/permission_set.rb @@ -0,0 +1,39 @@ +require 'active_model/mass_assignment_security/sanitizer' + +module ActiveModel + module MassAssignmentSecurity + class PermissionSet < Set + attr_accessor :logger + + def +(values) + super(values.map(&:to_s)) + end + + def include?(key) + super(remove_multiparameter_id(key)) + end + + protected + + def remove_multiparameter_id(key) + key.to_s.gsub(/\(.+/, '') + end + end + + class WhiteList < PermissionSet + include Sanitizer + + def deny?(key) + !include?(key) + end + end + + class BlackList < PermissionSet + include Sanitizer + + def deny?(key) + include?(key) + end + end + end +end
\ No newline at end of file diff --git a/activemodel/lib/active_model/mass_assignment_security/sanitizer.rb b/activemodel/lib/active_model/mass_assignment_security/sanitizer.rb new file mode 100644 index 0000000000..150beb1ff2 --- /dev/null +++ b/activemodel/lib/active_model/mass_assignment_security/sanitizer.rb @@ -0,0 +1,23 @@ +module ActiveModel + module MassAssignmentSecurity + module Sanitizer + # Returns all attributes not denied by the authorizer. + def sanitize(attributes) + sanitized_attributes = attributes.reject { |key, value| deny?(key) } + debug_protected_attribute_removal(attributes, sanitized_attributes) + sanitized_attributes + end + + protected + + def debug_protected_attribute_removal(attributes, sanitized_attributes) + removed_keys = attributes.keys - sanitized_attributes.keys + warn!(removed_keys) if removed_keys.any? + end + + def warn!(attrs) + self.logger.debug "WARNING: Can't mass-assign protected attributes: #{attrs.join(', ')}" if self.logger + end + end + end +end diff --git a/activemodel/lib/active_model/observing.rb b/activemodel/lib/active_model/observing.rb index d0f36ce3b1..c6a79acf81 100644 --- a/activemodel/lib/active_model/observing.rb +++ b/activemodel/lib/active_model/observing.rb @@ -1,6 +1,7 @@ require 'singleton' require 'active_support/core_ext/array/wrap' require 'active_support/core_ext/module/aliasing' +require 'active_support/core_ext/module/remove_method' require 'active_support/core_ext/string/inflections' module ActiveModel @@ -157,6 +158,7 @@ module ActiveModel def observe(*models) models.flatten! models.collect! { |model| model.respond_to?(:to_sym) ? model.to_s.camelize.constantize : model } + remove_possible_method(:observed_classes) define_method(:observed_classes) { models } end |