protected_attributes 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +17 -0
- data/.travis.yml +17 -0
- data/Gemfile +7 -0
- data/LICENSE.txt +22 -0
- data/README.md +111 -0
- data/Rakefile +11 -0
- data/lib/action_controller/accessible_params_wrapper.rb +29 -0
- data/lib/active_model/mass_assignment_security.rb +353 -0
- data/lib/active_model/mass_assignment_security/permission_set.rb +40 -0
- data/lib/active_model/mass_assignment_security/sanitizer.rb +74 -0
- data/lib/active_record/mass_assignment_security.rb +23 -0
- data/lib/active_record/mass_assignment_security/associations.rb +116 -0
- data/lib/active_record/mass_assignment_security/attribute_assignment.rb +88 -0
- data/lib/active_record/mass_assignment_security/core.rb +27 -0
- data/lib/active_record/mass_assignment_security/inheritance.rb +18 -0
- data/lib/active_record/mass_assignment_security/nested_attributes.rb +148 -0
- data/lib/active_record/mass_assignment_security/persistence.rb +81 -0
- data/lib/active_record/mass_assignment_security/reflection.rb +9 -0
- data/lib/active_record/mass_assignment_security/relation.rb +47 -0
- data/lib/active_record/mass_assignment_security/validations.rb +24 -0
- data/lib/protected_attributes.rb +14 -0
- data/lib/protected_attributes/railtie.rb +18 -0
- data/lib/protected_attributes/version.rb +3 -0
- data/protected_attributes.gemspec +26 -0
- data/test/abstract_unit.rb +156 -0
- data/test/accessible_params_wrapper_test.rb +76 -0
- data/test/ar_helper.rb +67 -0
- data/test/attribute_sanitization_test.rb +929 -0
- data/test/mass_assignment_security/black_list_test.rb +20 -0
- data/test/mass_assignment_security/permission_set_test.rb +36 -0
- data/test/mass_assignment_security/sanitizer_test.rb +50 -0
- data/test/mass_assignment_security/white_list_test.rb +19 -0
- data/test/mass_assignment_security_test.rb +118 -0
- data/test/models/company.rb +105 -0
- data/test/models/keyboard.rb +3 -0
- data/test/models/mass_assignment_specific.rb +76 -0
- data/test/models/person.rb +82 -0
- data/test/models/subscriber.rb +5 -0
- data/test/models/task.rb +5 -0
- data/test/test_helper.rb +3 -0
- metadata +199 -0
@@ -0,0 +1,81 @@
|
|
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(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
|
+
|
69
|
+
# Updates its receiver just like +update_attributes+ but calls <tt>save!</tt> instead
|
70
|
+
# of +save+, so an exception is raised if the record is invalid.
|
71
|
+
def update_attributes!(attributes, options = {})
|
72
|
+
# The following transaction covers any possible database side-effects of the
|
73
|
+
# attributes assignment. For example, setting the IDs of a child collection.
|
74
|
+
with_transaction_returning_status do
|
75
|
+
assign_attributes(attributes, options)
|
76
|
+
save!
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module MassAssignmentSecurity
|
3
|
+
module Relation
|
4
|
+
# Tries to load the first record; if it fails, then <tt>create</tt> is called with the same arguments as this method.
|
5
|
+
#
|
6
|
+
# Expects arguments in the same format as +Base.create+.
|
7
|
+
#
|
8
|
+
# ==== Examples
|
9
|
+
# # Find the first user named Penélope or create a new one.
|
10
|
+
# User.where(:first_name => 'Penélope').first_or_create
|
11
|
+
# # => <User id: 1, first_name: 'Penélope', last_name: nil>
|
12
|
+
#
|
13
|
+
# # Find the first user named Penélope or create a new one.
|
14
|
+
# # We already have one so the existing record will be returned.
|
15
|
+
# User.where(:first_name => 'Penélope').first_or_create
|
16
|
+
# # => <User id: 1, first_name: 'Penélope', last_name: nil>
|
17
|
+
#
|
18
|
+
# # Find the first user named Scarlett or create a new one with a particular last name.
|
19
|
+
# User.where(:first_name => 'Scarlett').first_or_create(:last_name => 'Johansson')
|
20
|
+
# # => <User id: 2, first_name: 'Scarlett', last_name: 'Johansson'>
|
21
|
+
#
|
22
|
+
# # Find the first user named Scarlett or create a new one with a different last name.
|
23
|
+
# # We already have one so the existing record will be returned.
|
24
|
+
# User.where(:first_name => 'Scarlett').first_or_create do |user|
|
25
|
+
# user.last_name = "O'Hara"
|
26
|
+
# end
|
27
|
+
# # => <User id: 2, first_name: 'Scarlett', last_name: 'Johansson'>
|
28
|
+
def first_or_create(attributes = nil, options = {}, &block)
|
29
|
+
first || create(attributes, options, &block)
|
30
|
+
end
|
31
|
+
|
32
|
+
# Like <tt>first_or_create</tt> but calls <tt>create!</tt> so an exception is raised if the created record is invalid.
|
33
|
+
#
|
34
|
+
# Expects arguments in the same format as <tt>Base.create!</tt>.
|
35
|
+
def first_or_create!(attributes = nil, options = {}, &block)
|
36
|
+
first || create!(attributes, options, &block)
|
37
|
+
end
|
38
|
+
|
39
|
+
# Like <tt>first_or_create</tt> but calls <tt>new</tt> instead of <tt>create</tt>.
|
40
|
+
#
|
41
|
+
# Expects arguments in the same format as <tt>Base.new</tt>.
|
42
|
+
def first_or_initialize(attributes = nil, &block)
|
43
|
+
first || new(attributes, options, &block)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,24 @@
|
|
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 <tt>save!</tt> 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
|
@@ -0,0 +1,14 @@
|
|
1
|
+
require "active_model/mass_assignment_security"
|
2
|
+
require "protected_attributes/railtie" if defined? Rails
|
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
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'rails/railtie'
|
2
|
+
|
3
|
+
module ProtectedAttributes
|
4
|
+
class Railtie < ::Rails::Railtie
|
5
|
+
config.before_configuration do |app|
|
6
|
+
config.action_controller.permit_all_parameters = true
|
7
|
+
config.active_record.whitelist_attributes = true if config.respond_to?(:active_record)
|
8
|
+
end
|
9
|
+
|
10
|
+
initializer "protected_attributes.active_record", :before => "active_record.set_configs" do |app|
|
11
|
+
ActiveSupport.on_load :active_record do
|
12
|
+
if app.config.respond_to?(:active_record) && app.config.active_record.delete(:whitelist_attributes)
|
13
|
+
attr_accessible(nil)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,26 @@
|
|
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 in AR models}
|
12
|
+
gem.summary = %q{Protect attributes from mass assignment in AR models}
|
13
|
+
gem.homepage = "https://github.com/rails/protected_attributes"
|
14
|
+
|
15
|
+
gem.files = `git ls-files`.split($/)
|
16
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
17
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
18
|
+
gem.require_paths = ["lib"]
|
19
|
+
|
20
|
+
gem.add_dependency "activemodel", ">= 4.0.0.beta", "< 5.0"
|
21
|
+
|
22
|
+
gem.add_development_dependency "activerecord", ">= 4.0.0.beta", "< 5.0"
|
23
|
+
gem.add_development_dependency "actionpack", ">= 4.0.0.beta", "< 5.0"
|
24
|
+
gem.add_development_dependency "sqlite3"
|
25
|
+
gem.add_development_dependency "mocha"
|
26
|
+
end
|
@@ -0,0 +1,156 @@
|
|
1
|
+
require 'action_dispatch'
|
2
|
+
require 'action_controller'
|
3
|
+
require 'active_support/core_ext/class/attribute_accessors'
|
4
|
+
require 'active_support/dependencies'
|
5
|
+
|
6
|
+
module SetupOnce
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
|
9
|
+
included do
|
10
|
+
cattr_accessor :setup_once_block
|
11
|
+
self.setup_once_block = nil
|
12
|
+
|
13
|
+
setup :run_setup_once
|
14
|
+
end
|
15
|
+
|
16
|
+
module ClassMethods
|
17
|
+
def setup_once(&block)
|
18
|
+
self.setup_once_block = block
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
def run_setup_once
|
24
|
+
if self.setup_once_block
|
25
|
+
self.setup_once_block.call
|
26
|
+
self.setup_once_block = nil
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
SharedTestRoutes = ActionDispatch::Routing::RouteSet.new
|
32
|
+
|
33
|
+
module ActiveSupport
|
34
|
+
class TestCase
|
35
|
+
include SetupOnce
|
36
|
+
# Hold off drawing routes until all the possible controller classes
|
37
|
+
# have been loaded.
|
38
|
+
setup_once do
|
39
|
+
SharedTestRoutes.draw do
|
40
|
+
get ':controller(/:action)'
|
41
|
+
end
|
42
|
+
|
43
|
+
ActionDispatch::IntegrationTest.app.routes.draw do
|
44
|
+
get ':controller(/:action)'
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
class RoutedRackApp
|
51
|
+
attr_reader :routes
|
52
|
+
|
53
|
+
def initialize(routes, &blk)
|
54
|
+
@routes = routes
|
55
|
+
@stack = ActionDispatch::MiddlewareStack.new(&blk).build(@routes)
|
56
|
+
end
|
57
|
+
|
58
|
+
def call(env)
|
59
|
+
@stack.call(env)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
class ActionDispatch::IntegrationTest < ActiveSupport::TestCase
|
64
|
+
setup do
|
65
|
+
@routes = SharedTestRoutes
|
66
|
+
end
|
67
|
+
|
68
|
+
def self.build_app(routes = nil)
|
69
|
+
RoutedRackApp.new(routes || ActionDispatch::Routing::RouteSet.new) do |middleware|
|
70
|
+
middleware.use "ActionDispatch::DebugExceptions"
|
71
|
+
middleware.use "ActionDispatch::Callbacks"
|
72
|
+
middleware.use "ActionDispatch::ParamsParser"
|
73
|
+
middleware.use "ActionDispatch::Cookies"
|
74
|
+
middleware.use "ActionDispatch::Flash"
|
75
|
+
middleware.use "Rack::Head"
|
76
|
+
yield(middleware) if block_given?
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
self.app = build_app
|
81
|
+
|
82
|
+
# Stub Rails dispatcher so it does not get controller references and
|
83
|
+
# simply return the controller#action as Rack::Body.
|
84
|
+
class StubDispatcher < ::ActionDispatch::Routing::RouteSet::Dispatcher
|
85
|
+
protected
|
86
|
+
def controller_reference(controller_param)
|
87
|
+
controller_param
|
88
|
+
end
|
89
|
+
|
90
|
+
def dispatch(controller, action, env)
|
91
|
+
[200, {'Content-Type' => 'text/html'}, ["#{controller}##{action}"]]
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def self.stub_controllers
|
96
|
+
old_dispatcher = ActionDispatch::Routing::RouteSet::Dispatcher
|
97
|
+
ActionDispatch::Routing::RouteSet.module_eval { remove_const :Dispatcher }
|
98
|
+
ActionDispatch::Routing::RouteSet.module_eval { const_set :Dispatcher, StubDispatcher }
|
99
|
+
yield ActionDispatch::Routing::RouteSet.new
|
100
|
+
ensure
|
101
|
+
ActionDispatch::Routing::RouteSet.module_eval { remove_const :Dispatcher }
|
102
|
+
ActionDispatch::Routing::RouteSet.module_eval { const_set :Dispatcher, old_dispatcher }
|
103
|
+
end
|
104
|
+
|
105
|
+
def with_routing(&block)
|
106
|
+
temporary_routes = ActionDispatch::Routing::RouteSet.new
|
107
|
+
old_app, self.class.app = self.class.app, self.class.build_app(temporary_routes)
|
108
|
+
old_routes = SharedTestRoutes
|
109
|
+
silence_warnings { Object.const_set(:SharedTestRoutes, temporary_routes) }
|
110
|
+
|
111
|
+
yield temporary_routes
|
112
|
+
ensure
|
113
|
+
self.class.app = old_app
|
114
|
+
silence_warnings { Object.const_set(:SharedTestRoutes, old_routes) }
|
115
|
+
end
|
116
|
+
|
117
|
+
def with_autoload_path(path)
|
118
|
+
path = File.join(File.dirname(__FILE__), "fixtures", path)
|
119
|
+
if ActiveSupport::Dependencies.autoload_paths.include?(path)
|
120
|
+
yield
|
121
|
+
else
|
122
|
+
begin
|
123
|
+
ActiveSupport::Dependencies.autoload_paths << path
|
124
|
+
yield
|
125
|
+
ensure
|
126
|
+
ActiveSupport::Dependencies.autoload_paths.reject! {|p| p == path}
|
127
|
+
ActiveSupport::Dependencies.clear
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
module ActionController
|
134
|
+
class Base
|
135
|
+
include ActionController::Testing
|
136
|
+
# This stub emulates the Railtie including the URL helpers from a Rails application
|
137
|
+
include SharedTestRoutes.url_helpers
|
138
|
+
include SharedTestRoutes.mounted_helpers
|
139
|
+
|
140
|
+
#self.view_paths = FIXTURE_LOAD_PATH
|
141
|
+
|
142
|
+
def self.test_routes(&block)
|
143
|
+
routes = ActionDispatch::Routing::RouteSet.new
|
144
|
+
routes.draw(&block)
|
145
|
+
include routes.url_helpers
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
class TestCase
|
150
|
+
include ActionDispatch::TestProcess
|
151
|
+
|
152
|
+
setup do
|
153
|
+
@routes = SharedTestRoutes
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
@@ -0,0 +1,76 @@
|
|
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
|