protected_attributes 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. data/.gitignore +17 -0
  2. data/.travis.yml +17 -0
  3. data/Gemfile +7 -0
  4. data/LICENSE.txt +22 -0
  5. data/README.md +111 -0
  6. data/Rakefile +11 -0
  7. data/lib/action_controller/accessible_params_wrapper.rb +29 -0
  8. data/lib/active_model/mass_assignment_security.rb +353 -0
  9. data/lib/active_model/mass_assignment_security/permission_set.rb +40 -0
  10. data/lib/active_model/mass_assignment_security/sanitizer.rb +74 -0
  11. data/lib/active_record/mass_assignment_security.rb +23 -0
  12. data/lib/active_record/mass_assignment_security/associations.rb +116 -0
  13. data/lib/active_record/mass_assignment_security/attribute_assignment.rb +88 -0
  14. data/lib/active_record/mass_assignment_security/core.rb +27 -0
  15. data/lib/active_record/mass_assignment_security/inheritance.rb +18 -0
  16. data/lib/active_record/mass_assignment_security/nested_attributes.rb +148 -0
  17. data/lib/active_record/mass_assignment_security/persistence.rb +81 -0
  18. data/lib/active_record/mass_assignment_security/reflection.rb +9 -0
  19. data/lib/active_record/mass_assignment_security/relation.rb +47 -0
  20. data/lib/active_record/mass_assignment_security/validations.rb +24 -0
  21. data/lib/protected_attributes.rb +14 -0
  22. data/lib/protected_attributes/railtie.rb +18 -0
  23. data/lib/protected_attributes/version.rb +3 -0
  24. data/protected_attributes.gemspec +26 -0
  25. data/test/abstract_unit.rb +156 -0
  26. data/test/accessible_params_wrapper_test.rb +76 -0
  27. data/test/ar_helper.rb +67 -0
  28. data/test/attribute_sanitization_test.rb +929 -0
  29. data/test/mass_assignment_security/black_list_test.rb +20 -0
  30. data/test/mass_assignment_security/permission_set_test.rb +36 -0
  31. data/test/mass_assignment_security/sanitizer_test.rb +50 -0
  32. data/test/mass_assignment_security/white_list_test.rb +19 -0
  33. data/test/mass_assignment_security_test.rb +118 -0
  34. data/test/models/company.rb +105 -0
  35. data/test/models/keyboard.rb +3 -0
  36. data/test/models/mass_assignment_specific.rb +76 -0
  37. data/test/models/person.rb +82 -0
  38. data/test/models/subscriber.rb +5 -0
  39. data/test/models/task.rb +5 -0
  40. data/test/test_helper.rb +3 -0
  41. 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,9 @@
1
+ module ActiveRecord
2
+ module Reflection
3
+ class AssociationReflection
4
+ def build_association(*options, &block)
5
+ klass.new(*options, &block)
6
+ end
7
+ end
8
+ end
9
+ 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,3 @@
1
+ module ProtectedAttributes
2
+ VERSION = "1.0.0"
3
+ 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