├── 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 | [](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 |
--------------------------------------------------------------------------------