├── test ├── models │ ├── book.rb │ ├── task.rb │ ├── keyboard.rb │ ├── wolf.rb │ ├── vampire.rb │ ├── pirate.rb │ ├── membership.rb │ ├── subscriber.rb │ ├── group.rb │ ├── battle.rb │ ├── company.rb │ ├── team.rb │ ├── mass_assignment_specific.rb │ └── person.rb ├── test_helper.rb ├── mass_assignment_security │ ├── white_list_test.rb │ ├── black_list_test.rb │ ├── permission_set_test.rb │ ├── sanitizer_test.rb │ └── strong_parameters_fallback_test.rb ├── ar_helper.rb ├── accessible_params_wrapper_test.rb ├── abstract_unit.rb ├── mass_assignment_security_test.rb └── attribute_sanitization_test.rb ├── Gemfile ├── .gitignore ├── lib ├── protected_attributes │ ├── version.rb │ └── railtie.rb ├── active_record │ ├── mass_assignment_security │ │ ├── core.rb │ │ ├── reflection.rb │ │ ├── validations.rb │ │ ├── inheritance.rb │ │ ├── relation.rb │ │ ├── attribute_assignment.rb │ │ ├── persistence.rb │ │ ├── associations.rb │ │ └── nested_attributes.rb │ └── mass_assignment_security.rb ├── protected_attributes.rb ├── active_model │ ├── mass_assignment_security │ │ ├── permission_set.rb │ │ └── sanitizer.rb │ └── mass_assignment_security.rb └── action_controller │ └── accessible_params_wrapper.rb ├── gemfiles ├── Gemfile-rails-4.0 ├── Gemfile-rails-4.1 ├── Gemfile-rails-4.0-stable ├── Gemfile-rails-4.1-stable ├── Gemfile-rails-4.2 └── Gemfile-rails-4.2-stable ├── Rakefile ├── .travis.yml ├── LICENSE.txt ├── protected_attributes.gemspec ├── CHANGELOG.md └── README.md /test/models/book.rb: -------------------------------------------------------------------------------- 1 | class Book < ActiveRecord::Base 2 | end 3 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'rails', '~> 4.0' 4 | 5 | gemspec 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | *.lock 5 | pkg 6 | .ruby-version 7 | .ruby-gemset 8 | -------------------------------------------------------------------------------- /lib/protected_attributes/version.rb: -------------------------------------------------------------------------------- 1 | module ProtectedAttributes 2 | VERSION = "1.1.4" 3 | end 4 | -------------------------------------------------------------------------------- /test/models/task.rb: -------------------------------------------------------------------------------- 1 | class Task < ActiveRecord::Base 2 | def updated_at 3 | ending 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /test/models/keyboard.rb: -------------------------------------------------------------------------------- 1 | class Keyboard < ActiveRecord::Base 2 | attr_accessible(nil) 3 | 4 | self.primary_key = 'key_number' 5 | end 6 | -------------------------------------------------------------------------------- /test/models/wolf.rb: -------------------------------------------------------------------------------- 1 | class Wolf < ActiveRecord::Base 2 | has_many :battles, :as => :battle 3 | has_many :teams, :through => :battles 4 | end 5 | -------------------------------------------------------------------------------- /test/models/vampire.rb: -------------------------------------------------------------------------------- 1 | class Vampire < ActiveRecord::Base 2 | has_many :battles, :as => :battle 3 | has_many :teams, :through => :battles 4 | end 5 | -------------------------------------------------------------------------------- /test/models/pirate.rb: -------------------------------------------------------------------------------- 1 | class Pirate < ActiveRecord::Base 2 | self.mass_assignment_sanitizer = :strict 3 | attr_accessible :name 4 | has_many :memberships 5 | end 6 | -------------------------------------------------------------------------------- /test/models/membership.rb: -------------------------------------------------------------------------------- 1 | class Membership < ActiveRecord::Base 2 | self.mass_assignment_sanitizer = :strict 3 | 4 | belongs_to :group 5 | belongs_to :pirate 6 | 7 | attr_accessible [] 8 | end 9 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | require 'bundler/setup' 2 | require 'minitest/autorun' 3 | require 'mocha/minitest' 4 | require 'rails' 5 | 6 | ActiveSupport.test_order = :random if ActiveSupport.respond_to?(:test_order) 7 | -------------------------------------------------------------------------------- /test/models/subscriber.rb: -------------------------------------------------------------------------------- 1 | class Subscriber < ActiveRecord::Base 2 | attr_accessible(nil) 3 | 4 | self.primary_key = 'nick' 5 | has_many :subscriptions 6 | has_many :books, :through => :subscriptions 7 | end 8 | -------------------------------------------------------------------------------- /gemfiles/Gemfile-rails-4.0: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'rails', '~> 4.0.13' 4 | 5 | if RUBY_VERSION > '1.9.3' 6 | gem 'mime-types' 7 | else 8 | gem 'mime-types', '2.99' 9 | end 10 | 11 | gemspec path: '..' 12 | -------------------------------------------------------------------------------- /gemfiles/Gemfile-rails-4.1: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'rails', '~> 4.1.9' 4 | 5 | if RUBY_VERSION > '1.9.3' 6 | gem 'mime-types' 7 | else 8 | gem 'mime-types', '2.99' 9 | end 10 | 11 | gemspec path: '..' 12 | -------------------------------------------------------------------------------- /test/models/group.rb: -------------------------------------------------------------------------------- 1 | class Group < ActiveRecord::Base 2 | self.mass_assignment_sanitizer = :strict 3 | 4 | has_many :memberships, :dependent => :destroy 5 | has_many :members, :through => :memberships, :source => :pirate 6 | end 7 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env rake 2 | require "bundler/gem_tasks" 3 | require 'rake/testtask' 4 | 5 | Rake::TestTask.new do |t| 6 | t.libs = ["test"] 7 | t.pattern = "test/**/*_test.rb" 8 | t.ruby_opts = ['-w'] 9 | end 10 | 11 | task :default => :test 12 | -------------------------------------------------------------------------------- /gemfiles/Gemfile-rails-4.0-stable: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'rails', github: 'rails/rails', branch: '4-0-stable' 4 | 5 | if RUBY_VERSION > '1.9.3' 6 | gem 'mime-types' 7 | else 8 | gem 'mime-types', '2.99' 9 | end 10 | 11 | gemspec path: '..' 12 | -------------------------------------------------------------------------------- /gemfiles/Gemfile-rails-4.1-stable: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'rails', github: 'rails/rails', branch: '4-1-stable' 4 | 5 | if RUBY_VERSION > '1.9.3' 6 | gem 'mime-types' 7 | else 8 | gem 'mime-types', '2.99' 9 | end 10 | 11 | gemspec path: '..' 12 | -------------------------------------------------------------------------------- /gemfiles/Gemfile-rails-4.2: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'rails', '~> 4.2.0' 4 | 5 | if RUBY_VERSION > '1.9.3' 6 | gem 'mime-types' 7 | else 8 | gem 'mime-types', '2.99' 9 | end 10 | 11 | if RUBY_VERSION < '2.1.0' 12 | gem 'nokogiri', '< 1.7' 13 | end 14 | 15 | gemspec path: '..' 16 | -------------------------------------------------------------------------------- /gemfiles/Gemfile-rails-4.2-stable: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'rails', github: 'rails/rails', branch: '4-2-stable' 4 | 5 | if RUBY_VERSION > '1.9.3' 6 | gem 'mime-types' 7 | else 8 | gem 'mime-types', '2.99' 9 | end 10 | 11 | if RUBY_VERSION < '2.1.0' 12 | gem 'nokogiri', '< 1.7' 13 | end 14 | 15 | gemspec path: '..' 16 | -------------------------------------------------------------------------------- /test/models/battle.rb: -------------------------------------------------------------------------------- 1 | class Battle < ActiveRecord::Base 2 | attr_accessible [] 3 | belongs_to :team 4 | belongs_to :battle, :polymorphic => true 5 | end 6 | 7 | class NestedBattle < ActiveRecord::Base 8 | self.table_name = "battles" 9 | 10 | belongs_to :team 11 | belongs_to :battle, :polymorphic => true 12 | 13 | accepts_nested_attributes_for :team 14 | end 15 | -------------------------------------------------------------------------------- /test/models/company.rb: -------------------------------------------------------------------------------- 1 | class AbstractCompany < ActiveRecord::Base 2 | self.abstract_class = true 3 | end 4 | 5 | class Company < AbstractCompany 6 | attr_protected :rating 7 | end 8 | 9 | class Firm < Company 10 | end 11 | 12 | class Corporation < Company 13 | attr_accessible :type, :name, :description 14 | end 15 | 16 | class SpecialCorporation < Corporation 17 | end 18 | -------------------------------------------------------------------------------- /test/models/team.rb: -------------------------------------------------------------------------------- 1 | class Team < ActiveRecord::Base 2 | has_many :battles 3 | has_many :wolf_battles, :through => :battles, :class_name => 'Wolf', :source => :battle, :source_type => 'Wolf' 4 | has_many :vampire_battles, :through => :battles, :class_name => 'Vampire', :source => :battle, :source_type => 'Vampire' 5 | has_many :nested_battles 6 | 7 | accepts_nested_attributes_for :nested_battles 8 | end 9 | -------------------------------------------------------------------------------- /lib/active_record/mass_assignment_security/core.rb: -------------------------------------------------------------------------------- 1 | module ActiveRecord 2 | module MassAssignmentSecurity 3 | module Core 4 | 5 | private 6 | 7 | def init_attributes(attributes, options) 8 | assign_attributes(attributes, options) 9 | end 10 | 11 | def init_internals 12 | super 13 | @mass_assignment_options = nil 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/protected_attributes.rb: -------------------------------------------------------------------------------- 1 | require "active_model/mass_assignment_security" 2 | require "protected_attributes/railtie" if defined? Rails::Railtie 3 | require "protected_attributes/version" 4 | 5 | ActiveSupport.on_load :active_record do 6 | require "active_record/mass_assignment_security" 7 | end 8 | 9 | ActiveSupport.on_load :action_controller do 10 | require "action_controller/accessible_params_wrapper" 11 | end 12 | 13 | module ProtectedAttributes 14 | end 15 | -------------------------------------------------------------------------------- /lib/protected_attributes/railtie.rb: -------------------------------------------------------------------------------- 1 | module ProtectedAttributes 2 | class Railtie < ::Rails::Railtie 3 | initializer "protected_attributes.active_record", :before => "active_record.set_configs" do |app| 4 | if app.config.respond_to?(:active_record) && app.config.active_record.delete(:whitelist_attributes) 5 | ActiveSupport::Deprecation.warn "config.active_record.whitelist_attributes is deprecated and have no effect. Remove its call from the configuration." 6 | end 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/active_record/mass_assignment_security/reflection.rb: -------------------------------------------------------------------------------- 1 | module ActiveRecord 2 | module Reflection 3 | if defined?(AbstractReflection) 4 | class AbstractReflection 5 | undef :build_association 6 | 7 | def build_association(*options, &block) 8 | klass.new(*options, &block) 9 | end 10 | end 11 | else 12 | class AssociationReflection 13 | undef :build_association 14 | 15 | def build_association(*options, &block) 16 | klass.new(*options, &block) 17 | end 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /test/mass_assignment_security/white_list_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | require 'active_model/mass_assignment_security' 3 | 4 | class WhiteListTest < ActiveModel::TestCase 5 | 6 | def setup 7 | @white_list = ActiveModel::MassAssignmentSecurity::WhiteList.new 8 | @included_key = 'first_name' 9 | @white_list += [ @included_key ] 10 | end 11 | 12 | test "deny? is false for included items" do 13 | assert_equal false, @white_list.deny?(@included_key) 14 | end 15 | 16 | test "deny? is true for non-included items" do 17 | assert_equal true, @white_list.deny?('admin') 18 | end 19 | 20 | end 21 | -------------------------------------------------------------------------------- /test/mass_assignment_security/black_list_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | require 'active_model/mass_assignment_security' 3 | 4 | class BlackListTest < ActiveModel::TestCase 5 | 6 | def setup 7 | @black_list = ActiveModel::MassAssignmentSecurity::BlackList.new 8 | @included_key = 'admin' 9 | @black_list += [ @included_key ] 10 | end 11 | 12 | test "deny? is true for included items" do 13 | assert_equal true, @black_list.deny?(@included_key) 14 | end 15 | 16 | test "deny? is false for non-included items" do 17 | assert_equal false, @black_list.deny?('first_name') 18 | end 19 | 20 | 21 | end 22 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | sudo: false 3 | cache: bundler 4 | rvm: 5 | - 1.9.3 6 | - 2.0.0 7 | - 2.1 8 | - 2.2 9 | gemfile: 10 | - gemfiles/Gemfile-rails-4.0-stable 11 | - gemfiles/Gemfile-rails-4.1-stable 12 | - gemfiles/Gemfile-rails-4.2-stable 13 | - gemfiles/Gemfile-rails-4.0 14 | - gemfiles/Gemfile-rails-4.1 15 | - gemfiles/Gemfile-rails-4.2 16 | notifications: 17 | email: false 18 | irc: 19 | on_success: change 20 | on_failure: always 21 | channels: 22 | - "irc.freenode.org#rails-contrib" 23 | campfire: 24 | on_success: change 25 | on_failure: always 26 | rooms: 27 | - secure: "RuC26E0TkxKzIQfqLuSHphe5FlgC1ULzhGpkZXqht3oDQjAuR+EmslxdWS9c\nVWwEr7PA+jnEp/8+sYM8TqHgLxFDHTTC6DgQoNNUY/Ti+mEyZunOPmXWP4UG\n6NLwr/rIuP1DEmDa6/q8V7qGn++SJrm1m4emGU4PbYNu6e6Nihc=" 28 | -------------------------------------------------------------------------------- /lib/active_record/mass_assignment_security/validations.rb: -------------------------------------------------------------------------------- 1 | require 'active_support/concern' 2 | 3 | module ActiveRecord 4 | module MassAssignmentSecurity 5 | module Validations 6 | extend ActiveSupport::Concern 7 | 8 | module ClassMethods 9 | # Creates an object just like Base.create but calls save! instead of +save+ 10 | # so an exception is raised if the record is invalid. 11 | def create!(attributes = nil, options = {}, &block) 12 | if attributes.is_a?(Array) 13 | attributes.collect { |attr| create!(attr, options, &block) } 14 | else 15 | object = new(attributes, options) 16 | yield(object) if block_given? 17 | object.save! 18 | object 19 | end 20 | end 21 | end 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/active_model/mass_assignment_security/permission_set.rb: -------------------------------------------------------------------------------- 1 | require 'set' 2 | 3 | module ActiveModel 4 | module MassAssignmentSecurity 5 | class PermissionSet < Set #:nodoc: 6 | 7 | def +(values) 8 | super(values.compact.map(&:to_s)) 9 | end 10 | 11 | def include?(key) 12 | super(remove_multiparameter_id(key)) 13 | end 14 | 15 | def deny?(key) 16 | raise NotImplementedError, "#deny?(key) supposed to be overwritten" 17 | end 18 | 19 | protected 20 | 21 | def remove_multiparameter_id(key) 22 | key.to_s.gsub(/\(.+/, '') 23 | end 24 | end 25 | 26 | class WhiteList < PermissionSet #:nodoc: 27 | 28 | def deny?(key) 29 | !include?(key) 30 | end 31 | end 32 | 33 | class BlackList < PermissionSet #:nodoc: 34 | 35 | def deny?(key) 36 | include?(key) 37 | end 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/action_controller/accessible_params_wrapper.rb: -------------------------------------------------------------------------------- 1 | require 'active_support/concern' 2 | require 'action_controller' 3 | require 'action_controller/metal/params_wrapper' 4 | 5 | module ActionController 6 | module ParamsWrapper 7 | class Options # :nodoc: 8 | undef :include 9 | 10 | def include 11 | return super if @include_set 12 | 13 | m = model 14 | synchronize do 15 | return super if @include_set 16 | 17 | @include_set = true 18 | 19 | unless super || exclude 20 | 21 | if m.respond_to?(:accessible_attributes) && m.accessible_attributes(:default).present? 22 | self.include = m.accessible_attributes(:default).to_a 23 | elsif m.respond_to?(:attribute_names) && m.attribute_names.any? 24 | self.include = m.attribute_names 25 | end 26 | end 27 | end 28 | end 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/active_record/mass_assignment_security/inheritance.rb: -------------------------------------------------------------------------------- 1 | module ActiveRecord 2 | module MassAssignmentSecurity 3 | module Inheritance 4 | extend ActiveSupport::Concern 5 | 6 | module ClassMethods 7 | private 8 | # Detect the subclass from the inheritance column of attrs. If the inheritance column value 9 | # is not self or a valid subclass, raises ActiveRecord::SubclassNotFound 10 | # If this is a StrongParameters hash, and access to inheritance_column is not permitted, 11 | # this will ignore the inheritance column and return nil 12 | def subclass_from_attributes?(attrs) 13 | active_authorizer[:default].deny?(inheritance_column) ? nil : super 14 | end 15 | 16 | # Support Active Record <= 4.0.3, which uses the old method signature. 17 | def subclass_from_attrs(attrs) 18 | active_authorizer[:default].deny?(inheritance_column) ? nil : super 19 | end 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012-2017 Guillermo Iguaran 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /protected_attributes.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'protected_attributes/version' 5 | 6 | Gem::Specification.new do |gem| 7 | gem.name = "protected_attributes" 8 | gem.version = ProtectedAttributes::VERSION 9 | gem.authors = ["David Heinemeier Hansson"] 10 | gem.email = ["david@loudthinking.com"] 11 | gem.description = %q{Protect attributes from mass assignment} 12 | gem.summary = %q{Protect attributes from mass assignment in Active Record models} 13 | gem.homepage = "https://github.com/rails/protected_attributes" 14 | gem.license = "MIT" 15 | 16 | gem.files = Dir["LICENSE.txt", "README.md", "lib/**/*"] 17 | gem.require_paths = ["lib"] 18 | 19 | gem.add_dependency "activemodel", ">= 4.0.1", "< 5.0" 20 | 21 | gem.add_development_dependency "activerecord", ">= 4.0.1", "< 5.0" 22 | gem.add_development_dependency "actionpack", ">= 4.0.1", "< 5.0" 23 | gem.add_development_dependency "railties", ">= 4.0.1", "< 5.0" 24 | gem.add_development_dependency "sqlite3" 25 | gem.add_development_dependency "mocha" 26 | end 27 | -------------------------------------------------------------------------------- /lib/active_record/mass_assignment_security.rb: -------------------------------------------------------------------------------- 1 | require "active_record" 2 | 3 | require "active_record/mass_assignment_security/associations" 4 | require "active_record/mass_assignment_security/attribute_assignment" 5 | require "active_record/mass_assignment_security/core" 6 | require "active_record/mass_assignment_security/nested_attributes" 7 | require "active_record/mass_assignment_security/persistence" 8 | require "active_record/mass_assignment_security/reflection" 9 | require "active_record/mass_assignment_security/relation" 10 | require "active_record/mass_assignment_security/validations" 11 | require "active_record/mass_assignment_security/associations" 12 | require "active_record/mass_assignment_security/inheritance" 13 | 14 | class ActiveRecord::Base 15 | include ActiveRecord::MassAssignmentSecurity::Core 16 | include ActiveRecord::MassAssignmentSecurity::AttributeAssignment 17 | include ActiveRecord::MassAssignmentSecurity::Persistence 18 | include ActiveRecord::MassAssignmentSecurity::Validations 19 | include ActiveRecord::MassAssignmentSecurity::NestedAttributes 20 | include ActiveRecord::MassAssignmentSecurity::Inheritance 21 | end 22 | 23 | class ActiveRecord::SchemaMigration < ActiveRecord::Base 24 | attr_accessible :version 25 | end 26 | -------------------------------------------------------------------------------- /test/mass_assignment_security/permission_set_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | require 'active_model/mass_assignment_security' 3 | 4 | class PermissionSetTest < ActiveModel::TestCase 5 | 6 | def setup 7 | @permission_list = ActiveModel::MassAssignmentSecurity::PermissionSet.new 8 | end 9 | 10 | test "+ stringifies added collection values" do 11 | symbol_collection = [ :admin ] 12 | new_list = @permission_list += symbol_collection 13 | 14 | assert new_list.include?('admin'), "did not add collection to #{@permission_list.inspect}}" 15 | end 16 | 17 | test "+ compacts added collection values" do 18 | added_collection = [ nil ] 19 | new_list = @permission_list + added_collection 20 | assert_equal new_list, @permission_list, "did not add collection to #{@permission_list.inspect}}" 21 | end 22 | 23 | test "include? normalizes multi-parameter keys" do 24 | multi_param_key = 'admin(1)' 25 | new_list = @permission_list += [ 'admin' ] 26 | 27 | assert new_list.include?(multi_param_key), "#{multi_param_key} not found in #{@permission_list.inspect}" 28 | end 29 | 30 | test "include? normal keys" do 31 | normal_key = 'admin' 32 | new_list = @permission_list += [ normal_key ] 33 | 34 | assert new_list.include?(normal_key), "#{normal_key} not found in #{@permission_list.inspect}" 35 | end 36 | 37 | end 38 | -------------------------------------------------------------------------------- /test/models/mass_assignment_specific.rb: -------------------------------------------------------------------------------- 1 | class User 2 | include ActiveModel::MassAssignmentSecurity 3 | attr_protected :admin 4 | 5 | public :sanitize_for_mass_assignment 6 | end 7 | 8 | class SpecialUser 9 | include ActiveModel::MassAssignmentSecurity 10 | attr_accessible :name, :email, :as => :moderator 11 | 12 | public :sanitize_for_mass_assignment 13 | end 14 | 15 | class Person 16 | include ActiveModel::MassAssignmentSecurity 17 | attr_accessible :name, :email 18 | attr_accessible :name, :email, :admin, :as => :admin 19 | 20 | public :sanitize_for_mass_assignment 21 | end 22 | 23 | class Account 24 | include ActiveModel::MassAssignmentSecurity 25 | attr_accessible :name, :email, :as => [:default, :admin] 26 | attr_accessible :admin, :as => :admin 27 | 28 | public :sanitize_for_mass_assignment 29 | end 30 | 31 | class Firm 32 | include ActiveModel::MassAssignmentSecurity 33 | 34 | public :sanitize_for_mass_assignment 35 | 36 | def self.attributes_protected_by_default 37 | ["type"] 38 | end 39 | end 40 | 41 | class Task 42 | include ActiveModel::MassAssignmentSecurity 43 | attr_protected :starting 44 | 45 | public :sanitize_for_mass_assignment 46 | end 47 | 48 | class SpecialLoosePerson 49 | include ActiveModel::MassAssignmentSecurity 50 | attr_protected :credit_rating, :administrator 51 | attr_protected :credit_rating, :as => :admin 52 | end 53 | 54 | class LooseDescendant < SpecialLoosePerson 55 | attr_protected :phone_number 56 | end 57 | 58 | class LooseDescendantSecond< SpecialLoosePerson 59 | attr_protected :phone_number 60 | attr_protected :name 61 | end 62 | 63 | class SpecialTightPerson 64 | include ActiveModel::MassAssignmentSecurity 65 | attr_accessible :name, :address 66 | attr_accessible :name, :address, :admin, :as => :admin 67 | 68 | def self.attributes_protected_by_default 69 | ["mobile_number"] 70 | end 71 | end 72 | 73 | class TightDescendant < SpecialTightPerson 74 | attr_accessible :phone_number 75 | attr_accessible :super_powers, :as => :admin 76 | end 77 | -------------------------------------------------------------------------------- /test/mass_assignment_security/sanitizer_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | require 'active_model/mass_assignment_security' 3 | require 'active_support/logger' 4 | 5 | class SanitizerTest < ActiveModel::TestCase 6 | attr_accessor :logger 7 | 8 | class Authorizer < ActiveModel::MassAssignmentSecurity::PermissionSet 9 | def deny?(key) 10 | ['admin', 'id'].include?(key) 11 | end 12 | end 13 | 14 | def setup 15 | @logger_sanitizer = ActiveModel::MassAssignmentSecurity::LoggerSanitizer.new(self) 16 | @strict_sanitizer = ActiveModel::MassAssignmentSecurity::StrictSanitizer.new(self) 17 | @authorizer = Authorizer.new 18 | end 19 | 20 | test "sanitize attributes" do 21 | original_attributes = { 'first_name' => 'allowed', 'admin' => 'denied' } 22 | attributes = @logger_sanitizer.sanitize(self.class, original_attributes, @authorizer) 23 | 24 | assert attributes.key?('first_name'), "Allowed key shouldn't be rejected" 25 | assert !attributes.key?('admin'), "Denied key should be rejected" 26 | end 27 | 28 | test "debug mass assignment removal with LoggerSanitizer" do 29 | original_attributes = { 'first_name' => 'allowed', 'admin' => 'denied' } 30 | log = StringIO.new 31 | self.logger = ActiveSupport::Logger.new(log) 32 | @logger_sanitizer.sanitize(self.class, original_attributes, @authorizer) 33 | assert_match(/admin/, log.string, "Should log removed attributes: #{log.string}") 34 | end 35 | 36 | test "debug mass assignment removal with StrictSanitizer" do 37 | original_attributes = { 'first_name' => 'allowed', 'admin' => 'denied' } 38 | assert_raise ActiveModel::MassAssignmentSecurity::Error do 39 | @strict_sanitizer.sanitize(self.class, original_attributes, @authorizer) 40 | end 41 | end 42 | 43 | test "mass assignment insensitive attributes" do 44 | original_attributes = {'id' => 1, 'first_name' => 'allowed'} 45 | 46 | assert_nothing_raised do 47 | @strict_sanitizer.sanitize(self.class, original_attributes, @authorizer) 48 | end 49 | end 50 | 51 | end 52 | -------------------------------------------------------------------------------- /test/models/person.rb: -------------------------------------------------------------------------------- 1 | class LoosePerson < ActiveRecord::Base 2 | self.table_name = 'people' 3 | 4 | attr_protected :comments, :best_friend_id, :best_friend_of_id 5 | attr_protected :as => :admin 6 | 7 | has_one :best_friend, :class_name => 'LoosePerson', :foreign_key => :best_friend_id 8 | belongs_to :best_friend_of, :class_name => 'LoosePerson', :foreign_key => :best_friend_of_id 9 | has_many :best_friends, :class_name => 'LoosePerson', :foreign_key => :best_friend_id 10 | 11 | accepts_nested_attributes_for :best_friend, :best_friend_of, :best_friends 12 | end 13 | 14 | class TightPerson < ActiveRecord::Base 15 | self.table_name = 'people' 16 | 17 | attr_accessible :first_name, :gender 18 | attr_accessible :first_name, :gender, :comments, :as => :admin 19 | attr_accessible :best_friend_attributes, :best_friend_of_attributes, :best_friends_attributes 20 | attr_accessible :best_friend_attributes, :best_friend_of_attributes, :best_friends_attributes, :as => :admin 21 | 22 | has_one :best_friend, :class_name => 'TightPerson', :foreign_key => :best_friend_id 23 | belongs_to :best_friend_of, :class_name => 'TightPerson', :foreign_key => :best_friend_of_id 24 | has_many :best_friends, :class_name => 'TightPerson', :foreign_key => :best_friend_id 25 | 26 | accepts_nested_attributes_for :best_friend, :best_friend_of, :best_friends 27 | end 28 | 29 | class NestedPerson < ActiveRecord::Base 30 | self.table_name = 'people' 31 | 32 | attr_accessible :first_name, :best_friend_first_name, :best_friend_attributes 33 | attr_accessible :first_name, :gender, :comments, :as => :admin 34 | attr_accessible :best_friend_attributes, :best_friend_first_name, :as => :admin 35 | 36 | has_one :best_friend, :class_name => 'NestedPerson', :foreign_key => :best_friend_id 37 | accepts_nested_attributes_for :best_friend, :update_only => true, :reject_if => :all_blank 38 | 39 | def comments=(new_comments) 40 | raise RuntimeError 41 | end 42 | 43 | def best_friend_first_name=(new_name) 44 | assign_attributes({ :best_friend_attributes => { :first_name => new_name } }) 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /lib/active_model/mass_assignment_security/sanitizer.rb: -------------------------------------------------------------------------------- 1 | module ActiveModel 2 | module MassAssignmentSecurity 3 | class Sanitizer #:nodoc: 4 | # Returns all attributes not denied by the authorizer. 5 | def sanitize(klass, attributes, authorizer) 6 | rejected = [] 7 | sanitized_attributes = attributes.reject do |key, value| 8 | rejected << key if authorizer.deny?(key) 9 | end 10 | process_removed_attributes(klass, rejected) unless rejected.empty? 11 | sanitized_attributes 12 | end 13 | 14 | protected 15 | 16 | def process_removed_attributes(klass, attrs) 17 | raise NotImplementedError, "#process_removed_attributes(klass, attrs) is intended to be overwritten by a subclass" 18 | end 19 | end 20 | 21 | class LoggerSanitizer < Sanitizer #:nodoc: 22 | def initialize(target) 23 | @target = target 24 | super() 25 | end 26 | 27 | def logger 28 | @target.logger 29 | end 30 | 31 | def logger? 32 | @target.respond_to?(:logger) && @target.logger 33 | end 34 | 35 | def backtrace 36 | if defined? Rails.backtrace_cleaner 37 | Rails.backtrace_cleaner.clean(caller) 38 | else 39 | caller 40 | end 41 | end 42 | 43 | def process_removed_attributes(klass, attrs) 44 | if logger? 45 | logger.warn do 46 | "WARNING: Can't mass-assign protected attributes for #{klass.name}: #{attrs.join(', ')}\n" + 47 | backtrace.map { |trace| "\t#{trace}" }.join("\n") 48 | end 49 | end 50 | end 51 | end 52 | 53 | class StrictSanitizer < Sanitizer #:nodoc: 54 | def initialize(target = nil) 55 | super() 56 | end 57 | 58 | def process_removed_attributes(klass, attrs) 59 | unless (attrs - insensitive_attributes).empty? 60 | raise ActiveModel::MassAssignmentSecurity::Error.new(klass, attrs) 61 | end 62 | end 63 | 64 | def insensitive_attributes 65 | ['id'] 66 | end 67 | end 68 | 69 | class Error < StandardError #:nodoc: 70 | def initialize(klass, attrs) 71 | super("Can't mass-assign protected attributes for #{klass.name}: #{attrs.join(', ')}") 72 | end 73 | end 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /test/ar_helper.rb: -------------------------------------------------------------------------------- 1 | require 'active_record' 2 | 3 | ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: ':memory:') 4 | 5 | ActiveRecord::Schema.verbose = false 6 | ActiveRecord::Schema.define do 7 | create_table :accounts, :force => true do |t| 8 | t.integer :firm_id 9 | t.string :firm_name 10 | t.integer :credit_limit 11 | end 12 | 13 | create_table :companies, :force => true do |t| 14 | t.string :type 15 | t.integer :firm_id 16 | t.string :firm_name 17 | t.string :name 18 | t.integer :client_of 19 | t.integer :rating, :default => 1 20 | t.integer :account_id 21 | t.string :description, :default => "" 22 | end 23 | 24 | add_index :companies, [:firm_id, :type, :rating], :name => "company_index" 25 | add_index :companies, [:firm_id, :type], :name => "company_partial_index", :where => "rating > 10" 26 | 27 | create_table :keyboards, :force => true, :id => false do |t| 28 | t.primary_key :key_number 29 | t.string :name 30 | end 31 | 32 | create_table :people, :force => true do |t| 33 | t.string :first_name, :null => false 34 | t.string :gender, :limit => 1 35 | t.string :comments 36 | t.references :best_friend 37 | t.references :best_friend_of 38 | t.timestamps null: false 39 | end 40 | 41 | create_table :subscribers, :force => true, :id => false do |t| 42 | t.string :nick, :null => false 43 | t.string :name 44 | end 45 | add_index :subscribers, :nick, :unique => true 46 | 47 | create_table :books, :force => true do |t| 48 | t.string :title 49 | end 50 | 51 | create_table :tasks, :force => true do |t| 52 | t.datetime :starting 53 | t.datetime :ending 54 | end 55 | 56 | create_table :pirates, :force => true do |t| 57 | t.string :name 58 | end 59 | 60 | create_table :groups, :force => true do |t| 61 | end 62 | 63 | create_table :memberships, :force => true do |t| 64 | t.integer "group_id" 65 | t.integer "pirate_id" 66 | end 67 | 68 | create_table :teams, :force => true 69 | create_table :wolves, :force => true 70 | create_table :vampires, :force => true 71 | create_table :battles, :force => true do |t| 72 | t.integer "team_id" 73 | t.integer "battle_id" 74 | t.string "battle_type" 75 | end 76 | end 77 | 78 | QUOTED_TYPE = ActiveRecord::Base.connection.quote_column_name('type') 79 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.1.4 2 | 3 | * Avoid double callbacks in Rails >= 4.1. 4 | 5 | * Fixes UnitializedConstant in TooManyRecords. 6 | 7 | ## 1.1.3 8 | 9 | * Fix deprecation code. 10 | 11 | ## 1.1.2 12 | 13 | * Deprecate `config.active_record.whitelist_attributes`. 14 | 15 | * Fix integration with associations. 16 | 17 | ## 1.1.1 18 | 19 | * Fix strong parameters integration. 20 | 21 | * Remove warnings 22 | 23 | * Fix `find_or_*` and `first_or_*` methods integration. 24 | 25 | ## 1.1.0 26 | 27 | * Integrate with strong parameters. This allows to migrate a codebase partially 28 | from `protected_attributes` to `strong_parameters`. Every model that does not 29 | use a protection macro (`attr_accessible` or `attr_protected`), will be 30 | protected by strong parameters. The behavior stays the same for models, which 31 | use a protection macro. 32 | 33 | To fully restore the old behavior set: 34 | 35 | config.action_controller.permit_all_parameters = true 36 | 37 | Or add a callback to your controllers like this: 38 | 39 | before_action { params.permit! } 40 | 41 | Fixes #41. 42 | 43 | ## 1.0.9 44 | 45 | * Fixes ThroughAssociation#build_record method on rails 4.1.10+ 46 | 47 | Fixes #60, #63 48 | 49 | * Fixes build_association method on rails 4.2.0+ 50 | 51 | Fixes https://github.com/rails/rails/issues/18121 52 | 53 | ## 1.0.8 (June 16, 2014) 54 | 55 | * Support Rails 4.0.6+ and 4.1.2+. 56 | 57 | Fixes #35 58 | 59 | 60 | ## 1.0.7 (March 12, 2014) 61 | 62 | * Fix STI support on Active Record <= 4.0.3. 63 | 64 | 65 | ## 1.0.6 (March 10, 2014) 66 | 67 | * Support to Rails 4.1 68 | 69 | * Fix `CollectionProxy#new` method. 70 | 71 | Fixes #21 72 | 73 | 74 | ## 1.0.5 (November 1, 2013) 75 | 76 | * Fix install error with Rails 4.0.1. 77 | Related with https://github.com/bundler/bundler/issues/2583 78 | 79 | 80 | ## 1.0.4 (October 18, 2013) 81 | 82 | * Avoid override the entire Active Record initialization. 83 | 84 | Fixes rails/rails#12243 85 | 86 | 87 | ## 1.0.3 (June 29, 2013) 88 | 89 | * Fix "uninitialized constant ActiveRecord::MassAssignmentSecurity::NestedAttributes::ClassMethods::REJECT_ALL_BLANK_PROC" 90 | error when using `:all_blank` option. 91 | 92 | Fixes #8 93 | 94 | * Fix `NoMethodError` exception when calling `raise_nested_attributes_record_not_found`. 95 | 96 | 97 | ## 1.0.2 (June 25, 2013) 98 | 99 | * Sync #initialize override to latest rails implementation 100 | 101 | Fixes #14 102 | 103 | 104 | ## 1.0.1 (April 6, 2013) 105 | 106 | * Fix "uninitialized constant `ActiveRecord::SchemaMigration`" error 107 | when checking pending migrations. 108 | 109 | Fixes rails/rails#10109 110 | 111 | 112 | ## 1.0.0 (January 22, 2013) 113 | 114 | * First public version 115 | -------------------------------------------------------------------------------- /test/accessible_params_wrapper_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | require 'abstract_unit' 3 | require 'action_controller/accessible_params_wrapper' 4 | 5 | module ParamsWrapperTestHelp 6 | def with_default_wrapper_options(&block) 7 | @controller.class._set_wrapper_options({:format => [:json]}) 8 | @controller.class.inherited(@controller.class) 9 | yield 10 | end 11 | 12 | def assert_parameters(expected) 13 | assert_equal expected, self.class.controller_class.last_parameters 14 | end 15 | end 16 | 17 | class AccessibleParamsWrapperTest < ActionController::TestCase 18 | include ParamsWrapperTestHelp 19 | 20 | class UsersController < ActionController::Base 21 | class << self 22 | attr_accessor :last_parameters 23 | end 24 | 25 | def parse 26 | self.class.last_parameters = request.params.except(:controller, :action) 27 | head :ok 28 | end 29 | end 30 | 31 | class User; end 32 | class Person; end 33 | 34 | tests UsersController 35 | 36 | def teardown 37 | UsersController.last_parameters = nil 38 | end 39 | 40 | def test_derived_wrapped_keys_from_matching_model 41 | User.expects(:respond_to?).with(:accessible_attributes).returns(false) 42 | User.expects(:respond_to?).with(:attribute_names).returns(true) 43 | User.expects(:attribute_names).twice.returns(["username"]) 44 | 45 | with_default_wrapper_options do 46 | @request.env['CONTENT_TYPE'] = 'application/json' 47 | post :parse, { 'username' => 'sikachu', 'title' => 'Developer' } 48 | assert_parameters({ 'username' => 'sikachu', 'title' => 'Developer', 'user' => { 'username' => 'sikachu' }}) 49 | end 50 | end 51 | 52 | def test_derived_wrapped_keys_from_specified_model 53 | with_default_wrapper_options do 54 | Person.expects(:respond_to?).with(:accessible_attributes).returns(false) 55 | Person.expects(:respond_to?).with(:attribute_names).returns(true) 56 | Person.expects(:attribute_names).twice.returns(["username"]) 57 | 58 | UsersController.wrap_parameters Person 59 | 60 | @request.env['CONTENT_TYPE'] = 'application/json' 61 | post :parse, { 'username' => 'sikachu', 'title' => 'Developer' } 62 | assert_parameters({ 'username' => 'sikachu', 'title' => 'Developer', 'person' => { 'username' => 'sikachu' }}) 63 | end 64 | end 65 | 66 | def test_accessible_wrapped_keys_from_matching_model 67 | User.expects(:respond_to?).with(:accessible_attributes).returns(true) 68 | User.expects(:accessible_attributes).with(:default).twice.returns(["username"]) 69 | 70 | with_default_wrapper_options do 71 | @request.env['CONTENT_TYPE'] = 'application/json' 72 | post :parse, { 'username' => 'sikachu', 'title' => 'Developer' } 73 | assert_parameters({ 'username' => 'sikachu', 'title' => 'Developer', 'user' => { 'username' => 'sikachu' }}) 74 | end 75 | end 76 | end 77 | -------------------------------------------------------------------------------- /lib/active_record/mass_assignment_security/relation.rb: -------------------------------------------------------------------------------- 1 | module ActiveRecord 2 | class Relation 3 | undef :first_or_create 4 | undef :first_or_create! 5 | undef :first_or_initialize 6 | undef :find_or_initialize_by 7 | undef :find_or_create_by 8 | undef :find_or_create_by! 9 | 10 | # Tries to load the first record; if it fails, then create is called with the same arguments as this method. 11 | # 12 | # Expects arguments in the same format as +Base.create+. 13 | # 14 | # ==== Examples 15 | # # Find the first user named Penélope or create a new one. 16 | # User.where(:first_name => 'Penélope').first_or_create 17 | # # => 18 | # 19 | # # Find the first user named Penélope or create a new one. 20 | # # We already have one so the existing record will be returned. 21 | # User.where(:first_name => 'Penélope').first_or_create 22 | # # => 23 | # 24 | # # Find the first user named Scarlett or create a new one with a particular last name. 25 | # User.where(:first_name => 'Scarlett').first_or_create(:last_name => 'Johansson') 26 | # # => 27 | # 28 | # # Find the first user named Scarlett or create a new one with a different last name. 29 | # # We already have one so the existing record will be returned. 30 | # User.where(:first_name => 'Scarlett').first_or_create do |user| 31 | # user.last_name = "O'Hara" 32 | # end 33 | # # => 34 | def first_or_create(attributes = nil, options = {}, &block) 35 | first || create(attributes, options, &block) 36 | end 37 | 38 | # Like first_or_create but calls create! so an exception is raised if the created record is invalid. 39 | # 40 | # Expects arguments in the same format as Base.create!. 41 | def first_or_create!(attributes = nil, options = {}, &block) 42 | first || create!(attributes, options, &block) 43 | end 44 | 45 | # Like first_or_create but calls new instead of create. 46 | # 47 | # Expects arguments in the same format as Base.new. 48 | def first_or_initialize(attributes = nil, options = {}, &block) 49 | first || new(attributes, options, &block) 50 | end 51 | 52 | def find_or_initialize_by(attributes, options = {}, &block) 53 | find_by(attributes) || new(attributes, options, &block) 54 | end 55 | 56 | def find_or_create_by(attributes, options = {}, &block) 57 | find_by(attributes) || create(attributes, options, &block) 58 | end 59 | 60 | def find_or_create_by!(attributes, options = {}, &block) 61 | find_by(attributes) || create!(attributes, options, &block) 62 | end 63 | end 64 | 65 | module QueryMethods 66 | protected 67 | 68 | def sanitize_forbidden_attributes(attributes) #:nodoc: 69 | if !model._uses_mass_assignment_security 70 | sanitize_for_mass_assignment(attributes) 71 | else 72 | attributes 73 | end 74 | end 75 | end 76 | end 77 | -------------------------------------------------------------------------------- /test/mass_assignment_security/strong_parameters_fallback_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | require 'ar_helper' 3 | require 'rack/test' 4 | require 'action_controller/metal/strong_parameters' 5 | require 'active_record/mass_assignment_security' 6 | require 'active_model/mass_assignment_security' 7 | require 'models/book' 8 | require 'models/person' 9 | 10 | 11 | class StrongParametersFallbackTest < ActiveModel::TestCase 12 | test "AR, use strong parameters when no protection macro (attr_accessible, attr_protected) was used." do 13 | untrusted_params = ActionController::Parameters.new(title: 'Agile Development with Ruby on Rails') 14 | 15 | assert_raises(ActiveModel::ForbiddenAttributesError) { Book.new untrusted_params } 16 | assert_raises(ActiveModel::ForbiddenAttributesError) { Book.new.attributes = untrusted_params } 17 | assert_raises(ActiveModel::ForbiddenAttributesError) { Book.where(title: 'Agile Development with Ruby on Rails').first_or_initialize(untrusted_params) } 18 | assert_raises(ActiveModel::ForbiddenAttributesError) { Book.where(title: 'Agile Development with Ruby on Rails').first_or_create(untrusted_params) } 19 | assert_raises(ActiveModel::ForbiddenAttributesError) { Book.where(title: 'Agile Development with Ruby on Rails').first_or_create!(untrusted_params) } 20 | assert_raises(ActiveModel::ForbiddenAttributesError) { Book.find_or_initialize_by(untrusted_params) } 21 | assert_raises(ActiveModel::ForbiddenAttributesError) { Book.find_or_create_by(untrusted_params) } 22 | assert_raises(ActiveModel::ForbiddenAttributesError) { Book.find_or_create_by!(untrusted_params) } 23 | end 24 | 25 | test "AR, ignore strong parameters when protection macro was used" do 26 | untrusted_params = ActionController::Parameters.new(first_name: "John") 27 | 28 | assert_nothing_raised { TightPerson.new untrusted_params } 29 | assert_nothing_raised { TightPerson.new.attributes = untrusted_params } 30 | assert_nothing_raised { TightPerson.where(first_name: "John").first_or_initialize(untrusted_params) } 31 | assert_nothing_raised { TightPerson.where(first_name: "John").first_or_create(untrusted_params) } 32 | assert_nothing_raised { TightPerson.where(first_name: "John").first_or_create!(untrusted_params) } 33 | assert_nothing_raised { TightPerson.find_or_initialize_by(untrusted_params) } 34 | assert_nothing_raised { TightPerson.find_or_create_by(untrusted_params) } 35 | assert_nothing_raised { TightPerson.find_or_create_by!(untrusted_params) } 36 | end 37 | 38 | test "with PORO including MassAssignmentSecurity that uses a protection marco" do 39 | klass = Class.new do 40 | include ActiveModel::MassAssignmentSecurity 41 | attr_protected :admin 42 | end 43 | 44 | untrusted_params = ActionController::Parameters.new(admin: true) 45 | assert_equal({}, klass.new.send(:sanitize_for_mass_assignment, untrusted_params)) 46 | end 47 | 48 | test "with PORO including MassAssignmentSecurity that does not use a protection marco" do 49 | klass = Class.new do 50 | include ActiveModel::MassAssignmentSecurity 51 | end 52 | 53 | untrusted_params = ActionController::Parameters.new(name: "37 signals") 54 | assert_raises ActiveModel::ForbiddenAttributesError do 55 | klass.new.send :sanitize_for_mass_assignment, untrusted_params 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /lib/active_record/mass_assignment_security/attribute_assignment.rb: -------------------------------------------------------------------------------- 1 | require 'active_model/mass_assignment_security' 2 | require 'active_record' 3 | 4 | module ActiveRecord 5 | module MassAssignmentSecurity 6 | module AttributeAssignment 7 | extend ActiveSupport::Concern 8 | include ActiveModel::MassAssignmentSecurity 9 | 10 | module ClassMethods 11 | private 12 | 13 | # The primary key and inheritance column can never be set by mass-assignment for security reasons. 14 | def attributes_protected_by_default 15 | default = [ primary_key, inheritance_column ] 16 | default << 'id' unless primary_key.eql? 'id' 17 | default 18 | end 19 | end 20 | 21 | # Allows you to set all the attributes for a particular mass-assignment 22 | # security role by passing in a hash of attributes with keys matching 23 | # the attribute names (which again matches the column names) and the role 24 | # name using the :as option. 25 | # 26 | # To bypass mass-assignment security you can use the :without_protection => true 27 | # option. 28 | # 29 | # class User < ActiveRecord::Base 30 | # attr_accessible :name 31 | # attr_accessible :name, :is_admin, :as => :admin 32 | # end 33 | # 34 | # user = User.new 35 | # user.assign_attributes({ :name => 'Josh', :is_admin => true }) 36 | # user.name # => "Josh" 37 | # user.is_admin? # => false 38 | # 39 | # user = User.new 40 | # user.assign_attributes({ :name => 'Josh', :is_admin => true }, :as => :admin) 41 | # user.name # => "Josh" 42 | # user.is_admin? # => true 43 | # 44 | # user = User.new 45 | # user.assign_attributes({ :name => 'Josh', :is_admin => true }, :without_protection => true) 46 | # user.name # => "Josh" 47 | # user.is_admin? # => true 48 | def assign_attributes(new_attributes, options = {}) 49 | return if new_attributes.blank? 50 | 51 | attributes = new_attributes.stringify_keys 52 | multi_parameter_attributes = [] 53 | nested_parameter_attributes = [] 54 | previous_options = @mass_assignment_options 55 | @mass_assignment_options = options 56 | 57 | unless options[:without_protection] 58 | attributes = sanitize_for_mass_assignment(attributes, mass_assignment_role) 59 | end 60 | 61 | attributes.each do |k, v| 62 | if k.include?("(") 63 | multi_parameter_attributes << [ k, v ] 64 | elsif v.is_a?(Hash) 65 | nested_parameter_attributes << [ k, v ] 66 | else 67 | _assign_attribute(k, v) 68 | end 69 | end 70 | 71 | assign_nested_parameter_attributes(nested_parameter_attributes) unless nested_parameter_attributes.empty? 72 | assign_multiparameter_attributes(multi_parameter_attributes) unless multi_parameter_attributes.empty? 73 | ensure 74 | @mass_assignment_options = previous_options 75 | end 76 | 77 | # alias attributes= so it points to the new assign_attributes method 78 | alias attributes= assign_attributes 79 | 80 | protected 81 | 82 | def mass_assignment_options 83 | @mass_assignment_options ||= {} 84 | end 85 | 86 | def mass_assignment_role 87 | mass_assignment_options[:as] || :default 88 | end 89 | end 90 | end 91 | end 92 | -------------------------------------------------------------------------------- /lib/active_record/mass_assignment_security/persistence.rb: -------------------------------------------------------------------------------- 1 | require 'active_support/concern' 2 | 3 | module ActiveRecord 4 | module MassAssignmentSecurity 5 | # = Active Record Persistence 6 | module Persistence 7 | extend ActiveSupport::Concern 8 | 9 | module ClassMethods 10 | # Creates an object (or multiple objects) and saves it to the database, if validations pass. 11 | # The resulting object is returned whether the object was saved successfully to the database or not. 12 | # 13 | # The +attributes+ parameter can be either a Hash or an Array of Hashes. These Hashes describe the 14 | # attributes on the objects that are to be created. 15 | # 16 | # +create+ respects mass-assignment security and accepts either +:as+ or +:without_protection+ options 17 | # in the +options+ parameter. 18 | # 19 | # ==== Examples 20 | # # Create a single new object 21 | # User.create(:first_name => 'Jamie') 22 | # 23 | # # Create a single new object using the :admin mass-assignment security role 24 | # User.create({ :first_name => 'Jamie', :is_admin => true }, :as => :admin) 25 | # 26 | # # Create a single new object bypassing mass-assignment security 27 | # User.create({ :first_name => 'Jamie', :is_admin => true }, :without_protection => true) 28 | # 29 | # # Create an Array of new objects 30 | # User.create([{ :first_name => 'Jamie' }, { :first_name => 'Jeremy' }]) 31 | # 32 | # # Create a single object and pass it into a block to set other attributes. 33 | # User.create(:first_name => 'Jamie') do |u| 34 | # u.is_admin = false 35 | # end 36 | # 37 | # # Creating an Array of new objects using a block, where the block is executed for each object: 38 | # User.create([{ :first_name => 'Jamie' }, { :first_name => 'Jeremy' }]) do |u| 39 | # u.is_admin = false 40 | # end 41 | def create(attributes = nil, options = {}, &block) 42 | if attributes.is_a?(Array) 43 | attributes.collect { |attr| create(attr, options, &block) } 44 | else 45 | object = new(attributes, options, &block) 46 | object.save 47 | object 48 | end 49 | end 50 | end 51 | 52 | # Updates the attributes of the model from the passed-in hash and saves the 53 | # record, all wrapped in a transaction. If the object is invalid, the saving 54 | # will fail and false will be returned. 55 | # 56 | # When updating model attributes, mass-assignment security protection is respected. 57 | # If no +:as+ option is supplied then the +:default+ role will be used. 58 | # If you want to bypass the forbidden attributes protection then you can do so using 59 | # the +:without_protection+ option. 60 | def update(attributes, options = {}) 61 | # The following transaction covers any possible database side-effects of the 62 | # attributes assignment. For example, setting the IDs of a child collection. 63 | with_transaction_returning_status do 64 | assign_attributes(attributes, options) 65 | save 66 | end 67 | end 68 | alias :update_attributes :update 69 | 70 | # Updates its receiver just like +update_attributes+ but calls save! instead 71 | # of +save+, so an exception is raised if the record is invalid. 72 | def update!(attributes, options = {}) 73 | # The following transaction covers any possible database side-effects of the 74 | # attributes assignment. For example, setting the IDs of a child collection. 75 | with_transaction_returning_status do 76 | assign_attributes(attributes, options) 77 | save! 78 | end 79 | end 80 | alias :update_attributes! :update! 81 | end 82 | end 83 | end 84 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Protected Attributes 2 | 3 | [![Build Status](https://api.travis-ci.org/rails/protected_attributes.svg?branch=master)](https://travis-ci.org/rails/protected_attributes) 4 | 5 | Protect attributes from mass-assignment in Active Record models. 6 | 7 | This plugin adds the class methods `attr_accessible` and `attr_protected` to your models to be able to declare white or black lists of attributes. 8 | 9 | Note: This plugin will be officially supported until the release of Rails 5.0. 10 | 11 | ## Installation 12 | 13 | Add this line to your application's `Gemfile`: 14 | 15 | gem 'protected_attributes' 16 | 17 | And then execute: 18 | 19 | bundle install 20 | 21 | ## Usage 22 | 23 | Mass assignment security provides an interface for protecting attributes from end-user injection. This plugin provides two class methods in Active Record classes to control access to their attributes. The `attr_protected` method takes a list of attributes that will be ignored in mass-assignment. 24 | 25 | For example: 26 | ```ruby 27 | attr_protected :admin 28 | ``` 29 | `attr_protected` also optionally takes a role option using `:as` which allows you to define multiple mass-assignment groupings. If no role is defined then attributes will be added to the `:default` role. 30 | 31 | ```ruby 32 | attr_protected :last_login, :as => :admin 33 | ``` 34 | A much better way, because it follows the whitelist-principle, is the `attr_accessible` method. It is the exact opposite of `attr_protected`, because it takes a list of attributes that will be mass-assigned if present. Any other attributes will be ignored. This way you won’t forget to protect attributes when adding new ones in the course of development. Here is an example: 35 | 36 | ```ruby 37 | attr_accessible :name 38 | attr_accessible :name, :is_admin, :as => :admin 39 | ``` 40 | 41 | If you want to set a protected attribute, you will have to assign it individually: 42 | 43 | ```ruby 44 | params[:user] # => {:name => "owned", :is_admin => true} 45 | @user = User.new(params[:user]) 46 | @user.is_admin # => false, not mass-assigned 47 | @user.is_admin = true 48 | @user.is_admin # => true 49 | ``` 50 | 51 | When assigning attributes in Active Record using `attributes=` the `:default` role will be used. To assign attributes using different roles you should use `assign_attributes` which accepts an optional `:as` options parameter. If no `:as` option is provided then the `:default` role will be used. 52 | 53 | You can also bypass mass-assignment security by using the `:without_protection` option. Here is an example: 54 | 55 | ```ruby 56 | @user = User.new 57 | 58 | @user.assign_attributes(:name => 'Josh', :is_admin => true) 59 | @user.name # => Josh 60 | @user.is_admin # => false 61 | 62 | @user.assign_attributes({ :name => 'Josh', :is_admin => true }, :as => :admin) 63 | @user.name # => Josh 64 | @user.is_admin # => true 65 | 66 | @user.assign_attributes({ :name => 'Josh', :is_admin => true }, :without_protection => true) 67 | @user.name # => Josh 68 | @user.is_admin # => true 69 | ``` 70 | 71 | In a similar way, `new`, `create`, `create!`, `update_attributes` and `update_attributes!` methods all respect mass-assignment security and accept either `:as` or `:without_protection` options. For example: 72 | 73 | ```ruby 74 | @user = User.new({ :name => 'Sebastian', :is_admin => true }, :as => :admin) 75 | @user.name # => Sebastian 76 | @user.is_admin # => true 77 | 78 | @user = User.create({ :name => 'Sebastian', :is_admin => true }, :without_protection => true) 79 | @user.name # => Sebastian 80 | @user.is_admin # => true 81 | ``` 82 | 83 | By default the gem will use the strong parameters protection when assigning attribute, unless your model has `attr_accessible` or `attr_protected` calls. 84 | 85 | ### Errors 86 | 87 | By default, attributes in the params hash which are not allowed to be updated are just ignored. If you prefer an exception to be raised configure: 88 | 89 | ```ruby 90 | config.active_record.mass_assignment_sanitizer = :strict 91 | ``` 92 | 93 | Any protected attributes violation raises `ActiveModel::MassAssignmentSecurity::Error` then. 94 | 95 | ## Contributing 96 | 97 | 1. Fork it 98 | 2. Create your feature branch (`git checkout -b my-new-feature`) 99 | 3. Commit your changes (`git commit -am 'Add some feature'`) 100 | 4. Push to the branch (`git push origin my-new-feature`) 101 | 5. Create new Pull Request 102 | -------------------------------------------------------------------------------- /lib/active_record/mass_assignment_security/associations.rb: -------------------------------------------------------------------------------- 1 | module ActiveRecord 2 | module Associations 3 | class Association 4 | undef :build_record 5 | 6 | def build_record(attributes, options) 7 | reflection.build_association(attributes, options) do |record| 8 | attributes = create_scope.except(*(record.changed - [reflection.foreign_key])) 9 | record.assign_attributes(attributes, without_protection: true) 10 | end 11 | end 12 | 13 | private :build_record 14 | end 15 | 16 | class CollectionAssociation 17 | undef :build 18 | undef :create 19 | undef :create! 20 | 21 | def build(attributes = {}, options = {}, &block) 22 | if attributes.is_a?(Array) 23 | attributes.collect { |attr| build(attr, options, &block) } 24 | else 25 | add_to_target(build_record(attributes, options)) do |record| 26 | yield(record) if block_given? 27 | end 28 | end 29 | end 30 | 31 | def create(attributes = {}, options = {}, &block) 32 | create_record(attributes, options, &block) 33 | end 34 | 35 | def create!(attributes = {}, options = {}, &block) 36 | create_record(attributes, options, true, &block) 37 | end 38 | 39 | def create_record(attributes, options, raise = false, &block) 40 | unless owner.persisted? 41 | raise ActiveRecord::RecordNotSaved, "You cannot call create unless the parent is saved" 42 | end 43 | 44 | if attributes.is_a?(Array) 45 | attributes.collect { |attr| create_record(attr, options, raise, &block) } 46 | else 47 | transaction do 48 | add_to_target(build_record(attributes, options)) do |record| 49 | yield(record) if block_given? 50 | insert_record(record, true, raise) 51 | end 52 | end 53 | end 54 | end 55 | 56 | private :create_record 57 | end 58 | 59 | class CollectionProxy 60 | undef :create 61 | undef :create! 62 | 63 | def build(attributes = {}, options = {}, &block) 64 | @association.build(attributes, options, &block) 65 | end 66 | alias_method :new, :build 67 | 68 | def create(attributes = {}, options = {}, &block) 69 | @association.create(attributes, options, &block) 70 | end 71 | 72 | def create!(attributes = {}, options = {}, &block) 73 | @association.create!(attributes, options, &block) 74 | end 75 | end 76 | 77 | module ThroughAssociation 78 | undef :build_record if respond_to?(:build_record, false) 79 | 80 | private 81 | 82 | def build_record(attributes, options={}) 83 | inverse = source_reflection.inverse_of 84 | target = through_association.target 85 | 86 | if inverse && target && !target.is_a?(Array) 87 | attributes[inverse.foreign_key] = target.id 88 | end 89 | 90 | super(attributes, options) 91 | end 92 | end 93 | 94 | class HasManyThroughAssociation 95 | undef :build_record 96 | undef :options_for_through_record if respond_to?(:options_for_through_record, false) 97 | 98 | def build_record(attributes, options = {}) 99 | ensure_not_nested 100 | 101 | record = super(attributes, options) 102 | 103 | inverse = source_reflection.inverse_of 104 | if inverse 105 | if inverse.macro == :has_many 106 | record.send(inverse.name) << build_through_record(record) 107 | elsif inverse.macro == :has_one 108 | record.send("#{inverse.name}=", build_through_record(record)) 109 | end 110 | end 111 | 112 | record 113 | end 114 | private :build_record 115 | 116 | def options_for_through_record 117 | [through_scope_attributes, without_protection: true] 118 | end 119 | private :options_for_through_record 120 | end 121 | 122 | class SingularAssociation 123 | undef :create 124 | undef :create! 125 | undef :build 126 | 127 | def create(attributes = {}, options = {}, &block) 128 | create_record(attributes, options, &block) 129 | end 130 | 131 | def create!(attributes = {}, options = {}, &block) 132 | create_record(attributes, options, true, &block) 133 | end 134 | 135 | def build(attributes = {}, options = {}) 136 | record = build_record(attributes, options) 137 | yield(record) if block_given? 138 | set_new_record(record) 139 | record 140 | end 141 | 142 | def create_record(attributes, options = {}, raise_error = false) 143 | record = build_record(attributes, options) 144 | yield(record) if block_given? 145 | saved = record.save 146 | set_new_record(record) 147 | raise RecordInvalid.new(record) if !saved && raise_error 148 | record 149 | end 150 | 151 | private :create_record 152 | end 153 | end 154 | end 155 | -------------------------------------------------------------------------------- /test/abstract_unit.rb: -------------------------------------------------------------------------------- 1 | require 'action_dispatch' 2 | require 'action_controller' 3 | require 'active_support/dependencies' 4 | 5 | def active_support_4_0? 6 | ActiveSupport::VERSION::MAJOR == 4 && ActiveSupport::VERSION::MINOR == 0 7 | end 8 | 9 | if active_support_4_0? 10 | require 'active_support/core_ext/class/attribute_accessors' 11 | else 12 | require 'active_support/core_ext/module/attribute_accessors' 13 | end 14 | 15 | module SetupOnce 16 | extend ActiveSupport::Concern 17 | 18 | included do 19 | cattr_accessor :setup_once_block 20 | self.setup_once_block = nil 21 | 22 | setup :run_setup_once 23 | end 24 | 25 | module ClassMethods 26 | def setup_once(&block) 27 | self.setup_once_block = block 28 | end 29 | end 30 | 31 | private 32 | def run_setup_once 33 | if self.setup_once_block 34 | self.setup_once_block.call 35 | self.setup_once_block = nil 36 | end 37 | end 38 | end 39 | 40 | SharedTestRoutes = ActionDispatch::Routing::RouteSet.new 41 | 42 | module ActiveSupport 43 | class TestCase 44 | include SetupOnce 45 | # Hold off drawing routes until all the possible controller classes 46 | # have been loaded. 47 | setup_once do 48 | SharedTestRoutes.draw do 49 | get ':controller(/:action)' 50 | end 51 | 52 | ActionDispatch::IntegrationTest.app.routes.draw do 53 | get ':controller(/:action)' 54 | end 55 | end 56 | end 57 | end 58 | 59 | class RoutedRackApp 60 | attr_reader :routes 61 | 62 | def initialize(routes, &blk) 63 | @routes = routes 64 | @stack = ActionDispatch::MiddlewareStack.new(&blk).build(@routes) 65 | end 66 | 67 | def call(env) 68 | @stack.call(env) 69 | end 70 | end 71 | 72 | class ActionDispatch::IntegrationTest < ActiveSupport::TestCase 73 | setup do 74 | @routes = SharedTestRoutes 75 | end 76 | 77 | def self.build_app(routes = nil) 78 | RoutedRackApp.new(routes || ActionDispatch::Routing::RouteSet.new) do |middleware| 79 | middleware.use "ActionDispatch::DebugExceptions" 80 | middleware.use "ActionDispatch::Callbacks" 81 | middleware.use "ActionDispatch::ParamsParser" 82 | middleware.use "ActionDispatch::Cookies" 83 | middleware.use "ActionDispatch::Flash" 84 | middleware.use "Rack::Head" 85 | yield(middleware) if block_given? 86 | end 87 | end 88 | 89 | self.app = build_app 90 | 91 | # Stub Rails dispatcher so it does not get controller references and 92 | # simply return the controller#action as Rack::Body. 93 | class StubDispatcher < ::ActionDispatch::Routing::RouteSet::Dispatcher 94 | protected 95 | def controller_reference(controller_param) 96 | controller_param 97 | end 98 | 99 | def dispatch(controller, action, env) 100 | [200, {'Content-Type' => 'text/html'}, ["#{controller}##{action}"]] 101 | end 102 | end 103 | 104 | def self.stub_controllers 105 | old_dispatcher = ActionDispatch::Routing::RouteSet::Dispatcher 106 | ActionDispatch::Routing::RouteSet.module_eval { remove_const :Dispatcher } 107 | ActionDispatch::Routing::RouteSet.module_eval { const_set :Dispatcher, StubDispatcher } 108 | yield ActionDispatch::Routing::RouteSet.new 109 | ensure 110 | ActionDispatch::Routing::RouteSet.module_eval { remove_const :Dispatcher } 111 | ActionDispatch::Routing::RouteSet.module_eval { const_set :Dispatcher, old_dispatcher } 112 | end 113 | 114 | def with_routing(&block) 115 | temporary_routes = ActionDispatch::Routing::RouteSet.new 116 | old_app, self.class.app = self.class.app, self.class.build_app(temporary_routes) 117 | old_routes = SharedTestRoutes 118 | silence_warnings { Object.const_set(:SharedTestRoutes, temporary_routes) } 119 | 120 | yield temporary_routes 121 | ensure 122 | self.class.app = old_app 123 | silence_warnings { Object.const_set(:SharedTestRoutes, old_routes) } 124 | end 125 | 126 | def with_autoload_path(path) 127 | path = File.join(File.dirname(__FILE__), "fixtures", path) 128 | if ActiveSupport::Dependencies.autoload_paths.include?(path) 129 | yield 130 | else 131 | begin 132 | ActiveSupport::Dependencies.autoload_paths << path 133 | yield 134 | ensure 135 | ActiveSupport::Dependencies.autoload_paths.reject! {|p| p == path} 136 | ActiveSupport::Dependencies.clear 137 | end 138 | end 139 | end 140 | end 141 | 142 | module ActionController 143 | class Base 144 | include ActionController::Testing 145 | # This stub emulates the Railtie including the URL helpers from a Rails application 146 | include SharedTestRoutes.url_helpers 147 | include SharedTestRoutes.mounted_helpers 148 | 149 | #self.view_paths = FIXTURE_LOAD_PATH 150 | 151 | def self.test_routes(&block) 152 | routes = ActionDispatch::Routing::RouteSet.new 153 | routes.draw(&block) 154 | include routes.url_helpers 155 | end 156 | end 157 | 158 | class TestCase 159 | include ActionDispatch::TestProcess 160 | 161 | setup do 162 | @routes = SharedTestRoutes 163 | end 164 | end 165 | end 166 | -------------------------------------------------------------------------------- /test/mass_assignment_security_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | require 'active_model/mass_assignment_security' 3 | require 'models/mass_assignment_specific' 4 | 5 | class CustomSanitizer < ActiveModel::MassAssignmentSecurity::Sanitizer 6 | 7 | def process_removed_attributes(klass, attrs) 8 | raise StandardError 9 | end 10 | 11 | end 12 | 13 | class MassAssignmentSecurityTest < ActiveModel::TestCase 14 | def test_attribute_protection 15 | user = User.new 16 | expected = { "name" => "John Smith", "email" => "john@smith.com" } 17 | sanitized = user.sanitize_for_mass_assignment(expected.merge("admin" => true)) 18 | assert_equal expected, sanitized 19 | end 20 | 21 | def test_attribute_protection_when_role_is_nil 22 | user = User.new 23 | expected = { "name" => "John Smith", "email" => "john@smith.com" } 24 | sanitized = user.sanitize_for_mass_assignment(expected.merge("admin" => true), nil) 25 | assert_equal expected, sanitized 26 | end 27 | 28 | def test_only_moderator_role_attribute_accessible 29 | user = SpecialUser.new 30 | expected = { "name" => "John Smith", "email" => "john@smith.com" } 31 | sanitized = user.sanitize_for_mass_assignment(expected.merge("admin" => true), :moderator) 32 | assert_equal expected, sanitized 33 | 34 | sanitized = user.sanitize_for_mass_assignment({ "name" => "John Smith", "email" => "john@smith.com", "admin" => true }) 35 | assert_equal({}, sanitized) 36 | end 37 | 38 | def test_attributes_accessible 39 | user = Person.new 40 | expected = { "name" => "John Smith", "email" => "john@smith.com" } 41 | sanitized = user.sanitize_for_mass_assignment(expected.merge("admin" => true)) 42 | assert_equal expected, sanitized 43 | end 44 | 45 | def test_attributes_accessible_with_admin_role 46 | user = Person.new 47 | expected = { "name" => "John Smith", "email" => "john@smith.com", "admin" => true } 48 | sanitized = user.sanitize_for_mass_assignment(expected.merge("super_powers" => true), :admin) 49 | assert_equal expected, sanitized 50 | end 51 | 52 | def test_attributes_accessible_with_roles_given_as_array 53 | user = Account.new 54 | expected = { "name" => "John Smith", "email" => "john@smith.com" } 55 | sanitized = user.sanitize_for_mass_assignment(expected.merge("admin" => true)) 56 | assert_equal expected, sanitized 57 | end 58 | 59 | def test_attributes_accessible_with_admin_role_when_roles_given_as_array 60 | user = Account.new 61 | expected = { "name" => "John Smith", "email" => "john@smith.com", "admin" => true } 62 | sanitized = user.sanitize_for_mass_assignment(expected.merge("super_powers" => true), :admin) 63 | assert_equal expected, sanitized 64 | end 65 | 66 | def test_attributes_protected_by_default 67 | firm = Firm.new 68 | expected = { } 69 | sanitized = firm.sanitize_for_mass_assignment({ "type" => "Client" }) 70 | assert_equal expected, sanitized 71 | end 72 | 73 | def test_mass_assignment_protection_inheritance 74 | assert SpecialLoosePerson.accessible_attributes.blank? 75 | assert_equal Set.new(['credit_rating', 'administrator']), SpecialLoosePerson.protected_attributes 76 | 77 | assert SpecialLoosePerson.accessible_attributes.blank? 78 | assert_equal Set.new(['credit_rating']), SpecialLoosePerson.protected_attributes(:admin) 79 | 80 | assert LooseDescendant.accessible_attributes.blank? 81 | assert_equal Set.new(['credit_rating', 'administrator', 'phone_number']), LooseDescendant.protected_attributes 82 | 83 | assert LooseDescendantSecond.accessible_attributes.blank? 84 | assert_equal Set.new(['credit_rating', 'administrator', 'phone_number', 'name']), LooseDescendantSecond.protected_attributes, 85 | 'Running attr_protected twice in one class should merge the protections' 86 | 87 | assert((SpecialTightPerson.protected_attributes - SpecialTightPerson.attributes_protected_by_default).blank?) 88 | assert_equal Set.new(['name', 'address']), SpecialTightPerson.accessible_attributes 89 | 90 | assert((SpecialTightPerson.protected_attributes(:admin) - SpecialTightPerson.attributes_protected_by_default).blank?) 91 | assert_equal Set.new(['name', 'address', 'admin']), SpecialTightPerson.accessible_attributes(:admin) 92 | 93 | assert((TightDescendant.protected_attributes - TightDescendant.attributes_protected_by_default).blank?) 94 | assert_equal Set.new(['name', 'address', 'phone_number']), TightDescendant.accessible_attributes 95 | 96 | assert((TightDescendant.protected_attributes(:admin) - TightDescendant.attributes_protected_by_default).blank?) 97 | assert_equal Set.new(['name', 'address', 'admin', 'super_powers']), TightDescendant.accessible_attributes(:admin) 98 | end 99 | 100 | def test_mass_assignment_multiparameter_protector 101 | task = Task.new 102 | attributes = { "starting(1i)" => "2004", "starting(2i)" => "6", "starting(3i)" => "24" } 103 | sanitized = task.sanitize_for_mass_assignment(attributes) 104 | assert_equal sanitized, { } 105 | end 106 | 107 | def test_custom_sanitizer 108 | old_sanitizer = User._mass_assignment_sanitizer 109 | 110 | user = User.new 111 | User.mass_assignment_sanitizer = CustomSanitizer.new 112 | assert_raise StandardError do 113 | user.sanitize_for_mass_assignment("admin" => true) 114 | end 115 | ensure 116 | User.mass_assignment_sanitizer = old_sanitizer 117 | end 118 | end 119 | -------------------------------------------------------------------------------- /lib/active_record/mass_assignment_security/nested_attributes.rb: -------------------------------------------------------------------------------- 1 | module ActiveRecord 2 | module MassAssignmentSecurity 3 | module NestedAttributes 4 | extend ActiveSupport::Concern 5 | 6 | module ClassMethods 7 | 8 | REJECT_ALL_BLANK_PROC = proc { |attributes| attributes.all? { |key, value| key == '_destroy' || value.blank? } } 9 | 10 | def accepts_nested_attributes_for(*attr_names) 11 | options = { :allow_destroy => false, :update_only => false } 12 | options.update(attr_names.extract_options!) 13 | options.assert_valid_keys(:allow_destroy, :reject_if, :limit, :update_only) 14 | options[:reject_if] = REJECT_ALL_BLANK_PROC if options[:reject_if] == :all_blank 15 | 16 | attr_names.each do |association_name| 17 | if reflection = reflect_on_association(association_name) 18 | if ActiveRecord::VERSION::MAJOR == 4 && ActiveRecord::VERSION::MINOR == 0 19 | reflection.options[:autosave] = true 20 | else 21 | reflection.autosave = true 22 | end 23 | add_autosave_association_callbacks(reflection) 24 | 25 | nested_attributes_options = self.nested_attributes_options.dup 26 | nested_attributes_options[association_name.to_sym] = options 27 | self.nested_attributes_options = nested_attributes_options 28 | 29 | type = (reflection.collection? ? :collection : :one_to_one) 30 | 31 | generated_methods_module = (ActiveRecord::VERSION::MAJOR == 4 && ActiveRecord::VERSION::MINOR == 0) ? generated_feature_methods : generated_association_methods 32 | 33 | # def pirate_attributes=(attributes) 34 | # assign_nested_attributes_for_one_to_one_association(:pirate, attributes, mass_assignment_options) 35 | # end 36 | generated_methods_module.module_eval <<-eoruby, __FILE__, __LINE__ + 1 37 | if method_defined?(:#{association_name}_attributes=) 38 | remove_method(:#{association_name}_attributes=) 39 | end 40 | def #{association_name}_attributes=(attributes) 41 | assign_nested_attributes_for_#{type}_association(:#{association_name}, attributes, mass_assignment_options) 42 | end 43 | eoruby 44 | else 45 | raise ArgumentError, "No association found for name `#{association_name}'. Has it been defined yet?" 46 | end 47 | end 48 | end 49 | end 50 | 51 | private 52 | 53 | UNASSIGNABLE_KEYS = %w( id _destroy ) 54 | 55 | def assign_nested_attributes_for_one_to_one_association(association_name, attributes, assignment_opts = {}) 56 | options = self.nested_attributes_options[association_name] 57 | attributes = attributes.with_indifferent_access 58 | 59 | if (options[:update_only] || !attributes['id'].blank?) && (record = send(association_name)) && 60 | (options[:update_only] || record.id.to_s == attributes['id'].to_s) 61 | assign_to_or_mark_for_destruction(record, attributes, options[:allow_destroy], assignment_opts) unless call_reject_if(association_name, attributes) 62 | 63 | elsif attributes['id'].present? && !assignment_opts[:without_protection] 64 | raise_nested_attributes_record_not_found!(association_name, attributes['id']) 65 | 66 | elsif !reject_new_record?(association_name, attributes) 67 | method = "build_#{association_name}" 68 | if respond_to?(method) 69 | send(method, attributes.except(*unassignable_keys(assignment_opts)), assignment_opts) 70 | else 71 | raise ArgumentError, "Cannot build association `#{association_name}'. Are you trying to build a polymorphic one-to-one association?" 72 | end 73 | end 74 | end 75 | 76 | def assign_nested_attributes_for_collection_association(association_name, attributes_collection, assignment_opts = {}) 77 | options = self.nested_attributes_options[association_name] 78 | 79 | unless attributes_collection.is_a?(Hash) || attributes_collection.is_a?(Array) 80 | raise ArgumentError, "Hash or Array expected, got #{attributes_collection.class.name} (#{attributes_collection.inspect})" 81 | end 82 | 83 | if limit = options[:limit] 84 | limit = case limit 85 | when Symbol 86 | send(limit) 87 | when Proc 88 | limit.call 89 | else 90 | limit 91 | end 92 | 93 | if limit && attributes_collection.size > limit 94 | raise ::ActiveRecord::NestedAttributes::TooManyRecords, "Maximum #{limit} records are allowed. Got #{attributes_collection.size} records instead." 95 | end 96 | end 97 | 98 | if attributes_collection.is_a? Hash 99 | keys = attributes_collection.keys 100 | attributes_collection = if keys.include?('id') || keys.include?(:id) 101 | [attributes_collection] 102 | else 103 | attributes_collection.values 104 | end 105 | end 106 | 107 | association = association(association_name) 108 | 109 | existing_records = if association.loaded? 110 | association.target 111 | else 112 | attribute_ids = attributes_collection.map {|a| a['id'] || a[:id] }.compact 113 | attribute_ids.empty? ? [] : association.scope.where(association.klass.primary_key => attribute_ids) 114 | end 115 | 116 | attributes_collection.each do |attributes| 117 | attributes = attributes.with_indifferent_access 118 | 119 | if attributes['id'].blank? 120 | unless reject_new_record?(association_name, attributes) 121 | association.build(attributes.except(*unassignable_keys(assignment_opts)), assignment_opts) 122 | end 123 | elsif existing_record = existing_records.detect { |record| record.id.to_s == attributes['id'].to_s } 124 | unless association.loaded? || call_reject_if(association_name, attributes) 125 | # Make sure we are operating on the actual object which is in the association's 126 | # proxy_target array (either by finding it, or adding it if not found) 127 | target_record = association.target.detect { |record| record == existing_record } 128 | 129 | if target_record 130 | existing_record = target_record 131 | else 132 | association.add_to_target(existing_record) 133 | end 134 | end 135 | 136 | if !call_reject_if(association_name, attributes) 137 | assign_to_or_mark_for_destruction(existing_record, attributes, options[:allow_destroy], assignment_opts) 138 | end 139 | elsif assignment_opts[:without_protection] 140 | association.build(attributes.except(*unassignable_keys(assignment_opts)), assignment_opts) 141 | else 142 | raise_nested_attributes_record_not_found!(association_name, attributes['id']) 143 | end 144 | end 145 | end 146 | 147 | def assign_to_or_mark_for_destruction(record, attributes, allow_destroy, assignment_opts) 148 | record.assign_attributes(attributes.except(*unassignable_keys(assignment_opts)), assignment_opts) 149 | record.mark_for_destruction if has_destroy_flag?(attributes) && allow_destroy 150 | end 151 | 152 | def unassignable_keys(assignment_opts) 153 | assignment_opts[:without_protection] ? UNASSIGNABLE_KEYS - %w[id] : UNASSIGNABLE_KEYS 154 | end 155 | end 156 | end 157 | end 158 | -------------------------------------------------------------------------------- /lib/active_model/mass_assignment_security.rb: -------------------------------------------------------------------------------- 1 | require 'active_support/concern' 2 | require 'active_support/core_ext/class/attribute' 3 | require 'active_support/core_ext/string/inflections' 4 | require 'active_model' 5 | require 'active_model/mass_assignment_security/permission_set' 6 | require 'active_model/mass_assignment_security/sanitizer' 7 | 8 | module ActiveModel 9 | # == Active Model Mass-Assignment Security 10 | # 11 | # Mass assignment security provides an interface for protecting attributes 12 | # from end-user assignment. For more complex permissions, mass assignment 13 | # security may be handled outside the model by extending a non-ActiveRecord 14 | # class, such as a controller, with this behavior. 15 | # 16 | # For example, a logged in user may need to assign additional attributes 17 | # depending on their role: 18 | # 19 | # class AccountsController < ApplicationController 20 | # include ActiveModel::MassAssignmentSecurity 21 | # 22 | # attr_accessible :first_name, :last_name 23 | # attr_accessible :first_name, :last_name, :plan_id, as: :admin 24 | # 25 | # def update 26 | # ... 27 | # @account.update_attributes(account_params) 28 | # ... 29 | # end 30 | # 31 | # protected 32 | # 33 | # def account_params 34 | # role = admin ? :admin : :default 35 | # sanitize_for_mass_assignment(params[:account], role) 36 | # end 37 | # 38 | # end 39 | # 40 | # === Configuration options 41 | # 42 | # * mass_assignment_sanitizer - Defines sanitize method. Possible 43 | # values are: 44 | # * :logger (default) - writes filtered attributes to logger 45 | # * :strict - raise ActiveModel::MassAssignmentSecurity::Error 46 | # on any protected attribute update. 47 | # 48 | # You can specify your own sanitizer object eg. MySanitizer.new. 49 | # See ActiveModel::MassAssignmentSecurity::LoggerSanitizer for 50 | # example implementation. 51 | module MassAssignmentSecurity 52 | extend ActiveSupport::Concern 53 | include ActiveModel::ForbiddenAttributesProtection 54 | 55 | included do 56 | class_attribute :_accessible_attributes, instance_writer: false 57 | class_attribute :_protected_attributes, instance_writer: false 58 | class_attribute :_active_authorizer, instance_writer: false 59 | class_attribute :_uses_mass_assignment_security, instance_writer: false 60 | self._uses_mass_assignment_security = false 61 | 62 | class_attribute :_mass_assignment_sanitizer, instance_writer: false 63 | self.mass_assignment_sanitizer = :logger 64 | end 65 | 66 | module ClassMethods 67 | # Attributes named in this macro are protected from mass-assignment 68 | # whenever attributes are sanitized before assignment. A role for the 69 | # attributes is optional, if no role is provided then :default 70 | # is used. A role can be defined by using the :as option with a 71 | # symbol or an array of symbols as the value. 72 | # 73 | # Mass-assignment to these attributes will simply be ignored, to assign 74 | # to them you can use direct writer methods. This is meant to protect 75 | # sensitive attributes from being overwritten by malicious users 76 | # tampering with URLs or forms. 77 | # 78 | # class Customer 79 | # include ActiveModel::MassAssignmentSecurity 80 | # 81 | # attr_accessor :name, :email, :logins_count 82 | # 83 | # attr_protected :logins_count 84 | # # Suppose that admin can not change email for customer 85 | # attr_protected :logins_count, :email, as: :admin 86 | # 87 | # def assign_attributes(values, options = {}) 88 | # sanitize_for_mass_assignment(values, options[:as]).each do |k, v| 89 | # send("#{k}=", v) 90 | # end 91 | # end 92 | # end 93 | # 94 | # When using the :default role: 95 | # 96 | # customer = Customer.new 97 | # customer.assign_attributes({ name: 'David', email: 'a@b.com', logins_count: 5 }, as: :default) 98 | # customer.name # => "David" 99 | # customer.email # => "a@b.com" 100 | # customer.logins_count # => nil 101 | # 102 | # And using the :admin role: 103 | # 104 | # customer = Customer.new 105 | # customer.assign_attributes({ name: 'David', email: 'a@b.com', logins_count: 5}, as: :admin) 106 | # customer.name # => "David" 107 | # customer.email # => nil 108 | # customer.logins_count # => nil 109 | # 110 | # customer.email = 'c@d.com' 111 | # customer.email # => "c@d.com" 112 | # 113 | # To start from an all-closed default and enable attributes as needed, 114 | # have a look at +attr_accessible+. 115 | # 116 | # Note that using Hash#except or Hash#slice in place of 117 | # +attr_protected+ to sanitize attributes provides basically the same 118 | # functionality, but it makes a bit tricky to deal with nested attributes. 119 | def attr_protected(*args) 120 | options = args.extract_options! 121 | role = options[:as] || :default 122 | 123 | self._protected_attributes = protected_attributes_configs.dup 124 | 125 | Array(role).each do |name| 126 | self._protected_attributes[name] = self.protected_attributes(name) + args 127 | end 128 | 129 | self._uses_mass_assignment_security = true 130 | self._active_authorizer = self._protected_attributes 131 | end 132 | 133 | # Specifies a white list of model attributes that can be set via 134 | # mass-assignment. 135 | # 136 | # Like +attr_protected+, a role for the attributes is optional, 137 | # if no role is provided then :default is used. A role can be 138 | # defined by using the :as option with a symbol or an array of 139 | # symbols as the value. 140 | # 141 | # This is the opposite of the +attr_protected+ macro: Mass-assignment 142 | # will only set attributes in this list, to assign to the rest of 143 | # attributes you can use direct writer methods. This is meant to protect 144 | # sensitive attributes from being overwritten by malicious users 145 | # tampering with URLs or forms. If you'd rather start from an all-open 146 | # default and restrict attributes as needed, have a look at 147 | # +attr_protected+. 148 | # 149 | # class Customer 150 | # include ActiveModel::MassAssignmentSecurity 151 | # 152 | # attr_accessor :name, :credit_rating 153 | # 154 | # # Both admin and default user can change name of a customer 155 | # attr_accessible :name, as: [:admin, :default] 156 | # # Only admin can change credit rating of a customer 157 | # attr_accessible :credit_rating, as: :admin 158 | # 159 | # def assign_attributes(values, options = {}) 160 | # sanitize_for_mass_assignment(values, options[:as]).each do |k, v| 161 | # send("#{k}=", v) 162 | # end 163 | # end 164 | # end 165 | # 166 | # When using the :default role: 167 | # 168 | # customer = Customer.new 169 | # customer.assign_attributes({ name: 'David', credit_rating: 'Excellent', last_login: 1.day.ago }, as: :default) 170 | # customer.name # => "David" 171 | # customer.credit_rating # => nil 172 | # 173 | # customer.credit_rating = 'Average' 174 | # customer.credit_rating # => "Average" 175 | # 176 | # And using the :admin role: 177 | # 178 | # customer = Customer.new 179 | # customer.assign_attributes({ name: 'David', credit_rating: 'Excellent', last_login: 1.day.ago }, as: :admin) 180 | # customer.name # => "David" 181 | # customer.credit_rating # => "Excellent" 182 | # 183 | # Note that using Hash#except or Hash#slice in place of 184 | # +attr_accessible+ to sanitize attributes provides basically the same 185 | # functionality, but it makes a bit tricky to deal with nested attributes. 186 | def attr_accessible(*args) 187 | options = args.extract_options! 188 | role = options[:as] || :default 189 | 190 | self._accessible_attributes = accessible_attributes_configs.dup 191 | 192 | Array(role).each do |name| 193 | self._accessible_attributes[name] = self.accessible_attributes(name) + args 194 | end 195 | 196 | self._uses_mass_assignment_security = true 197 | self._active_authorizer = self._accessible_attributes 198 | end 199 | 200 | # Returns an instance of ActiveModel::MassAssignmentSecurity::BlackList 201 | # with the attributes protected by #attr_protected method. If no +role+ 202 | # is provided, then :default is used. 203 | # 204 | # class Customer 205 | # include ActiveModel::MassAssignmentSecurity 206 | # 207 | # attr_accessor :name, :email, :logins_count 208 | # 209 | # attr_protected :logins_count 210 | # attr_protected :logins_count, :email, as: :admin 211 | # end 212 | # 213 | # Customer.protected_attributes 214 | # # => # 215 | # 216 | # Customer.protected_attributes(:default) 217 | # # => # 218 | # 219 | # Customer.protected_attributes(:admin) 220 | # # => # 221 | def protected_attributes(role = :default) 222 | protected_attributes_configs[role] 223 | end 224 | 225 | # Returns an instance of ActiveModel::MassAssignmentSecurity::WhiteList 226 | # with the attributes protected by #attr_accessible method. If no +role+ 227 | # is provided, then :default is used. 228 | # 229 | # class Customer 230 | # include ActiveModel::MassAssignmentSecurity 231 | # 232 | # attr_accessor :name, :credit_rating 233 | # 234 | # attr_accessible :name, as: [:admin, :default] 235 | # attr_accessible :credit_rating, as: :admin 236 | # end 237 | # 238 | # Customer.accessible_attributes 239 | # # => # 240 | # 241 | # Customer.accessible_attributes(:default) 242 | # # => # 243 | # 244 | # Customer.accessible_attributes(:admin) 245 | # # => # 246 | def accessible_attributes(role = :default) 247 | accessible_attributes_configs[role] 248 | end 249 | 250 | # Returns a hash with the protected attributes (by #attr_accessible or 251 | # #attr_protected) per role. 252 | # 253 | # class Customer 254 | # include ActiveModel::MassAssignmentSecurity 255 | # 256 | # attr_accessor :name, :credit_rating 257 | # 258 | # attr_accessible :name, as: [:admin, :default] 259 | # attr_accessible :credit_rating, as: :admin 260 | # end 261 | # 262 | # Customer.active_authorizers 263 | # # => { 264 | # # :admin=> #, 265 | # # :default=># 266 | # # } 267 | def active_authorizers 268 | self._active_authorizer ||= protected_attributes_configs 269 | end 270 | alias active_authorizer active_authorizers 271 | 272 | # Returns an empty array by default. You can still override this to define 273 | # the default attributes protected by #attr_protected method. 274 | # 275 | # class Customer 276 | # include ActiveModel::MassAssignmentSecurity 277 | # 278 | # def self.attributes_protected_by_default 279 | # [:name] 280 | # end 281 | # end 282 | # 283 | # Customer.protected_attributes 284 | # # => # 285 | def attributes_protected_by_default 286 | [] 287 | end 288 | 289 | # Defines sanitize method. 290 | # 291 | # class Customer 292 | # include ActiveModel::MassAssignmentSecurity 293 | # 294 | # attr_accessor :name 295 | # 296 | # attr_protected :name 297 | # 298 | # def assign_attributes(values) 299 | # sanitize_for_mass_assignment(values).each do |k, v| 300 | # send("#{k}=", v) 301 | # end 302 | # end 303 | # end 304 | # 305 | # # See ActiveModel::MassAssignmentSecurity::StrictSanitizer for more information. 306 | # Customer.mass_assignment_sanitizer = :strict 307 | # 308 | # customer = Customer.new 309 | # customer.assign_attributes(name: 'David') 310 | # # => ActiveModel::MassAssignmentSecurity::Error: Can't mass-assign protected attributes for Customer: name 311 | # 312 | # Also, you can specify your own sanitizer object. 313 | # 314 | # class CustomSanitizer < ActiveModel::MassAssignmentSecurity::Sanitizer 315 | # def process_removed_attributes(klass, attrs) 316 | # raise StandardError 317 | # end 318 | # end 319 | # 320 | # Customer.mass_assignment_sanitizer = CustomSanitizer.new 321 | # 322 | # customer = Customer.new 323 | # customer.assign_attributes(name: 'David') 324 | # # => StandardError: StandardError 325 | def mass_assignment_sanitizer=(value) 326 | self._mass_assignment_sanitizer = if value.is_a?(Symbol) 327 | const_get(:"#{value.to_s.camelize}Sanitizer").new(self) 328 | else 329 | value 330 | end 331 | end 332 | 333 | private 334 | 335 | def protected_attributes_configs 336 | self._protected_attributes ||= begin 337 | Hash.new { |h,k| h[k] = BlackList.new(attributes_protected_by_default) } 338 | end 339 | end 340 | 341 | def accessible_attributes_configs 342 | self._accessible_attributes ||= begin 343 | Hash.new { |h,k| h[k] = WhiteList.new } 344 | end 345 | end 346 | end 347 | 348 | protected 349 | 350 | def sanitize_for_mass_assignment(attributes, role = nil) #:nodoc: 351 | if _uses_mass_assignment_security 352 | _mass_assignment_sanitizer.sanitize(self.class, attributes, mass_assignment_authorizer(role)) 353 | else 354 | sanitize_forbidden_attributes(attributes) 355 | end 356 | end 357 | 358 | def mass_assignment_authorizer(role) #:nodoc: 359 | self.class.active_authorizer[role || :default] 360 | end 361 | end 362 | end 363 | -------------------------------------------------------------------------------- /test/attribute_sanitization_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | require 'ar_helper' 3 | require 'active_record/mass_assignment_security' 4 | require 'models/battle' 5 | require 'models/company' 6 | require 'models/group' 7 | require 'models/keyboard' 8 | require 'models/membership' 9 | require 'models/person' 10 | require 'models/pirate' 11 | require 'models/subscriber' 12 | require 'models/task' 13 | require 'models/team' 14 | require 'models/vampire' 15 | require 'models/wolf' 16 | 17 | module MassAssignmentTestHelpers 18 | def teardown 19 | super 20 | ActiveRecord::Base.send(:descendants).each do |klass| 21 | begin 22 | klass.delete_all 23 | rescue 24 | end 25 | end 26 | end 27 | 28 | def attributes_hash 29 | { 30 | :id => 5, 31 | :first_name => 'Josh', 32 | :gender => 'm', 33 | :comments => 'rides a sweet bike' 34 | } 35 | end 36 | 37 | def assert_default_attributes(person, create = false) 38 | unless create 39 | assert_nil person.id 40 | else 41 | assert !!person.id 42 | end 43 | assert_equal 'Josh', person.first_name 44 | assert_equal 'm', person.gender 45 | assert_nil person.comments 46 | end 47 | 48 | def assert_admin_attributes(person, create = false) 49 | unless create 50 | assert_nil person.id 51 | else 52 | assert !!person.id 53 | end 54 | assert_equal 'Josh', person.first_name 55 | assert_equal 'm', person.gender 56 | assert_equal 'rides a sweet bike', person.comments 57 | end 58 | 59 | def assert_all_attributes(person) 60 | assert_equal 5, person.id 61 | assert_equal 'Josh', person.first_name 62 | assert_equal 'm', person.gender 63 | assert_equal 'rides a sweet bike', person.comments 64 | end 65 | 66 | def with_strict_sanitizer 67 | ActiveRecord::Base.mass_assignment_sanitizer = :strict 68 | yield 69 | ensure 70 | ActiveRecord::Base.mass_assignment_sanitizer = :logger 71 | end 72 | end 73 | 74 | module MassAssignmentRelationTestHelpers 75 | def setup 76 | super 77 | @person = LoosePerson.create(attributes_hash) 78 | end 79 | end 80 | 81 | class AttributeSanitizationTest < ActiveSupport::TestCase 82 | include MassAssignmentTestHelpers 83 | 84 | def test_customized_primary_key_remains_protected 85 | subscriber = Subscriber.new(:nick => 'webster123', :name => 'nice try') 86 | assert_nil subscriber.id 87 | 88 | keyboard = Keyboard.new(:key_number => 9, :name => 'nice try') 89 | assert_nil keyboard.id 90 | end 91 | 92 | def test_customized_primary_key_remains_protected_when_referred_to_as_id 93 | subscriber = Subscriber.new(:id => 'webster123', :name => 'nice try') 94 | assert_nil subscriber.id 95 | 96 | keyboard = Keyboard.new(:id => 9, :name => 'nice try') 97 | assert_nil keyboard.id 98 | end 99 | 100 | def test_mass_assigning_invalid_attribute 101 | firm = Firm.new 102 | 103 | assert_raise(ActiveRecord::UnknownAttributeError) do 104 | firm.attributes = { "id" => 5, "type" => "Client", "i_dont_even_exist" => 20 } 105 | end 106 | end 107 | 108 | def test_mass_assigning_does_not_choke_on_nil 109 | assert_nil Firm.new.assign_attributes(nil) 110 | end 111 | 112 | def test_mass_assigning_does_not_choke_on_empty_hash 113 | assert_nil Firm.new.assign_attributes({}) 114 | end 115 | 116 | def test_assign_attributes_uses_default_role_when_no_role_is_provided 117 | p = LoosePerson.new 118 | p.assign_attributes(attributes_hash) 119 | 120 | assert_default_attributes(p) 121 | end 122 | 123 | def test_assign_attributes_skips_mass_assignment_security_protection_when_without_protection_is_used 124 | p = LoosePerson.new 125 | p.assign_attributes(attributes_hash, :without_protection => true) 126 | 127 | assert_all_attributes(p) 128 | end 129 | 130 | def test_assign_attributes_with_default_role_and_attr_protected_attributes 131 | p = LoosePerson.new 132 | p.assign_attributes(attributes_hash, :as => :default) 133 | 134 | assert_default_attributes(p) 135 | end 136 | 137 | def test_assign_attributes_with_admin_role_and_attr_protected_attributes 138 | p = LoosePerson.new 139 | p.assign_attributes(attributes_hash, :as => :admin) 140 | 141 | assert_admin_attributes(p) 142 | end 143 | 144 | def test_assign_attributes_with_default_role_and_attr_accessible_attributes 145 | p = TightPerson.new 146 | p.assign_attributes(attributes_hash, :as => :default) 147 | 148 | assert_default_attributes(p) 149 | end 150 | 151 | def test_assign_attributes_with_admin_role_and_attr_accessible_attributes 152 | p = TightPerson.new 153 | p.assign_attributes(attributes_hash, :as => :admin) 154 | 155 | assert_admin_attributes(p) 156 | end 157 | 158 | def test_new_with_attr_accessible_attributes 159 | p = TightPerson.new(attributes_hash) 160 | 161 | assert_default_attributes(p) 162 | end 163 | 164 | def test_new_with_attr_protected_attributes 165 | p = LoosePerson.new(attributes_hash) 166 | 167 | assert_default_attributes(p) 168 | end 169 | 170 | def test_create_with_attr_accessible_attributes 171 | p = TightPerson.create(attributes_hash) 172 | 173 | assert_default_attributes(p, true) 174 | end 175 | 176 | def test_create_with_attr_protected_attributes 177 | p = LoosePerson.create(attributes_hash) 178 | 179 | assert_default_attributes(p, true) 180 | end 181 | 182 | def test_new_with_admin_role_with_attr_accessible_attributes 183 | p = TightPerson.new(attributes_hash, :as => :admin) 184 | 185 | assert_admin_attributes(p) 186 | end 187 | 188 | def test_new_with_admin_role_with_attr_protected_attributes 189 | p = LoosePerson.new(attributes_hash, :as => :admin) 190 | 191 | assert_admin_attributes(p) 192 | end 193 | 194 | def test_create_with_admin_role_with_attr_accessible_attributes 195 | p = TightPerson.create(attributes_hash, :as => :admin) 196 | 197 | assert_admin_attributes(p, true) 198 | end 199 | 200 | def test_create_with_admin_role_with_attr_protected_attributes 201 | p = LoosePerson.create(attributes_hash, :as => :admin) 202 | 203 | assert_admin_attributes(p, true) 204 | end 205 | 206 | def test_create_with_bang_with_admin_role_with_attr_accessible_attributes 207 | p = TightPerson.create!(attributes_hash, :as => :admin) 208 | 209 | assert_admin_attributes(p, true) 210 | end 211 | 212 | def test_create_with_bang_with_admin_role_with_attr_protected_attributes 213 | p = LoosePerson.create!(attributes_hash, :as => :admin) 214 | 215 | assert_admin_attributes(p, true) 216 | end 217 | 218 | def test_new_with_without_protection_with_attr_accessible_attributes 219 | p = TightPerson.new(attributes_hash, :without_protection => true) 220 | 221 | assert_all_attributes(p) 222 | end 223 | 224 | def test_new_with_without_protection_with_attr_protected_attributes 225 | p = LoosePerson.new(attributes_hash, :without_protection => true) 226 | 227 | assert_all_attributes(p) 228 | end 229 | 230 | def test_create_with_without_protection_with_attr_accessible_attributes 231 | p = TightPerson.create(attributes_hash, :without_protection => true) 232 | 233 | assert_all_attributes(p) 234 | end 235 | 236 | def test_create_with_without_protection_with_attr_protected_attributes 237 | p = LoosePerson.create(attributes_hash, :without_protection => true) 238 | 239 | assert_all_attributes(p) 240 | end 241 | 242 | def test_create_with_bang_with_without_protection_with_attr_accessible_attributes 243 | p = TightPerson.create!(attributes_hash, :without_protection => true) 244 | 245 | assert_all_attributes(p) 246 | end 247 | 248 | def test_create_with_bang_with_without_protection_with_attr_protected_attributes 249 | p = LoosePerson.create!(attributes_hash, :without_protection => true) 250 | 251 | assert_all_attributes(p) 252 | end 253 | 254 | def test_protection_against_class_attribute_writers 255 | attribute_writers = [:logger, :configurations, :primary_key_prefix_type, :table_name_prefix, :table_name_suffix, :pluralize_table_names, 256 | :default_timezone, :schema_format, :lock_optimistically, :timestamped_migrations, :default_scopes, 257 | :connection_handler, :nested_attributes_options, 258 | :attribute_method_matchers, :time_zone_aware_attributes, :skip_time_zone_conversion_for_attributes] 259 | 260 | attribute_writers.push(:_attr_readonly) if ActiveRecord::VERSION::MAJOR == 4 && ActiveRecord::VERSION::MINOR == 0 261 | 262 | attribute_writers.each do |method| 263 | assert_respond_to Task, method 264 | assert_respond_to Task, "#{method}=" 265 | assert_respond_to Task.new, method unless method == :configurations && !(ActiveRecord::VERSION::MAJOR == 4 && ActiveRecord::VERSION::MINOR == 0) 266 | assert !Task.new.respond_to?("#{method}=") 267 | end 268 | end 269 | 270 | def test_new_with_protected_inheritance_column 271 | firm = Company.new(type: "Firm") 272 | assert_equal Company, firm.class 273 | end 274 | 275 | def test_new_with_accessible_inheritance_column 276 | corporation = Corporation.new(type: "SpecialCorporation") 277 | assert_equal SpecialCorporation, corporation.class 278 | end 279 | 280 | def test_new_with_invalid_inheritance_column_class 281 | assert_raise(ActiveRecord::SubclassNotFound) { Corporation.new(type: "InvalidCorporation") } 282 | end 283 | 284 | def test_new_with_unrelated_inheritance_column_class 285 | assert_raise(ActiveRecord::SubclassNotFound) { Corporation.new(type: "Person") } 286 | end 287 | 288 | def test_update_attributes_as_admin 289 | person = TightPerson.create({ "first_name" => 'Joshua' }) 290 | person.update_attributes({ "first_name" => 'Josh', "gender" => 'm', "comments" => 'from NZ' }, :as => :admin) 291 | person.reload 292 | 293 | assert_equal 'Josh', person.first_name 294 | assert_equal 'm', person.gender 295 | assert_equal 'from NZ', person.comments 296 | end 297 | 298 | def test_update_attributes_without_protection 299 | person = TightPerson.create({ "first_name" => 'Joshua' }) 300 | person.update_attributes({ "first_name" => 'Josh', "gender" => 'm', "comments" => 'from NZ' }, :without_protection => true) 301 | person.reload 302 | 303 | assert_equal 'Josh', person.first_name 304 | assert_equal 'm', person.gender 305 | assert_equal 'from NZ', person.comments 306 | end 307 | 308 | def test_update_attributes_with_bang_as_admin 309 | person = TightPerson.create({ "first_name" => 'Joshua' }) 310 | person.update_attributes!({ "first_name" => 'Josh', "gender" => 'm', "comments" => 'from NZ' }, :as => :admin) 311 | person.reload 312 | 313 | assert_equal 'Josh', person.first_name 314 | assert_equal 'm', person.gender 315 | assert_equal 'from NZ', person.comments 316 | end 317 | 318 | def test_update_attributes_with_bang_without_protection 319 | person = TightPerson.create({ "first_name" => 'Joshua' }) 320 | person.update_attributes!({ "first_name" => 'Josh', "gender" => 'm', "comments" => 'from NZ' }, :without_protection => true) 321 | person.reload 322 | 323 | assert_equal 'Josh', person.first_name 324 | assert_equal 'm', person.gender 325 | assert_equal 'from NZ', person.comments 326 | end 327 | 328 | def test_update_as_admin 329 | person = TightPerson.create({ "first_name" => 'Joshua' }) 330 | person.update({ "first_name" => 'Josh', "gender" => 'm', "comments" => 'from NZ' }, :as => :admin) 331 | person.reload 332 | 333 | assert_equal 'Josh', person.first_name 334 | assert_equal 'm', person.gender 335 | assert_equal 'from NZ', person.comments 336 | end 337 | 338 | def test_update_without_protection 339 | person = TightPerson.create({ "first_name" => 'Joshua' }) 340 | person.update({ "first_name" => 'Josh', "gender" => 'm', "comments" => 'from NZ' }, :without_protection => true) 341 | person.reload 342 | 343 | assert_equal 'Josh', person.first_name 344 | assert_equal 'm', person.gender 345 | assert_equal 'from NZ', person.comments 346 | end 347 | 348 | def test_update_with_bang_as_admin 349 | person = TightPerson.create({ "first_name" => 'Joshua' }) 350 | person.update!({ "first_name" => 'Josh', "gender" => 'm', "comments" => 'from NZ' }, :as => :admin) 351 | person.reload 352 | 353 | assert_equal 'Josh', person.first_name 354 | assert_equal 'm', person.gender 355 | assert_equal 'from NZ', person.comments 356 | end 357 | 358 | def test_update_with_bang_without_protection 359 | person = TightPerson.create({ "first_name" => 'Joshua' }) 360 | person.update!({ "first_name" => 'Josh', "gender" => 'm', "comments" => 'from NZ' }, :without_protection => true) 361 | person.reload 362 | 363 | assert_equal 'Josh', person.first_name 364 | assert_equal 'm', person.gender 365 | assert_equal 'from NZ', person.comments 366 | end 367 | end 368 | 369 | class MassAssignmentSecurityRelationTest < ActiveSupport::TestCase 370 | include MassAssignmentTestHelpers 371 | 372 | def test_find_or_initialize_by_with_attr_accessible_attributes 373 | p = TightPerson.where(first_name: 'Josh').first_or_initialize(attributes_hash) 374 | 375 | assert_default_attributes(p) 376 | end 377 | 378 | def test_find_or_initialize_by_with_admin_role_with_attr_accessible_attributes 379 | p = TightPerson.where(first_name: 'Josh').first_or_initialize(attributes_hash, as: :admin) 380 | 381 | assert_admin_attributes(p) 382 | end 383 | 384 | def test_find_or_initialize_by_with_attr_protected_attributes 385 | p = LoosePerson.where(first_name: 'Josh').first_or_initialize(attributes_hash) 386 | 387 | assert_default_attributes(p) 388 | end 389 | 390 | def test_find_or_initialize_by_with_admin_role_with_attr_protected_attributes 391 | p = LoosePerson.where(first_name: 'Josh').first_or_initialize(attributes_hash, as: :admin) 392 | 393 | assert_admin_attributes(p) 394 | end 395 | 396 | def test_find_or_create_by_with_attr_accessible_attributes 397 | p = TightPerson.where(first_name: 'Josh').first_or_create(attributes_hash) 398 | 399 | assert_default_attributes(p, true) 400 | end 401 | 402 | def test_find_or_create_by_with_admin_role_with_attr_accessible_attributes 403 | p = TightPerson.where(first_name: 'Josh').first_or_create(attributes_hash, as: :admin) 404 | 405 | assert_admin_attributes(p, true) 406 | end 407 | 408 | def test_find_or_create_by_with_attr_protected_attributes 409 | p = LoosePerson.where(first_name: 'Josh').first_or_create(attributes_hash) 410 | 411 | assert_default_attributes(p, true) 412 | end 413 | 414 | def test_find_or_create_by_with_admin_role_with_attr_protected_attributes 415 | p = LoosePerson.where(first_name: 'Josh').first_or_create(attributes_hash, as: :admin) 416 | 417 | assert_admin_attributes(p, true) 418 | end 419 | 420 | def test_find_or_create_by_bang_with_attr_accessible_attributes 421 | p = TightPerson.where(first_name: 'Josh').first_or_create!(attributes_hash) 422 | 423 | assert_default_attributes(p, true) 424 | end 425 | 426 | def test_find_or_create_by_bang_with_admin_role_with_attr_accessible_attributes 427 | p = TightPerson.where(first_name: 'Josh').first_or_create!(attributes_hash, as: :admin) 428 | 429 | assert_admin_attributes(p, true) 430 | end 431 | 432 | def test_find_or_create_by_bang_with_attr_protected_attributes 433 | p = LoosePerson.where(first_name: 'Josh').first_or_create!(attributes_hash) 434 | 435 | assert_default_attributes(p, true) 436 | end 437 | 438 | def test_find_or_create_by_bang_with_admin_role_with_attr_protected_attributes 439 | p = LoosePerson.where(first_name: 'Josh').first_or_create!(attributes_hash, as: :admin) 440 | 441 | assert_admin_attributes(p, true) 442 | end 443 | end 444 | 445 | class MassAssignmentSecurityFindersTest < ActiveSupport::TestCase 446 | include MassAssignmentTestHelpers 447 | 448 | def test_find_or_initialize_by_with_attr_accessible_attributes 449 | p = TightPerson.find_or_initialize_by(attributes_hash) 450 | 451 | assert_default_attributes(p) 452 | end 453 | 454 | def test_find_or_initialize_by_with_admin_role_with_attr_accessible_attributes 455 | p = TightPerson.find_or_initialize_by(attributes_hash, as: :admin) 456 | 457 | assert_admin_attributes(p) 458 | end 459 | 460 | def test_find_or_initialize_by_with_attr_protected_attributes 461 | p = LoosePerson.find_or_initialize_by(attributes_hash) 462 | 463 | assert_default_attributes(p) 464 | end 465 | 466 | def test_find_or_initialize_by_with_admin_role_with_attr_protected_attributes 467 | p = LoosePerson.find_or_initialize_by(attributes_hash, as: :admin) 468 | 469 | assert_admin_attributes(p) 470 | end 471 | 472 | def test_find_or_create_by_with_attr_accessible_attributes 473 | p = TightPerson.find_or_create_by(attributes_hash) 474 | 475 | assert_default_attributes(p, true) 476 | end 477 | 478 | def test_find_or_create_by_with_admin_role_with_attr_accessible_attributes 479 | p = TightPerson.find_or_create_by(attributes_hash, as: :admin) 480 | 481 | assert_admin_attributes(p, true) 482 | end 483 | 484 | def test_find_or_create_by_with_attr_protected_attributes 485 | p = LoosePerson.find_or_create_by(attributes_hash) 486 | 487 | assert_default_attributes(p, true) 488 | end 489 | 490 | def test_find_or_create_by_with_admin_role_with_attr_protected_attributes 491 | p = LoosePerson.find_or_create_by(attributes_hash, as: :admin) 492 | 493 | assert_admin_attributes(p, true) 494 | end 495 | 496 | def test_find_or_create_by_bang_with_attr_accessible_attributes 497 | p = TightPerson.find_or_create_by!(attributes_hash) 498 | 499 | assert_default_attributes(p, true) 500 | end 501 | 502 | def test_find_or_create_by_bang_with_admin_role_with_attr_accessible_attributes 503 | p = TightPerson.find_or_create_by!(attributes_hash, as: :admin) 504 | 505 | assert_admin_attributes(p, true) 506 | end 507 | 508 | def test_find_or_create_by_bang_with_attr_protected_attributes 509 | p = LoosePerson.find_or_create_by!(attributes_hash) 510 | 511 | assert_default_attributes(p, true) 512 | end 513 | 514 | def test_find_or_create_by_bang_with_admin_role_with_attr_protected_attributes 515 | p = LoosePerson.find_or_create_by!(attributes_hash, as: :admin) 516 | 517 | assert_admin_attributes(p, true) 518 | end 519 | end 520 | 521 | if ActiveRecord::VERSION::MAJOR == 4 && ActiveRecord::VERSION::MINOR == 0 522 | # This class should be deleted when we remove activerecord-deprecated_finders as a 523 | # dependency. 524 | class MassAssignmentSecurityDeprecatedFindersTest < ActiveSupport::TestCase 525 | include MassAssignmentTestHelpers 526 | 527 | def setup 528 | super 529 | @deprecation_behavior = ActiveSupport::Deprecation.behavior 530 | ActiveSupport::Deprecation.behavior = :silence 531 | end 532 | 533 | def teardown 534 | super 535 | ActiveSupport::Deprecation.behavior = @deprecation_behavior 536 | end 537 | 538 | def test_find_or_initialize_by_with_attr_accessible_attributes 539 | p = TightPerson.find_or_initialize_by_first_name('Josh', attributes_hash) 540 | 541 | assert_default_attributes(p) 542 | end 543 | 544 | def test_find_or_initialize_by_with_admin_role_with_attr_accessible_attributes 545 | p = TightPerson.find_or_initialize_by_first_name('Josh', attributes_hash, :as => :admin) 546 | 547 | assert_admin_attributes(p) 548 | end 549 | 550 | def test_find_or_initialize_by_with_attr_protected_attributes 551 | p = LoosePerson.find_or_initialize_by_first_name('Josh', attributes_hash) 552 | 553 | assert_default_attributes(p) 554 | end 555 | 556 | def test_find_or_initialize_by_with_admin_role_with_attr_protected_attributes 557 | p = LoosePerson.find_or_initialize_by_first_name('Josh', attributes_hash, :as => :admin) 558 | 559 | assert_admin_attributes(p) 560 | end 561 | 562 | def test_find_or_create_by_with_attr_accessible_attributes 563 | p = TightPerson.find_or_create_by_first_name('Josh', attributes_hash) 564 | 565 | assert_default_attributes(p, true) 566 | end 567 | 568 | def test_find_or_create_by_with_admin_role_with_attr_accessible_attributes 569 | p = TightPerson.find_or_create_by_first_name('Josh', attributes_hash, :as => :admin) 570 | 571 | assert_admin_attributes(p, true) 572 | end 573 | 574 | def test_find_or_create_by_with_attr_protected_attributes 575 | p = LoosePerson.find_or_create_by_first_name('Josh', attributes_hash) 576 | 577 | assert_default_attributes(p, true) 578 | end 579 | 580 | def test_find_or_create_by_with_admin_role_with_attr_protected_attributes 581 | p = LoosePerson.find_or_create_by_first_name('Josh', attributes_hash, :as => :admin) 582 | 583 | assert_admin_attributes(p, true) 584 | end 585 | end 586 | end 587 | 588 | class MassAssignmentSecurityHasOneRelationsTest < ActiveSupport::TestCase 589 | include MassAssignmentTestHelpers 590 | include MassAssignmentRelationTestHelpers 591 | 592 | # build 593 | 594 | def test_has_one_build_with_attr_protected_attributes 595 | best_friend = @person.build_best_friend(attributes_hash) 596 | assert_default_attributes(best_friend) 597 | end 598 | 599 | def test_has_one_build_with_attr_accessible_attributes 600 | best_friend = @person.build_best_friend(attributes_hash) 601 | assert_default_attributes(best_friend) 602 | end 603 | 604 | def test_has_one_build_with_admin_role_with_attr_protected_attributes 605 | best_friend = @person.build_best_friend(attributes_hash, :as => :admin) 606 | assert_admin_attributes(best_friend) 607 | end 608 | 609 | def test_has_one_build_with_admin_role_with_attr_accessible_attributes 610 | best_friend = @person.build_best_friend(attributes_hash, :as => :admin) 611 | assert_admin_attributes(best_friend) 612 | end 613 | 614 | def test_has_one_build_without_protection 615 | best_friend = @person.build_best_friend(attributes_hash, :without_protection => true) 616 | assert_all_attributes(best_friend) 617 | end 618 | 619 | def test_has_one_build_with_strict_sanitizer 620 | with_strict_sanitizer do 621 | best_friend = @person.build_best_friend(attributes_hash.except(:id, :comments)) 622 | assert_equal @person.id, best_friend.best_friend_id 623 | end 624 | end 625 | 626 | # create 627 | 628 | def test_has_one_create_with_attr_protected_attributes 629 | best_friend = @person.create_best_friend(attributes_hash) 630 | assert_default_attributes(best_friend, true) 631 | end 632 | 633 | def test_has_one_create_with_attr_accessible_attributes 634 | best_friend = @person.create_best_friend(attributes_hash) 635 | assert_default_attributes(best_friend, true) 636 | end 637 | 638 | def test_has_one_create_with_admin_role_with_attr_protected_attributes 639 | best_friend = @person.create_best_friend(attributes_hash, :as => :admin) 640 | assert_admin_attributes(best_friend, true) 641 | end 642 | 643 | def test_has_one_create_with_admin_role_with_attr_accessible_attributes 644 | best_friend = @person.create_best_friend(attributes_hash, :as => :admin) 645 | assert_admin_attributes(best_friend, true) 646 | end 647 | 648 | def test_has_one_create_without_protection 649 | best_friend = @person.create_best_friend(attributes_hash, :without_protection => true) 650 | assert_all_attributes(best_friend) 651 | end 652 | 653 | def test_has_one_create_with_strict_sanitizer 654 | with_strict_sanitizer do 655 | best_friend = @person.create_best_friend(attributes_hash.except(:id, :comments)) 656 | assert_equal @person.id, best_friend.best_friend_id 657 | end 658 | end 659 | 660 | # create! 661 | 662 | def test_has_one_create_with_bang_with_attr_protected_attributes 663 | best_friend = @person.create_best_friend!(attributes_hash) 664 | assert_default_attributes(best_friend, true) 665 | end 666 | 667 | def test_has_one_create_with_bang_with_attr_accessible_attributes 668 | best_friend = @person.create_best_friend!(attributes_hash) 669 | assert_default_attributes(best_friend, true) 670 | end 671 | 672 | def test_has_one_create_with_bang_with_admin_role_with_attr_protected_attributes 673 | best_friend = @person.create_best_friend!(attributes_hash, :as => :admin) 674 | assert_admin_attributes(best_friend, true) 675 | end 676 | 677 | def test_has_one_create_with_bang_with_admin_role_with_attr_accessible_attributes 678 | best_friend = @person.create_best_friend!(attributes_hash, :as => :admin) 679 | assert_admin_attributes(best_friend, true) 680 | end 681 | 682 | def test_has_one_create_with_bang_without_protection 683 | best_friend = @person.create_best_friend!(attributes_hash, :without_protection => true) 684 | assert_all_attributes(best_friend) 685 | end 686 | 687 | def test_has_one_create_with_bang_with_strict_sanitizer 688 | with_strict_sanitizer do 689 | best_friend = @person.create_best_friend!(attributes_hash.except(:id, :comments)) 690 | assert_equal @person.id, best_friend.best_friend_id 691 | end 692 | end 693 | 694 | end 695 | 696 | 697 | class MassAssignmentSecurityBelongsToRelationsTest < ActiveSupport::TestCase 698 | include MassAssignmentTestHelpers 699 | include MassAssignmentRelationTestHelpers 700 | 701 | # build 702 | 703 | def test_belongs_to_build_with_attr_protected_attributes 704 | best_friend = @person.build_best_friend_of(attributes_hash) 705 | assert_default_attributes(best_friend) 706 | end 707 | 708 | def test_belongs_to_build_with_attr_accessible_attributes 709 | best_friend = @person.build_best_friend_of(attributes_hash) 710 | assert_default_attributes(best_friend) 711 | end 712 | 713 | def test_belongs_to_build_with_admin_role_with_attr_protected_attributes 714 | best_friend = @person.build_best_friend_of(attributes_hash, :as => :admin) 715 | assert_admin_attributes(best_friend) 716 | end 717 | 718 | def test_belongs_to_build_with_admin_role_with_attr_accessible_attributes 719 | best_friend = @person.build_best_friend_of(attributes_hash, :as => :admin) 720 | assert_admin_attributes(best_friend) 721 | end 722 | 723 | def test_belongs_to_build_without_protection 724 | best_friend = @person.build_best_friend_of(attributes_hash, :without_protection => true) 725 | assert_all_attributes(best_friend) 726 | end 727 | 728 | # create 729 | 730 | def test_belongs_to_create_with_attr_protected_attributes 731 | best_friend = @person.create_best_friend_of(attributes_hash) 732 | assert_default_attributes(best_friend, true) 733 | end 734 | 735 | def test_belongs_to_create_with_attr_accessible_attributes 736 | best_friend = @person.create_best_friend_of(attributes_hash) 737 | assert_default_attributes(best_friend, true) 738 | end 739 | 740 | def test_belongs_to_create_with_admin_role_with_attr_protected_attributes 741 | best_friend = @person.create_best_friend_of(attributes_hash, :as => :admin) 742 | assert_admin_attributes(best_friend, true) 743 | end 744 | 745 | def test_belongs_to_create_with_admin_role_with_attr_accessible_attributes 746 | best_friend = @person.create_best_friend_of(attributes_hash, :as => :admin) 747 | assert_admin_attributes(best_friend, true) 748 | end 749 | 750 | def test_belongs_to_create_without_protection 751 | best_friend = @person.create_best_friend_of(attributes_hash, :without_protection => true) 752 | assert_all_attributes(best_friend) 753 | end 754 | 755 | def test_belongs_to_create_with_strict_sanitizer 756 | with_strict_sanitizer do 757 | best_friend = @person.create_best_friend_of(attributes_hash.except(:id, :comments)) 758 | assert_equal best_friend.id, @person.best_friend_of_id 759 | end 760 | end 761 | 762 | # create! 763 | 764 | def test_belongs_to_create_with_bang_with_attr_protected_attributes 765 | best_friend = @person.create_best_friend!(attributes_hash) 766 | assert_default_attributes(best_friend, true) 767 | end 768 | 769 | def test_belongs_to_create_with_bang_with_attr_accessible_attributes 770 | best_friend = @person.create_best_friend!(attributes_hash) 771 | assert_default_attributes(best_friend, true) 772 | end 773 | 774 | def test_belongs_to_create_with_bang_with_admin_role_with_attr_protected_attributes 775 | best_friend = @person.create_best_friend!(attributes_hash, :as => :admin) 776 | assert_admin_attributes(best_friend, true) 777 | end 778 | 779 | def test_belongs_to_create_with_bang_with_admin_role_with_attr_accessible_attributes 780 | best_friend = @person.create_best_friend!(attributes_hash, :as => :admin) 781 | assert_admin_attributes(best_friend, true) 782 | end 783 | 784 | def test_belongs_to_create_with_bang_without_protection 785 | best_friend = @person.create_best_friend!(attributes_hash, :without_protection => true) 786 | assert_all_attributes(best_friend) 787 | end 788 | 789 | def test_belongs_to_create_with_bang_with_strict_sanitizer 790 | with_strict_sanitizer do 791 | best_friend = @person.create_best_friend_of!(attributes_hash.except(:id, :comments)) 792 | assert_equal best_friend.id, @person.best_friend_of_id 793 | end 794 | end 795 | 796 | end 797 | 798 | 799 | class MassAssignmentSecurityHasManyRelationsTest < ActiveSupport::TestCase 800 | include MassAssignmentTestHelpers 801 | include MassAssignmentRelationTestHelpers 802 | 803 | # build 804 | 805 | def test_has_many_build_with_attr_protected_attributes 806 | best_friend = @person.best_friends.build(attributes_hash) 807 | assert_default_attributes(best_friend) 808 | end 809 | 810 | def test_has_many_build_with_attr_accessible_attributes 811 | best_friend = @person.best_friends.build(attributes_hash) 812 | assert_default_attributes(best_friend) 813 | end 814 | 815 | def test_has_many_build_with_admin_role_with_attr_protected_attributes 816 | best_friend = @person.best_friends.build(attributes_hash, :as => :admin) 817 | assert_admin_attributes(best_friend) 818 | end 819 | 820 | def test_has_many_build_with_admin_role_with_attr_accessible_attributes 821 | best_friend = @person.best_friends.build(attributes_hash, :as => :admin) 822 | assert_admin_attributes(best_friend) 823 | end 824 | 825 | def test_has_many_build_without_protection 826 | best_friend = @person.best_friends.build(attributes_hash, :without_protection => true) 827 | assert_all_attributes(best_friend) 828 | end 829 | 830 | def test_has_many_build_with_strict_sanitizer 831 | with_strict_sanitizer do 832 | best_friend = @person.best_friends.build(attributes_hash.except(:id, :comments)) 833 | assert_equal @person.id, best_friend.best_friend_id 834 | end 835 | end 836 | 837 | def test_has_many_through_build_with_attr_accessible_attributes 838 | group = Group.create! 839 | pirate = group.members.build(name: "Murphy") 840 | assert_equal "Murphy", pirate.name 841 | end 842 | 843 | # new 844 | 845 | def test_has_many_new_with_attr_protected_attributes 846 | best_friend = @person.best_friends.new(attributes_hash) 847 | assert_default_attributes(best_friend) 848 | end 849 | 850 | def test_has_many_new_with_attr_accessible_attributes 851 | best_friend = @person.best_friends.new(attributes_hash) 852 | assert_default_attributes(best_friend) 853 | end 854 | 855 | def test_has_many_new_with_admin_role_with_attr_protected_attributes 856 | best_friend = @person.best_friends.new(attributes_hash, :as => :admin) 857 | assert_admin_attributes(best_friend) 858 | end 859 | 860 | def test_has_many_new_with_admin_role_with_attr_accessible_attributes 861 | best_friend = @person.best_friends.new(attributes_hash, :as => :admin) 862 | assert_admin_attributes(best_friend) 863 | end 864 | 865 | def test_has_many_new_without_protection 866 | best_friend = @person.best_friends.new(attributes_hash, :without_protection => true) 867 | assert_all_attributes(best_friend) 868 | end 869 | 870 | def test_has_many_new_with_strict_sanitizer 871 | with_strict_sanitizer do 872 | best_friend = @person.best_friends.new(attributes_hash.except(:id, :comments)) 873 | assert_equal @person.id, best_friend.best_friend_id 874 | end 875 | end 876 | 877 | # create 878 | 879 | def test_has_many_create_with_attr_protected_attributes 880 | best_friend = @person.best_friends.create(attributes_hash) 881 | assert_default_attributes(best_friend, true) 882 | end 883 | 884 | def test_has_many_create_with_attr_accessible_attributes 885 | best_friend = @person.best_friends.create(attributes_hash) 886 | assert_default_attributes(best_friend, true) 887 | end 888 | 889 | def test_has_many_create_with_admin_role_with_attr_protected_attributes 890 | best_friend = @person.best_friends.create(attributes_hash, :as => :admin) 891 | assert_admin_attributes(best_friend, true) 892 | end 893 | 894 | def test_has_many_create_with_admin_role_with_attr_accessible_attributes 895 | best_friend = @person.best_friends.create(attributes_hash, :as => :admin) 896 | assert_admin_attributes(best_friend, true) 897 | end 898 | 899 | def test_has_many_create_without_protection 900 | best_friend = @person.best_friends.create(attributes_hash, :without_protection => true) 901 | assert_all_attributes(best_friend) 902 | end 903 | 904 | def test_has_many_create_with_strict_sanitizer 905 | with_strict_sanitizer do 906 | best_friend = @person.best_friends.create(attributes_hash.except(:id, :comments)) 907 | assert_equal @person.id, best_friend.best_friend_id 908 | end 909 | end 910 | 911 | # create! 912 | 913 | def test_has_many_create_with_bang_with_attr_protected_attributes 914 | best_friend = @person.best_friends.create!(attributes_hash) 915 | assert_default_attributes(best_friend, true) 916 | end 917 | 918 | def test_has_many_create_with_bang_with_attr_accessible_attributes 919 | best_friend = @person.best_friends.create!(attributes_hash) 920 | assert_default_attributes(best_friend, true) 921 | end 922 | 923 | def test_has_many_create_with_bang_with_admin_role_with_attr_protected_attributes 924 | best_friend = @person.best_friends.create!(attributes_hash, :as => :admin) 925 | assert_admin_attributes(best_friend, true) 926 | end 927 | 928 | def test_has_many_create_with_bang_with_admin_role_with_attr_accessible_attributes 929 | best_friend = @person.best_friends.create!(attributes_hash, :as => :admin) 930 | assert_admin_attributes(best_friend, true) 931 | end 932 | 933 | def test_has_many_create_with_bang_without_protection 934 | best_friend = @person.best_friends.create!(attributes_hash, :without_protection => true) 935 | assert_all_attributes(best_friend) 936 | end 937 | 938 | def test_has_many_create_with_bang_with_strict_sanitizer 939 | with_strict_sanitizer do 940 | best_friend = @person.best_friends.create!(attributes_hash.except(:id, :comments)) 941 | assert_equal @person.id, best_friend.best_friend_id 942 | end 943 | end 944 | 945 | # concat 946 | 947 | def test_concat_has_many_through_association_member 948 | group = Group.create! 949 | pirate = Pirate.create! 950 | group.members << pirate 951 | assert_equal pirate.memberships.first, group.memberships.first 952 | end 953 | 954 | def test_concat_has_many_through_polymorphic_association 955 | team = Team.create! 956 | vampire = Vampire.create! 957 | wolf = Wolf.create! 958 | 959 | team.vampire_battles << vampire 960 | wolf.teams << team 961 | assert_equal team.wolf_battles.first, wolf 962 | end 963 | end 964 | 965 | 966 | class MassAssignmentSecurityNestedAttributesTest < ActiveSupport::TestCase 967 | include MassAssignmentTestHelpers 968 | 969 | def nested_attributes_hash(association, collection = false, except = [:id]) 970 | if collection 971 | { :first_name => 'David' }.merge(:"#{association}_attributes" => [attributes_hash.except(*except)]) 972 | else 973 | { :first_name => 'David' }.merge(:"#{association}_attributes" => attributes_hash.except(*except)) 974 | end 975 | end 976 | 977 | # build 978 | 979 | def test_has_one_new_with_attr_protected_attributes 980 | person = LoosePerson.new(nested_attributes_hash(:best_friend)) 981 | assert_default_attributes(person.best_friend) 982 | end 983 | 984 | def test_has_one_new_with_attr_accessible_attributes 985 | person = TightPerson.new(nested_attributes_hash(:best_friend)) 986 | assert_default_attributes(person.best_friend) 987 | end 988 | 989 | def test_has_one_new_with_admin_role_with_attr_protected_attributes 990 | person = LoosePerson.new(nested_attributes_hash(:best_friend), :as => :admin) 991 | assert_admin_attributes(person.best_friend) 992 | end 993 | 994 | def test_has_one_new_with_admin_role_with_attr_accessible_attributes 995 | person = TightPerson.new(nested_attributes_hash(:best_friend), :as => :admin) 996 | assert_admin_attributes(person.best_friend) 997 | end 998 | 999 | def test_has_one_new_without_protection 1000 | person = LoosePerson.new(nested_attributes_hash(:best_friend, false, nil), :without_protection => true) 1001 | assert_all_attributes(person.best_friend) 1002 | end 1003 | 1004 | def test_belongs_to_new_with_attr_protected_attributes 1005 | person = LoosePerson.new(nested_attributes_hash(:best_friend_of)) 1006 | assert_default_attributes(person.best_friend_of) 1007 | end 1008 | 1009 | def test_belongs_to_new_with_attr_accessible_attributes 1010 | person = TightPerson.new(nested_attributes_hash(:best_friend_of)) 1011 | assert_default_attributes(person.best_friend_of) 1012 | end 1013 | 1014 | def test_belongs_to_new_with_admin_role_with_attr_protected_attributes 1015 | person = LoosePerson.new(nested_attributes_hash(:best_friend_of), :as => :admin) 1016 | assert_admin_attributes(person.best_friend_of) 1017 | end 1018 | 1019 | def test_belongs_to_new_with_admin_role_with_attr_accessible_attributes 1020 | person = TightPerson.new(nested_attributes_hash(:best_friend_of), :as => :admin) 1021 | assert_admin_attributes(person.best_friend_of) 1022 | end 1023 | 1024 | def test_belongs_to_new_without_protection 1025 | person = LoosePerson.new(nested_attributes_hash(:best_friend_of, false, nil), :without_protection => true) 1026 | assert_all_attributes(person.best_friend_of) 1027 | end 1028 | 1029 | def test_has_many_new_with_attr_protected_attributes 1030 | person = LoosePerson.new(nested_attributes_hash(:best_friends, true)) 1031 | assert_default_attributes(person.best_friends.first) 1032 | end 1033 | 1034 | def test_has_many_new_with_attr_accessible_attributes 1035 | person = TightPerson.new(nested_attributes_hash(:best_friends, true)) 1036 | assert_default_attributes(person.best_friends.first) 1037 | end 1038 | 1039 | def test_has_many_new_with_admin_role_with_attr_protected_attributes 1040 | person = LoosePerson.new(nested_attributes_hash(:best_friends, true), :as => :admin) 1041 | assert_admin_attributes(person.best_friends.first) 1042 | end 1043 | 1044 | def test_has_many_new_with_admin_role_with_attr_accessible_attributes 1045 | person = TightPerson.new(nested_attributes_hash(:best_friends, true), :as => :admin) 1046 | assert_admin_attributes(person.best_friends.first) 1047 | end 1048 | 1049 | def test_has_many_new_without_protection 1050 | person = LoosePerson.new(nested_attributes_hash(:best_friends, true, nil), :without_protection => true) 1051 | assert_all_attributes(person.best_friends.first) 1052 | end 1053 | 1054 | # create 1055 | 1056 | def test_has_one_create_with_attr_protected_attributes 1057 | person = LoosePerson.create(nested_attributes_hash(:best_friend)) 1058 | assert_default_attributes(person.best_friend, true) 1059 | end 1060 | 1061 | def test_has_one_create_with_attr_accessible_attributes 1062 | person = TightPerson.create(nested_attributes_hash(:best_friend)) 1063 | assert_default_attributes(person.best_friend, true) 1064 | end 1065 | 1066 | def test_has_one_create_with_admin_role_with_attr_protected_attributes 1067 | person = LoosePerson.create(nested_attributes_hash(:best_friend), :as => :admin) 1068 | assert_admin_attributes(person.best_friend, true) 1069 | end 1070 | 1071 | def test_has_one_create_with_admin_role_with_attr_accessible_attributes 1072 | person = TightPerson.create(nested_attributes_hash(:best_friend), :as => :admin) 1073 | assert_admin_attributes(person.best_friend, true) 1074 | end 1075 | 1076 | def test_has_one_create_without_protection 1077 | person = LoosePerson.create(nested_attributes_hash(:best_friend, false, nil), :without_protection => true) 1078 | assert_all_attributes(person.best_friend) 1079 | end 1080 | 1081 | def test_belongs_to_create_with_attr_protected_attributes 1082 | person = LoosePerson.create(nested_attributes_hash(:best_friend_of)) 1083 | assert_default_attributes(person.best_friend_of, true) 1084 | end 1085 | 1086 | def test_belongs_to_create_with_attr_accessible_attributes 1087 | person = TightPerson.create(nested_attributes_hash(:best_friend_of)) 1088 | assert_default_attributes(person.best_friend_of, true) 1089 | end 1090 | 1091 | def test_belongs_to_create_with_admin_role_with_attr_protected_attributes 1092 | person = LoosePerson.create(nested_attributes_hash(:best_friend_of), :as => :admin) 1093 | assert_admin_attributes(person.best_friend_of, true) 1094 | end 1095 | 1096 | def test_belongs_to_create_with_admin_role_with_attr_accessible_attributes 1097 | person = TightPerson.create(nested_attributes_hash(:best_friend_of), :as => :admin) 1098 | assert_admin_attributes(person.best_friend_of, true) 1099 | end 1100 | 1101 | def test_belongs_to_create_without_protection 1102 | person = LoosePerson.create(nested_attributes_hash(:best_friend_of, false, nil), :without_protection => true) 1103 | assert_all_attributes(person.best_friend_of) 1104 | end 1105 | 1106 | def test_has_many_create_with_attr_protected_attributes 1107 | person = LoosePerson.create(nested_attributes_hash(:best_friends, true)) 1108 | assert_default_attributes(person.best_friends.first, true) 1109 | end 1110 | 1111 | def test_has_many_create_with_attr_accessible_attributes 1112 | person = TightPerson.create(nested_attributes_hash(:best_friends, true)) 1113 | assert_default_attributes(person.best_friends.first, true) 1114 | end 1115 | 1116 | def test_has_many_create_with_admin_role_with_attr_protected_attributes 1117 | person = LoosePerson.create(nested_attributes_hash(:best_friends, true), :as => :admin) 1118 | assert_admin_attributes(person.best_friends.first, true) 1119 | end 1120 | 1121 | def test_has_many_create_with_admin_role_with_attr_accessible_attributes 1122 | person = TightPerson.create(nested_attributes_hash(:best_friends, true), :as => :admin) 1123 | assert_admin_attributes(person.best_friends.first, true) 1124 | end 1125 | 1126 | def test_has_many_create_without_protection 1127 | person = LoosePerson.create(nested_attributes_hash(:best_friends, true, nil), :without_protection => true) 1128 | assert_all_attributes(person.best_friends.first) 1129 | end 1130 | 1131 | # create! 1132 | 1133 | def test_has_one_create_with_bang_with_attr_protected_attributes 1134 | person = LoosePerson.create!(nested_attributes_hash(:best_friend)) 1135 | assert_default_attributes(person.best_friend, true) 1136 | end 1137 | 1138 | def test_has_one_create_with_bang_with_attr_accessible_attributes 1139 | person = TightPerson.create!(nested_attributes_hash(:best_friend)) 1140 | assert_default_attributes(person.best_friend, true) 1141 | end 1142 | 1143 | def test_has_one_create_with_bang_with_admin_role_with_attr_protected_attributes 1144 | person = LoosePerson.create!(nested_attributes_hash(:best_friend), :as => :admin) 1145 | assert_admin_attributes(person.best_friend, true) 1146 | end 1147 | 1148 | def test_has_one_create_with_bang_with_admin_role_with_attr_accessible_attributes 1149 | person = TightPerson.create!(nested_attributes_hash(:best_friend), :as => :admin) 1150 | assert_admin_attributes(person.best_friend, true) 1151 | end 1152 | 1153 | def test_has_one_create_with_bang_without_protection 1154 | person = LoosePerson.create!(nested_attributes_hash(:best_friend, false, nil), :without_protection => true) 1155 | assert_all_attributes(person.best_friend) 1156 | end 1157 | 1158 | def test_belongs_to_create_with_bang_with_attr_protected_attributes 1159 | person = LoosePerson.create!(nested_attributes_hash(:best_friend_of)) 1160 | assert_default_attributes(person.best_friend_of, true) 1161 | end 1162 | 1163 | def test_belongs_to_create_with_bang_with_attr_accessible_attributes 1164 | person = TightPerson.create!(nested_attributes_hash(:best_friend_of)) 1165 | assert_default_attributes(person.best_friend_of, true) 1166 | end 1167 | 1168 | def test_belongs_to_create_with_bang_with_admin_role_with_attr_protected_attributes 1169 | person = LoosePerson.create!(nested_attributes_hash(:best_friend_of), :as => :admin) 1170 | assert_admin_attributes(person.best_friend_of, true) 1171 | end 1172 | 1173 | def test_belongs_to_create_with_bang_with_admin_role_with_attr_accessible_attributes 1174 | person = TightPerson.create!(nested_attributes_hash(:best_friend_of), :as => :admin) 1175 | assert_admin_attributes(person.best_friend_of, true) 1176 | end 1177 | 1178 | def test_belongs_to_create_with_bang_without_protection 1179 | person = LoosePerson.create!(nested_attributes_hash(:best_friend_of, false, nil), :without_protection => true) 1180 | assert_all_attributes(person.best_friend_of) 1181 | end 1182 | 1183 | def test_has_many_create_with_bang_with_attr_protected_attributes 1184 | person = LoosePerson.create!(nested_attributes_hash(:best_friends, true)) 1185 | assert_default_attributes(person.best_friends.first, true) 1186 | end 1187 | 1188 | def test_has_many_create_with_bang_with_attr_accessible_attributes 1189 | person = TightPerson.create!(nested_attributes_hash(:best_friends, true)) 1190 | assert_default_attributes(person.best_friends.first, true) 1191 | end 1192 | 1193 | def test_has_many_create_with_bang_with_admin_role_with_attr_protected_attributes 1194 | person = LoosePerson.create!(nested_attributes_hash(:best_friends, true), :as => :admin) 1195 | assert_admin_attributes(person.best_friends.first, true) 1196 | end 1197 | 1198 | def test_has_many_create_with_bang_with_admin_role_with_attr_accessible_attributes 1199 | person = TightPerson.create!(nested_attributes_hash(:best_friends, true), :as => :admin) 1200 | assert_admin_attributes(person.best_friends.first, true) 1201 | end 1202 | 1203 | def test_has_many_create_with_bang_without_protection 1204 | person = LoosePerson.create!(nested_attributes_hash(:best_friends, true, nil), :without_protection => true) 1205 | assert_all_attributes(person.best_friends.first) 1206 | end 1207 | 1208 | def test_mass_assignment_options_are_reset_after_exception 1209 | person = NestedPerson.create!({ :first_name => 'David', :gender => 'm' }, :as => :admin) 1210 | person.create_best_friend!({ :first_name => 'Jeremy', :gender => 'm' }, :as => :admin) 1211 | 1212 | attributes = { :best_friend_attributes => { :comments => 'rides a sweet bike' } } 1213 | assert_raises(RuntimeError) { person.assign_attributes(attributes, :as => :admin) } 1214 | assert_equal 'm', person.best_friend.gender 1215 | 1216 | person.best_friend_attributes = { :gender => 'f' } 1217 | assert_equal 'm', person.best_friend.gender 1218 | end 1219 | 1220 | def test_mass_assignment_options_are_nested_correctly 1221 | person = NestedPerson.create!({ :first_name => 'David', :gender => 'm' }, :as => :admin) 1222 | person.create_best_friend!({ :first_name => 'Jeremy', :gender => 'm' }, :as => :admin) 1223 | 1224 | attributes = { :best_friend_first_name => 'Josh', :best_friend_attributes => { :gender => 'f' } } 1225 | person.assign_attributes(attributes, :as => :admin) 1226 | assert_equal 'Josh', person.best_friend.first_name 1227 | assert_equal 'f', person.best_friend.gender 1228 | end 1229 | 1230 | def test_accepts_nested_attributes_for_and_protected_attributes_on_both_sides 1231 | team = Team.create 1232 | 1233 | team.update_attributes({ :nested_battles_attributes => { 1234 | '0' => { :team_id => Team.create.id }, 1235 | '1' => { :team_id => Team.create.id } } 1236 | }) 1237 | 1238 | assert_equal 2, Team.find(team.id).nested_battles.count 1239 | end 1240 | 1241 | def test_accepts_nested_attributes_for_raises_when_limit_is_reached 1242 | team = Team.create 1243 | team.nested_attributes_options[:nested_battles].merge!(limit: 2) 1244 | 1245 | assert_raises(ActiveRecord::NestedAttributes::TooManyRecords) do 1246 | team.nested_battles_attributes = { 1247 | '0' => { :team_id => Team.create.id }, 1248 | '1' => { :team_id => Team.create.id }, 1249 | '2' => { :team_id => Team.create.id } 1250 | } 1251 | end 1252 | end 1253 | end 1254 | --------------------------------------------------------------------------------