protected_attributes 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|