aboutsummaryrefslogtreecommitdiffstats
path: root/activemodel/lib
diff options
context:
space:
mode:
Diffstat (limited to 'activemodel/lib')
-rw-r--r--activemodel/lib/active_model.rb1
-rw-r--r--activemodel/lib/active_model/errors.rb1
-rw-r--r--activemodel/lib/active_model/mass_assignment_security.rb160
-rw-r--r--activemodel/lib/active_model/mass_assignment_security/permission_set.rb39
-rw-r--r--activemodel/lib/active_model/mass_assignment_security/sanitizer.rb23
-rw-r--r--activemodel/lib/active_model/observing.rb2
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