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.
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