mass_assignment 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 Lance Ivy
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.textile ADDED
@@ -0,0 +1,129 @@
1
+ h2. MassAssignment
2
+
3
+ Copyright (c) 2009 Lance Ivy, released under the MIT license
4
+
5
+ From initial discussion at http://groups.google.com/group/rubyonrails-core/browse_thread/thread/3b6818496d0d07f1
6
+
7
+ h3. What It Is
8
+
9
+ A robust mass assignment method with a small and obvious syntax.
10
+
11
+ The normal mass assignment protection comes from attr_protected and attr_accessible. There are a few problems with this approach:
12
+
13
+ * Often never implemented, leaving a wide-open system. And once implemented, easy to forget when adding new attributes, leading to bugs (in an attr_accessible system) or security holes (in an attr_protected system).
14
+ * Restricts coding syntax. You can't easily use update_attributes() or attributes= because your whitelist/blacklist gets in your own way.
15
+ * Not contextual. The list of allowed attributes can't change to accomodate different user permissions or situations.
16
+
17
+ This plugin's solution is to let you specify an obvious list of allowed attributes when you mass assign attributes.
18
+
19
+ * The list of allowed attributes is in your controller at calltime, so it's easier to remember and update (it's not a hidden, magical system).
20
+ * The list of allowed attributes is optional, so it doesn't get in your way.
21
+ * Your controller can easily enforce permissions by evaluating the current user (Admin Controller) or the current situation (creates vs updates).
22
+
23
+ And as a bonus, permission plugins have a much easier time of things. The list of allowed attributes may be pulled from a permissions table without any awkward User.current class or thread variables.
24
+
25
+ For those who would still like attr_protected- and attr_accessible-like functionality, this plugin offers mass assignment policies. You may choose to a default mass assignment protection as open or closed as you like using familiar :only/:except syntax. But you may also specify regular expressions such as /_id$/ to reject all id fields by default, and since these policies inherit, you may set them globally on ActiveRecord::Base. See the examples below.
26
+
27
+ h3. Example
28
+
29
+ Let's take a very plausible situation where you would want three separate lists of allowed attributes. You have users that sign up to your application. But after they have signed up, they may not change their username. Admins, however, may manually change a username as needed.
30
+
31
+ <pre><code>
32
+ class UsersController < ApplicationController
33
+ def create
34
+ @user = User.new
35
+ # during signup a user may pick a username
36
+ @user.assign(params[:user], [:username, :email, :password, :password_confirmation])
37
+ @user.save!
38
+ ...
39
+ end
40
+
41
+ def update
42
+ @user = User.find(params[:id])
43
+ # username is no longer accepted later
44
+ @user.assign(params[:user], [:email, :password, :password_confirmation])
45
+ @user.save!
46
+ ...
47
+ end
48
+ end
49
+
50
+ class Admin::UsersController < ApplicationController
51
+ before_filter :admin_required
52
+
53
+ def update
54
+ @user = User.find(params[:id])
55
+ # admins, on the other hand, may change the username as needed, but may not set passwords
56
+ @user.assign(:params[:user], [:username, :email])
57
+ @user.save!
58
+ ...
59
+ end
60
+ end
61
+ </code></pre>
62
+
63
+ If you don't always want to set attribute lists, you may use the mass_assignment_policy API to configure defaults whitelists or blacklists.
64
+
65
+ <pre><code>
66
+ class User < ActiveRecord::Base
67
+ # The boring usage. You may as well specify attributes at calltime.
68
+ mass_assignment_policy :only => [:email, :username]
69
+
70
+ # More interesting. No id fields!
71
+ mass_assignment_policy :except => /_id$/
72
+ end
73
+
74
+ # Hardcore. Disables mass assignment globally unless overridden!
75
+ ActiveRecord::Base.mass_assignment_policy :except => :all
76
+ </code></pre>
77
+
78
+ Note that mass_assignment_policy only applies to usage of methods supplied in this plugin.
79
+
80
+ h3. Nested Assignment
81
+
82
+ Nested assignment is supported by passing hashes and arrays for the whitelist.
83
+
84
+ <pre><code>
85
+ class Pirate < ActiveRecord::Base
86
+ accepts_nested_attributes_for :ships
87
+ end
88
+
89
+ class PiratesController < ApplicationController
90
+ def update
91
+ @pirate = Pirate.find(params[:id])
92
+ @pirate.assign(params[:pirate], [:name, :eyepatch, {:ships_attributes => [:id, :_destroy, :name, :cannons, :capacity]}])
93
+ @pirate.save!
94
+ ...
95
+ end
96
+ end
97
+ </code></pre>
98
+
99
+ As you can see, the punctuation gets messy. I'm interested in better options.
100
+
101
+ h3. Deep Assignment
102
+
103
+ Sometimes nested assignment isn't completely appropriate but you still need to assign attributes to associated objects. Try the block syntax:
104
+
105
+ <pre><code>
106
+ class ShipsController < ApplicationController
107
+ def update
108
+ @ship = Ship.find(params[:id])
109
+ @ship.assign(params[:ship], [:name, :cannons, :capacity]) do |ship_params|
110
+ @ship.pirate.assign(ship_params[:pirate], [:name, :eyepatch])
111
+ end
112
+ end
113
+ end
114
+ </code></pre>
115
+
116
+ The major benefit here is that the block won't yield unless the params exist, so you don't need to check 'if params[:ship] && params[:ship][:pirate]'.
117
+
118
+ h3. Feedback
119
+
120
+ I can think of a couple alternate implementations for this API. Consider:
121
+
122
+ @user.assign(params[:user], [:username, :email])
123
+
124
+ vs
125
+
126
+ @user.assign(params[:user], :only => [:username, :email])
127
+ @user.assign(params[:user], :except => [:admin])
128
+
129
+ I personally prefer the former because I think that blacklists are inherently less safe than whitelists, with no compensating advantage. But I'd love to hear some other use cases and get feedback on this! Fork this repo and send pull requests or just contact me (github.com/cainlevy). Let's talk.
data/Rakefile ADDED
@@ -0,0 +1,23 @@
1
+ require 'rake'
2
+ require 'rake/testtask'
3
+ require 'rake/rdoctask'
4
+
5
+ desc 'Default: run unit tests.'
6
+ task :default => :test
7
+
8
+ desc 'Test the mass_assignment plugin.'
9
+ Rake::TestTask.new(:test) do |t|
10
+ t.libs << 'lib'
11
+ t.libs << 'test'
12
+ t.pattern = 'test/**/*_test.rb'
13
+ t.verbose = true
14
+ end
15
+
16
+ desc 'Generate documentation for the mass_assignment plugin.'
17
+ Rake::RDocTask.new(:rdoc) do |rdoc|
18
+ rdoc.rdoc_dir = 'rdoc'
19
+ rdoc.title = 'MassAssignment'
20
+ rdoc.options << '--line-numbers' << '--inline-source'
21
+ rdoc.rdoc_files.include('README')
22
+ rdoc.rdoc_files.include('lib/**/*.rb')
23
+ end
@@ -0,0 +1,91 @@
1
+ module MassAssignment
2
+ VERSION = '1.0.0'
3
+
4
+ def self.included(base)
5
+ base.class_eval do extend ClassMethods end
6
+ end
7
+
8
+ # Basic Example:
9
+ #
10
+ # @user = User.new
11
+ # @user.assign(params[:user], [:username, :email, :password, :password_confirmation])
12
+ # @user.save!
13
+ #
14
+ # Nested Assignment:
15
+ #
16
+ # @user = User.find_by_id(params[:id])
17
+ # @user.assign(params[:user], [:username, :email, {:dog_attributes => [:id, :_destroy, :name, :color]}])
18
+ # @user.save!
19
+ #
20
+ # Deep Assignment:
21
+ #
22
+ # @user = User.find_by_id(params[:id])
23
+ # @user.assign(params[:user], [:username, :email]) do |user_params|
24
+ # @user.dog.assign(user_params[:dog], [:name, :color])
25
+ # end
26
+ # @user.save!
27
+ def assign(attributes, allowed_attributes = nil, &block)
28
+ return unless attributes and attributes.is_a? Hash
29
+
30
+ if allowed_attributes
31
+ safe_attributes = filter_attributes(attributes, :only => allowed_attributes)
32
+ yield attributes if block_given?
33
+ self.send("attributes=", safe_attributes, false)
34
+ else
35
+ if policy = self.class.get_mass_assignment_policy
36
+ safe_attributes = filter_attributes(attributes, policy)
37
+ self.send("attributes=", safe_attributes, false)
38
+ else
39
+ # backwards compatibility. use attr_protected and attr_accessible.
40
+ self.attributes = attributes
41
+ end
42
+ end
43
+ end
44
+
45
+ private
46
+
47
+ def filter_attributes(attributes, options = {}) # could surely be refactored.
48
+ attributes = attributes.stringify_keys
49
+
50
+ if options[:only]
51
+ if options[:only].is_a? Regexp
52
+ attributes.reject { |k, v| !k.gsub(/\(.+/, "").match(options[:only]) }
53
+ elsif options[:only] == :all
54
+ attributes
55
+ else
56
+ whitelist = options[:only].map{|i| i.is_a?(Hash) ? i.keys.first.to_s : i.to_s}
57
+ options[:only].each do |i|
58
+ next unless i.is_a? Hash
59
+ name = i.keys.first.to_s
60
+ next unless attributes[name].is_a? Hash
61
+ attributes[name] = filter_attributes(attributes[name], :only => i.values.first)
62
+ end
63
+ attributes.reject { |k, v| !whitelist.include?(k.gsub(/\(.+/, "")) }
64
+ end
65
+ elsif options[:except]
66
+ if options[:except].is_a? Regexp
67
+ attributes.reject { |k, v| k.gsub(/\(.+/, "").match(options[:except]) }
68
+ elsif options[:except] == :all
69
+ {}
70
+ else
71
+ blacklist = options[:except].map(&:to_s)
72
+ attributes.reject { |k, v| blacklist.include?(k.gsub(/\(.+/, "")) }
73
+ end
74
+ else
75
+ attributes
76
+ end
77
+ end
78
+
79
+ module ClassMethods
80
+ # sets a default mass assignment policy for your model's attributes. you may choose to start from a
81
+ # closed state that allows no mass assignment, an open state that allows any mass assignment (this is
82
+ # activerecord's default), or somewhere inbetween.
83
+ def mass_assignment_policy(val)
84
+ write_inheritable_attribute :mass_assignment_policy, val
85
+ end
86
+
87
+ def get_mass_assignment_policy
88
+ read_inheritable_attribute :mass_assignment_policy
89
+ end
90
+ end
91
+ end
data/rails/init.rb ADDED
@@ -0,0 +1 @@
1
+ ActiveRecord::Base.class_eval do include MassAssignment end
@@ -0,0 +1,146 @@
1
+ require File.dirname(__FILE__) + '/test_helper'
2
+
3
+ class User < ActiveRecord::Base
4
+ class << self
5
+ def columns
6
+ @columns ||= [
7
+ ActiveRecord::ConnectionAdapters::Column.new("name", nil, "varchar(100)", true),
8
+ ActiveRecord::ConnectionAdapters::Column.new("email", nil, "varchar(100)", true),
9
+ ActiveRecord::ConnectionAdapters::Column.new("role_id", nil, "integer(11)", false)
10
+ ]
11
+ end
12
+ end
13
+
14
+ def friend
15
+ @friend ||= User.new
16
+ end
17
+
18
+ def friend_attributes=(attributes)
19
+ friend.attributes = attributes
20
+ end
21
+ end
22
+
23
+ class ProtectedUser < User
24
+ attr_protected :role_id
25
+ end
26
+
27
+ class ClosedUser < User
28
+ mass_assignment_policy :except => :all
29
+ end
30
+
31
+ class SemiClosedUser < User
32
+ mass_assignment_policy :except => /_id$/
33
+ end
34
+
35
+ class OpenUser < User
36
+ mass_assignment_policy :only => :all
37
+ end
38
+
39
+ class MassAssignmentTest < ActiveSupport::TestCase
40
+ def setup
41
+ @user = ProtectedUser.new
42
+ @attributes = {"name" => "Bob", "email" => "bob@example.com"}
43
+ ActiveRecord::Base.logger = stub('debug' => true)
44
+ end
45
+
46
+ test "assigning nothing" do
47
+ params = {}
48
+ assert_nothing_raised do
49
+ @user.assign(params[:user])
50
+ end
51
+ end
52
+
53
+ test "assigning a string" do
54
+ params = {:user => "well this is embarrassing"}
55
+ assert_nothing_raised do
56
+ @user.assign(params[:user])
57
+ end
58
+ end
59
+
60
+ test "assigning attributes" do
61
+ @user.assign(@attributes)
62
+ assert_equal "Bob", @user.name
63
+ assert_equal "bob@example.com", @user.email
64
+ end
65
+
66
+ test "assigning protected attributes" do
67
+ @user.assign(@attributes.merge(:role_id => 1))
68
+ assert_equal "Bob", @user.name
69
+ assert_nil @user.role_id
70
+ end
71
+
72
+ test "overriding protected attributes" do
73
+ @user.assign(@attributes.merge(:role_id => 1), [:name, :email, :role_id])
74
+ assert_equal "Bob", @user.name
75
+ assert_equal 1, @user.role_id
76
+ end
77
+
78
+ test "assigning unallowed attributes" do
79
+ @user.assign(@attributes.merge(:role_id => 1), [:name, :email])
80
+ assert_equal "Bob", @user.name
81
+ assert_nil @user.role_id
82
+ end
83
+
84
+ test "nested assignment" do
85
+ @user.assign(@attributes.merge(:friend_attributes => {:name => 'Joe', :role_id => 1}), [:name, :role_id, {:friend_attributes => [:name]}])
86
+ assert_equal "Joe", @user.friend.name
87
+ assert_nil @user.friend.role_id
88
+ end
89
+
90
+ test "deep assignment" do
91
+ @user.assign(@attributes.merge(:friend => {:name => 'Joe', :role_id => 1}), [:name, :role_id]) do |params|
92
+ @user.friend.assign(params[:friend], [:name])
93
+ end
94
+ assert_equal "Joe", @user.friend.name
95
+ assert_nil @user.friend.role_id
96
+ end
97
+ end
98
+
99
+ class MassAssignmentPolicyTest < ActiveSupport::TestCase
100
+ def setup
101
+ @attributes = {"name" => "Bob", "role_id" => 1}
102
+ ActiveRecord::Base.logger = stub('debug' => true)
103
+ end
104
+
105
+ test "an open policy" do
106
+ @user = OpenUser.new
107
+ @user.assign(@attributes)
108
+ assert_equal "Bob", @user.name
109
+ assert_equal 1, @user.role_id
110
+ end
111
+
112
+ test "an overridden open policy" do
113
+ @user = OpenUser.new
114
+ @user.assign(@attributes, [:name])
115
+ assert_equal "Bob", @user.name
116
+ assert_nil @user.role_id
117
+ end
118
+
119
+ test "a closed policy" do
120
+ @user = ClosedUser.new
121
+ @user.assign(@attributes)
122
+ assert_nil @user.name
123
+ assert_nil @user.role_id
124
+ end
125
+
126
+ test "an overridden closed policy" do
127
+ @user = ClosedUser.new
128
+ @user.assign(@attributes, [:name, :role_id])
129
+ assert_equal "Bob", @user.name
130
+ assert_equal 1, @user.role_id
131
+ end
132
+
133
+ test "a semi-closed policy" do
134
+ @user = SemiClosedUser.new
135
+ @user.assign(@attributes)
136
+ assert_equal "Bob", @user.name
137
+ assert_nil @user.role_id
138
+ end
139
+
140
+ test "an overridden semi-closed policy" do
141
+ @user = SemiClosedUser.new
142
+ @user.assign(@attributes, [:name, :role_id])
143
+ assert_equal "Bob", @user.name
144
+ assert_equal 1, @user.role_id
145
+ end
146
+ end
@@ -0,0 +1,12 @@
1
+ require 'rubygems'
2
+ require 'test/unit'
3
+ gem 'rails', '3.0.6'
4
+ require 'active_support'
5
+ require 'active_support/test_case'
6
+ require 'active_record'
7
+
8
+ PLUGIN_ROOT = File.dirname(__FILE__) + '/../'
9
+ ActiveSupport::Dependencies.autoload_paths << File.join(PLUGIN_ROOT, 'lib')
10
+ require 'rails/init'
11
+
12
+ require 'mocha'
metadata ADDED
@@ -0,0 +1,87 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mass_assignment
3
+ version: !ruby/object:Gem::Version
4
+ hash: 23
5
+ prerelease: false
6
+ segments:
7
+ - 1
8
+ - 0
9
+ - 0
10
+ version: 1.0.0
11
+ platform: ruby
12
+ authors:
13
+ - Lance Ivy
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2011-04-13 00:00:00 -07:00
19
+ default_executable:
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: mocha
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ hash: 3
30
+ segments:
31
+ - 0
32
+ version: "0"
33
+ type: :development
34
+ version_requirements: *id001
35
+ description: An alternative to attr_protected that supports a simpler, more secure params assignment mindset while also encouraging obviousness.
36
+ email: lance@cainlevy.net
37
+ executables: []
38
+
39
+ extensions: []
40
+
41
+ extra_rdoc_files: []
42
+
43
+ files:
44
+ - lib/mass_assignment.rb
45
+ - LICENSE
46
+ - README.textile
47
+ - Rakefile
48
+ - rails/init.rb
49
+ - test/test_helper.rb
50
+ - test/mass_assignment_test.rb
51
+ has_rdoc: true
52
+ homepage: http://github.com/cainlevy/mass_assignment
53
+ licenses: []
54
+
55
+ post_install_message:
56
+ rdoc_options: []
57
+
58
+ require_paths:
59
+ - lib
60
+ required_ruby_version: !ruby/object:Gem::Requirement
61
+ none: false
62
+ requirements:
63
+ - - ">="
64
+ - !ruby/object:Gem::Version
65
+ hash: 3
66
+ segments:
67
+ - 0
68
+ version: "0"
69
+ required_rubygems_version: !ruby/object:Gem::Requirement
70
+ none: false
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ hash: 3
75
+ segments:
76
+ - 0
77
+ version: "0"
78
+ requirements: []
79
+
80
+ rubyforge_project:
81
+ rubygems_version: 1.3.7
82
+ signing_key:
83
+ specification_version: 3
84
+ summary: Simple and secure params assignment for ActiveRecord
85
+ test_files:
86
+ - test/test_helper.rb
87
+ - test/mass_assignment_test.rb