Skip to content

Commit

Permalink
Add mass_assignment_options support to ActiveRecord
Browse files Browse the repository at this point in the history
  • Loading branch information
guilleiguaran committed Sep 19, 2012
1 parent a11bc9a commit 9a9f697
Show file tree
Hide file tree
Showing 12 changed files with 612 additions and 106 deletions.
104 changes: 0 additions & 104 deletions lib/active_record/attribute_sanitization.rb

This file was deleted.

47 changes: 47 additions & 0 deletions lib/active_record/mass_assignment_security.rb
@@ -0,0 +1,47 @@
require "active_record"
require "active_record/mass_assignment_security/associations"
require "active_record/mass_assignment_security/attribute_assignment"
require "active_record/mass_assignment_security/core"
require "active_record/mass_assignment_security/nested_attributes"
require "active_record/mass_assignment_security/persistence"
require "active_record/mass_assignment_security/reflection"
require "active_record/mass_assignment_security/relation"
require "active_record/mass_assignment_security/validations"
require "active_record/mass_assignment_security/associations"

class ActiveRecord::Base
include ActiveRecord::MassAssignmentSecurity::Core
include ActiveRecord::MassAssignmentSecurity::AttributeAssignment
include ActiveRecord::MassAssignmentSecurity::Persistence
include ActiveRecord::MassAssignmentSecurity::Relation
include ActiveRecord::MassAssignmentSecurity::Validations
include ActiveRecord::MassAssignmentSecurity::NestedAttributes
end

class ActiveRecord::Reflection::AssociationReflection
include ActiveRecord::MassAssignmentSecurity::Reflection::AssociationReflection
end

module ActiveRecord::Associations
class Association
include ActiveRecord::MassAssignmentSecurity::Associations::Association
end

class CollectionAssociation
include ActiveRecord::MassAssignmentSecurity::Associations::CollectionAssociation
end

class CollectionProxy
include ActiveRecord::MassAssignmentSecurity::Associations::CollectionProxy
end

class HasManyThroughAssociation
include ActiveRecord::MassAssignmentSecurity::Associations::HasManyThroughAssociation
end

class SingularAssociation
include ActiveRecord::MassAssignmentSecurity::Associations::SingularAssociation
end
end

ActiveRecord::SchemaMigration.attr_accessible(:version)
118 changes: 118 additions & 0 deletions lib/active_record/mass_assignment_security/associations.rb
@@ -0,0 +1,118 @@
module ActiveRecord
module MassAssignmentSecurity
module Associations
module Association
def build_record(attributes, options)
reflection.build_association(attributes, options) do |record|
attributes = create_scope.except(*(record.changed - [reflection.foreign_key]))
record.assign_attributes(attributes, without_protection: true)
end
end

private :build_record
end

module CollectionAssociation
def build(attributes = {}, options = {}, &block)
if attributes.is_a?(Array)
attributes.collect { |attr| build(attr, options, &block) }
else
add_to_target(build_record(attributes, options)) do |record|
yield(record) if block_given?
end
end
end

def create(attributes = {}, options = {}, &block)
create_record(attributes, options, &block)
end

def create!(attributes = {}, options = {}, &block)
create_record(attributes, options, true, &block)
end

def create_record(attributes, options, raise = false, &block)
unless owner.persisted?
raise ActiveRecord::RecordNotSaved, "You cannot call create unless the parent is saved"
end

if attributes.is_a?(Array)
attributes.collect { |attr| create_record(attr, options, raise, &block) }
else
transaction do
add_to_target(build_record(attributes, options)) do |record|
yield(record) if block_given?
insert_record(record, true, raise)
end
end
end
end

private :create_record
end

module CollectionProxy
def build(attributes = {}, options = {}, &block)
@association.build(attributes, options, &block)
end

def create(attributes = {}, options = {}, &block)
@association.create(attributes, options, &block)
end

def create!(attributes = {}, options = {}, &block)
@association.create!(attributes, options, &block)
end
end

module HasManyThroughAssociation
def build_record(attributes, options = {})
ensure_not_nested

record = super(attributes, options)

inverse = source_reflection.inverse_of
if inverse
if inverse.macro == :has_many
record.send(inverse.name) << build_through_record(record)
elsif inverse.macro == :has_one
record.send("#{inverse.name}=", build_through_record(record))
end
end

record
end

private :build_record
end

module SingularAssociation
def create(attributes = {}, options = {}, &block)
create_record(attributes, options, &block)
end

def create!(attributes = {}, options = {}, &block)
create_record(attributes, options, true, &block)
end

def build(attributes = {}, options = {})
record = build_record(attributes, options)
yield(record) if block_given?
set_new_record(record)
record
end

def create_record(attributes, options = {}, raise_error = false)
record = build_record(attributes, options)
yield(record) if block_given?
saved = record.save
set_new_record(record)
raise RecordInvalid.new(record) if !saved && raise_error
record
end

private :create_record
end
end
end
end
107 changes: 107 additions & 0 deletions lib/active_record/mass_assignment_security/attribute_assignment.rb
@@ -0,0 +1,107 @@
require 'active_model/mass_assignment_security'
require 'active_record'

module ActiveRecord
ActiveSupport.on_load(:active_record_config) do
mattr_accessor :whitelist_attributes, instance_accessor: false
mattr_accessor :mass_assignment_sanitizer, instance_accessor: false
end

module MassAssignmentSecurity
module AttributeAssignment
extend ActiveSupport::Concern
include ActiveModel::MassAssignmentSecurity

included do
initialize_mass_assignment_sanitizer
end

module ClassMethods
def inherited(child) # :nodoc:
child.send :initialize_mass_assignment_sanitizer if self == Base
super
end

private

# The primary key and inheritance column can never be set by mass-assignment for security reasons.
def attributes_protected_by_default
default = [ primary_key, inheritance_column ]
default << 'id' unless primary_key.eql? 'id'
default
end

def initialize_mass_assignment_sanitizer
attr_accessible(nil) if Model.whitelist_attributes
self.mass_assignment_sanitizer = Model.mass_assignment_sanitizer if Model.mass_assignment_sanitizer
end
end

# Allows you to set all the attributes for a particular mass-assignment
# security role by passing in a hash of attributes with keys matching
# the attribute names (which again matches the column names) and the role
# name using the :as option.
#
# To bypass mass-assignment security you can use the :without_protection => true
# option.
#
# class User < ActiveRecord::Base
# attr_accessible :name
# attr_accessible :name, :is_admin, :as => :admin
# end
#
# user = User.new
# user.assign_attributes({ :name => 'Josh', :is_admin => true })
# user.name # => "Josh"
# user.is_admin? # => false
#
# user = User.new
# user.assign_attributes({ :name => 'Josh', :is_admin => true }, :as => :admin)
# user.name # => "Josh"
# user.is_admin? # => true
#
# user = User.new
# user.assign_attributes({ :name => 'Josh', :is_admin => true }, :without_protection => true)
# user.name # => "Josh"
# user.is_admin? # => true
def assign_attributes(new_attributes, options = {})
return if new_attributes.blank?

attributes = new_attributes.stringify_keys
multi_parameter_attributes = []
nested_parameter_attributes = []
previous_options = @mass_assignment_options
@mass_assignment_options = options

unless options[:without_protection]
attributes = sanitize_for_mass_assignment(attributes, mass_assignment_role)
end

attributes.each do |k, v|
if k.include?("(")
multi_parameter_attributes << [ k, v ]
elsif v.is_a?(Hash)
nested_parameter_attributes << [ k, v ]
else
_assign_attribute(k, v)
end
end

assign_nested_parameter_attributes(nested_parameter_attributes) unless nested_parameter_attributes.empty?
assign_multiparameter_attributes(multi_parameter_attributes) unless multi_parameter_attributes.empty?
ensure
@mass_assignment_options = previous_options
end

protected

def mass_assignment_options
@mass_assignment_options ||= {}
end

def mass_assignment_role
mass_assignment_options[:as] || :default
end
end
end
end

0 comments on commit 9a9f697

Please sign in to comment.