scoped_attr_accessible 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/.bundle/config ADDED
@@ -0,0 +1,2 @@
1
+ ---
2
+ BUNDLE_DISABLE_SHARED_GEMS: "1"
data/.gitignore ADDED
@@ -0,0 +1 @@
1
+ coverage
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --colour
2
+ --format=documentation
3
+ --debug
data/.rvmrc ADDED
@@ -0,0 +1 @@
1
+ rvm use @scopedattraccessible --create
data/Gemfile ADDED
@@ -0,0 +1,12 @@
1
+ source 'http://rubygems.org/'
2
+
3
+ gem 'activesupport', '~> 3.0.0'
4
+ gem 'activemodel', '~> 3.0.0'
5
+
6
+ group :development do
7
+ gem 'rspec', '~> 2.0'
8
+ gem 'rr'
9
+ gem 'ruby-debug'
10
+ gem 'rcov'
11
+ gem 'ZenTest', :require => nil
12
+ end
data/Gemfile.lock ADDED
@@ -0,0 +1,43 @@
1
+ GEM
2
+ remote: http://rubygems.org/
3
+ specs:
4
+ ZenTest (4.4.0)
5
+ activemodel (3.0.0)
6
+ activesupport (= 3.0.0)
7
+ builder (~> 2.1.2)
8
+ i18n (~> 0.4.1)
9
+ activesupport (3.0.0)
10
+ builder (2.1.2)
11
+ columnize (0.3.1)
12
+ diff-lcs (1.1.2)
13
+ i18n (0.4.1)
14
+ linecache (0.43)
15
+ rcov (0.9.9)
16
+ rr (1.0.0)
17
+ rspec (2.0.0)
18
+ rspec-core (= 2.0.0)
19
+ rspec-expectations (= 2.0.0)
20
+ rspec-mocks (= 2.0.0)
21
+ rspec-core (2.0.0)
22
+ rspec-expectations (2.0.0)
23
+ diff-lcs (>= 1.1.2)
24
+ rspec-mocks (2.0.0)
25
+ rspec-core (= 2.0.0)
26
+ rspec-expectations (= 2.0.0)
27
+ ruby-debug (0.10.3)
28
+ columnize (>= 0.1)
29
+ ruby-debug-base (~> 0.10.3.0)
30
+ ruby-debug-base (0.10.3)
31
+ linecache (>= 0.3)
32
+
33
+ PLATFORMS
34
+ ruby
35
+
36
+ DEPENDENCIES
37
+ ZenTest
38
+ activemodel (~> 3.0.0)
39
+ activesupport (~> 3.0.0)
40
+ rcov
41
+ rr
42
+ rspec (~> 2.0)
43
+ ruby-debug
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 The Frontier Group.
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.md ADDED
@@ -0,0 +1,150 @@
1
+ # scoped\_attr\_accessible
2
+
3
+ scoped\_attr\_accessible is a plugin that makes it easy to scope the `attr_accessible` and `attr_protected`
4
+ methods on any library using ActiveModel's MassAssignmentSecurity module. For those unfamiliar with it,
5
+ [read here](http://api.rubyonrails.org/classes/ActiveModel/MassAssignmentSecurity/ClassMethods.html#method-i-attr_accessible)
6
+ to get a bit of back story about how it works in ActiveRecord - thanks to ActiveModel, you can
7
+ now get the joy of scoped access restrictions across any ORM built on ActiveModel, including [mongoid](http://mongoid.org/)!
8
+
9
+ ## Installation ##
10
+
11
+
12
+ To use, just add to any application using ActiveModel. In Rails 3, this is a simple job of adding:
13
+
14
+ gem 'scoped_attr_accessible'
15
+
16
+ To our Gemfile and running `bundle install`.
17
+
18
+ ## Usage
19
+
20
+ With it enabled, your application should continue to work as usual with classic `attr_accessible` and `attr_protected`.
21
+ When in use, you can simply pass the `:scope` option in your declaration to declare a scope in which it should be accessible.
22
+
23
+ For example,
24
+
25
+ class User < ActiveRecord::Base
26
+
27
+ # All attributes are accessible for the admin scope.
28
+ attr_accessible :all, :scope => :admin
29
+
30
+ # The default scope can only access a and b.
31
+ attr_accessible :a, :b
32
+
33
+ # Make both :c and :d accessible for owners and the default scope
34
+ attr_accessible :c, :d, :scope => [:owner, :default]
35
+
36
+ # Also, it works the same with attr_protected!
37
+ attr_protected :n, :scope => :default
38
+
39
+ end
40
+
41
+ If both `attr_accessible` and `attr_protected` are used on a given scope, attributes
42
+ declared in `attr_protected` take precedence. Also, If `attr_accessible` isn't called for a scope
43
+ at all, it will allow all variables except those marked as protected.
44
+
45
+ When declaring the scopes in the accessible / protected part, please note that they need to
46
+ be symbol names for simplicity's sake.
47
+
48
+ ### Setting the Scope
49
+
50
+ Next, when you call methods that use mass assignment (e.g. `ActiveRecord::Base#attributes=`),
51
+ it will use your current scope to sanitize mass-assigned variables. By default, with no
52
+ user intervention this scope is simply `:default`.
53
+
54
+ To set the scope, you can do so on a class an instance level with instance-level taking precedence.
55
+
56
+ To set it on a class level, simply do:
57
+
58
+ User.current_sanitizer_scope = :admin
59
+ # Or, dynamically:
60
+ User.current_sanitizer_scope = @user.role.name.to_sym
61
+
62
+ This will be set Thread local. Also note you can get the current class-level scope:
63
+
64
+ p User.current_sanitizer_scope # => nil by default
65
+
66
+ Or, temporarily switch it out, resetting it afterwards:
67
+
68
+ p User.current_sanitizer_scope
69
+ User.with_sanitizer_scope :admin do
70
+ p User.current_sanitizer_scope
71
+ end
72
+ p User.current_sanitizer_scope
73
+
74
+ You can also declare this on the instance level, e.g:
75
+
76
+ user = User.find(params[:id])
77
+ user.current_sanitizer_scope = :admin
78
+ # Or, more complex:
79
+ user.current_sanitizer_scope = "something-else"
80
+
81
+ ### Complex Scoping
82
+
83
+ Although the scope on a given accessible / protected declaration must be a symbol,
84
+ scoped\_attr\_accessible provides a way to deal with non-symbol scopes when assigning them - Namely,
85
+ you can set the `current_sanitizer_scope` value on classes or instances to an
86
+ arbitrary object and let scoped\_attr\_accessible dynamically convert it for you.
87
+
88
+ This is done using two seperate processes - Recognizers and Converters, each run when a given
89
+ scope is not a symbol.
90
+
91
+ The first of these (and the highest priority) are recognizers - they are simply blocks you
92
+ can declare (like below) that have a scope name and return a value denoting whether or not they
93
+ match. e.g:
94
+
95
+ # Reeopen the class
96
+ class User < ActiveRecord::Base
97
+
98
+ sanitizer_scope_recognizer :admin do |record, scope_value|
99
+ scope_value.is_a?(User) && user.admin?
100
+ end
101
+
102
+ sanitizer_scope_recognizer :owner do |record, scope_value|
103
+ scope_value.is_a?(User) && scope_value == record
104
+ end
105
+
106
+ end
107
+
108
+ In this example, we could simply do:
109
+
110
+ user = User.find(params[:id])
111
+ user.current_sanitizer_scope = current_user
112
+ user.update_attributes params[:user]
113
+
114
+ And it would automatically set the scope to :owner / :admin when sanitizing the attributes.
115
+
116
+ The second and more flexible option is scope convertors - they're given the same information (e.g.
117
+ a record and scope value) and they are responsible for returning nil or a reduced form of the scope.
118
+ If they return a reduced form (e.g. they may return their creating user, or a plain symbol) it is
119
+ smart enough to reduce it until it does have a symbol.
120
+
121
+ As an example, we could implement the following:
122
+
123
+ # Reeopen the class
124
+ class User < ActiveRecord::Base
125
+
126
+ sanitizer_scope_converter do |record, scope_value|
127
+ return user.role.name.to_sym if scope_value.is_a?(User)
128
+ return scope_value.user if scope_value.is_a?(UserSession)
129
+ end
130
+
131
+ end
132
+
133
+ When combined, these all form a very flexible way to dynamically scope attribute accessible.
134
+
135
+ ## Note on Patches/Pull Requests
136
+
137
+ * Fork the project.
138
+ * Make your feature addition or bug fix.
139
+ * Add tests for it. This is important so I don't break it in a future version unintentionally.
140
+ * Commit, do not mess with rakefile, version, or history. (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
141
+ * Send me a pull request. Bonus points for topic branches.
142
+
143
+ ## Contributors
144
+
145
+ * Darcy Laycock
146
+ * Mario Visic
147
+
148
+ ## Copyright
149
+
150
+ Copyright (c) 2010 The Frontier Group. See LICENSE for details.
data/Rakefile ADDED
@@ -0,0 +1,47 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "scoped_attr_accessible"
8
+ gem.summary = %Q{Scoping for attr_accessible and attr_protected on ActiveModel objects.}
9
+ gem.description = %Q{scoped_attr_accessible is a plugin that makes it easy to scope the `attr_accessible` and `attr_protected`
10
+ methods on any library using ActiveModel's MassAssignmentSecurity module.}
11
+ gem.email = "team+darcy+mario@thefrontiergroup.com.au"
12
+ gem.homepage = "http://github.com/thefrontiergroup/scoped_attr_accessible"
13
+ gem.authors = ["Darcy Laycock", "Mario Visic"]
14
+ gem.add_dependency "activemodel", "~> 3.0"
15
+ gem.add_development_dependency "rspec", "~> 2.0"
16
+ end
17
+ Jeweler::GemcutterTasks.new
18
+ rescue LoadError
19
+ puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
20
+ end
21
+
22
+ require 'rspec/core'
23
+ require 'rspec/core/rake_task'
24
+ task :default => :spec
25
+
26
+ desc "Run all specs in spec directory (excluding plugin specs)"
27
+ RSpec::Core::RakeTask.new(:spec)
28
+
29
+ namespace :spec do
30
+ desc "Run all specs with rcov"
31
+ RSpec::Core::RakeTask.new(:rcov) do |t|
32
+ t.rcov = true
33
+ t.pattern = "./spec/**/*_spec.rb"
34
+ t.rcov_opts = '--exclude spec/,/gems/,/Library/,/usr/,lib/tasks,.bundle,config,/lib/rspec/,/lib/rspec-'
35
+ end
36
+ end
37
+
38
+
39
+
40
+ require 'rake/rdoctask'
41
+ Rake::RDocTask.new do |rdoc|
42
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
43
+ rdoc.rdoc_dir = 'rdoc'
44
+ rdoc.title = "scoped_attr_accessible #{version}"
45
+ rdoc.rdoc_files.include('README*')
46
+ rdoc.rdoc_files.include('lib/**/*.rb')
47
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.0
@@ -0,0 +1,7 @@
1
+ begin
2
+ require 'autotest/fsevent'
3
+ require 'autotest/growl'
4
+ rescue LoadError
5
+ end
6
+
7
+ Autotest.add_discovery { "rspec2" }
@@ -0,0 +1,109 @@
1
+ require 'active_support/concern'
2
+ require 'active_support/core_ext/class/inheritable_attributes'
3
+ require 'active_support/core_ext/array/extract_options'
4
+
5
+ module ScopedAttrAccessible
6
+ module ActiveModelMixin
7
+ extend ActiveSupport::Concern
8
+
9
+
10
+ module IncludedHook
11
+ def included(base)
12
+ super
13
+ base.class_eval { include ActiveModelMixin }
14
+ end
15
+ end
16
+
17
+ included do
18
+ extlib_inheritable_accessor :_scoped_attr_sanitizer
19
+ end
20
+
21
+ module ClassMethods
22
+
23
+ def attr_accessible(*args)
24
+ scopes = scopes_from_args(args)
25
+ sanitizer = self.scoped_attr_sanitizer
26
+ args.each do |attribute|
27
+ scopes.each { |s| sanitizer.make_accessible attribute, s }
28
+ end
29
+ self._active_authorizer = sanitizer
30
+ end
31
+
32
+ def attr_protected(*args)
33
+ scopes = scopes_from_args(args)
34
+ sanitizer = self.scoped_attr_sanitizer
35
+ args.each do |attribute|
36
+ scopes.each { |s| sanitizer.make_protected attribute, s }
37
+ end
38
+ self._active_authorizer = sanitizer
39
+ end
40
+
41
+ def scoped_attr_sanitizer
42
+ self._scoped_attr_sanitizer ||= ScopedAttrAccessible::Sanitizer.new
43
+ end
44
+
45
+ def current_sanitizer_scope
46
+ Thread.current[current_sanitizer_scope_key]
47
+ end
48
+
49
+ def current_sanitizer_scope=(value)
50
+ Thread.current[current_sanitizer_scope_key] = value
51
+ end
52
+
53
+ def with_sanitizer_scope(scope_name)
54
+ old_scope = current_sanitizer_scope
55
+ self.current_sanitizer_scope = scope_name
56
+ yield if block_given?
57
+ ensure
58
+ self.current_sanitizer_scope = old_scope
59
+ end
60
+
61
+ def sanitizer_scope_recognizer(name, &recognizer)
62
+ scoped_attr_sanitizer.define_recognizer(name, &recognizer)
63
+ end
64
+
65
+ def sanitizer_scope_converter(&converter)
66
+ scoped_attr_sanitizer.define_converter(&converter)
67
+ end
68
+
69
+ protected
70
+
71
+ def current_sanitizer_scope_key
72
+ :"#{name}_sanitizer_scope"
73
+ end
74
+
75
+ def scopes_from_args(args)
76
+ options = args.extract_options!
77
+ scope = Array(options.delete(:scope)).map(&:to_sym)
78
+ scope << :default if scope.empty?
79
+ args << options unless options.empty?
80
+ scope
81
+ end
82
+
83
+ end
84
+
85
+ module InstanceMethods
86
+
87
+ def current_sanitizer_scope
88
+ @current_sanitizer_scope || self.class.current_sanitizer_scope || :default
89
+ end
90
+
91
+ def current_sanitizer_scope=(value)
92
+ @current_sanitizer_scope = value
93
+ end
94
+
95
+ protected
96
+
97
+ def sanitize_for_mass_assignment(attributes)
98
+ authorizer = self.mass_assignment_authorizer
99
+ if authorizer.respond_to?(:sanitize_with_scope)
100
+ authorizer.sanitize_with_scope attributes, current_sanitizer_scope, self
101
+ else
102
+ super
103
+ end
104
+ end
105
+
106
+ end
107
+
108
+ end
109
+ end
@@ -0,0 +1,80 @@
1
+ require 'set'
2
+
3
+ module ScopedAttrAccessible
4
+ class Sanitizer
5
+
6
+ def initialize
7
+ @accessible_attributes = Hash.new { |h,k| h[k] = Set.new }
8
+ @protected_attributes = Hash.new { |h,k| h[k] = Set.new }
9
+ # Scope recognizers return a boolean, with a hash key
10
+ @scope_recognizers = Hash.new { |h,k| h[k] = [] }
11
+ # Returns a scope symbol.
12
+ @scope_converters = []
13
+ end
14
+
15
+ # Looks up a scope name from the registered recognizers and then from the converters.
16
+ def normalize_scope(object, context)
17
+ return object if object.is_a?(Symbol)
18
+ # 1. Process recognizers, looking for a match.
19
+ @scope_recognizers.each_pair do |name, recognizers|
20
+ return name if recognizers.any? { |r| lambda(&r).call(context, object) }
21
+ end
22
+ # 2. Process converters, finding a result.
23
+ @scope_converters.each do |converter|
24
+ scope = lambda(&converter).call(context, object)
25
+ return normalize_scope(scope, converter) unless scope.nil?
26
+ end
27
+ # 3. Fall back to default
28
+ return :default
29
+ end
30
+
31
+ def sanitize(attributes, context = Object.new)
32
+ sanitize_with_scope attributes, :default, context
33
+ end
34
+
35
+ def sanitize_with_scope(attributes, scope, context)
36
+ scope = normalize_scope scope, context
37
+ attributes.reject { |k, v| deny? k, scope }
38
+ end
39
+
40
+ def define_recognizer(scope, &blk)
41
+ @scope_recognizers[scope.to_sym] << blk
42
+ end
43
+
44
+ def define_converter(&blk)
45
+ @scope_converters << blk
46
+ end
47
+
48
+ def make_protected(attribute, scope = :default)
49
+ @protected_attributes[scope.to_sym] << attribute.to_s
50
+ end
51
+
52
+ def make_accessible(attribute, scope = :default)
53
+ @accessible_attributes[scope.to_sym] << attribute.to_s
54
+ end
55
+
56
+ def deny?(attribute, scope = :default)
57
+ !attribute_assignable_with_scope?(attribute, scope)
58
+ end
59
+
60
+ def allow?(attribute, scope = :default)
61
+ attribute_assignable_with_scope?(attribute, scope)
62
+ end
63
+
64
+ def attribute_assignable_with_scope?(attribute, scope)
65
+ attribute = attribute.to_s.gsub(/\(.+/, '')
66
+ scope = scope.to_sym
67
+ scope_protected, scope_accessible = @protected_attributes[scope], @accessible_attributes[scope]
68
+ if scope_protected.include? attribute
69
+ return false
70
+ elsif scope_accessible.include?('all') || scope_accessible.include?(attribute)
71
+ return true
72
+ elsif !scope_accessible.empty?
73
+ return false
74
+ else
75
+ return true
76
+ end
77
+ end
78
+
79
+ end
80
+ end
@@ -0,0 +1,15 @@
1
+ require 'active_support/concern'
2
+
3
+ module ScopedAttrAccessible
4
+ autoload :Sanitizer, 'scoped_attr_accessible/sanitizer'
5
+ autoload :ActiveModelMixin, 'scoped_attr_accessible/active_model_mixin'
6
+
7
+ # Mixes the am mixin into ActiveModel's mass assignment helpers.
8
+ def self.mixin!
9
+ require 'active_model/mass_assignment_security'
10
+ ActiveModel::MassAssignmentSecurity.module_eval do
11
+ extend ScopedAttrAccessible::ActiveModelMixin::IncludedHook
12
+ end
13
+ end
14
+
15
+ end
@@ -0,0 +1,314 @@
1
+ require 'spec_helper'
2
+
3
+ describe ScopedAttrAccessible::ActiveModelMixin do
4
+
5
+ before :all do
6
+ ScopedAttrAccessible.mixin!
7
+ end
8
+
9
+ subject { Class.new { include ActiveModel::MassAssignmentSecurity } }
10
+
11
+ let(:subject_instance) { subject.new }
12
+
13
+ context 'attr_accessible overrides' do
14
+
15
+ before :each do
16
+ @sanitizer = ScopedAttrAccessible::Sanitizer.new
17
+ stub(subject).scoped_attr_sanitizer { @sanitizer }
18
+ end
19
+
20
+ it 'should call make_accessible' do
21
+ mock(@sanitizer).make_accessible(:a, :x)
22
+ subject.attr_accessible :a, :scope => :x
23
+ end
24
+
25
+ it 'should default the scope' do
26
+ mock(@sanitizer).make_accessible(:a, :default)
27
+ subject.attr_accessible :a
28
+ end
29
+
30
+ it 'should call it correctly when given multiple scopes' do
31
+ mock(@sanitizer).make_accessible(:a, :admin)
32
+ mock(@sanitizer).make_accessible(:a, :normal)
33
+ subject.attr_accessible :a, :scope => [:admin, :normal]
34
+ end
35
+
36
+ it 'should call make_accessible for each attribute' do
37
+ mock(@sanitizer).make_accessible(:a, :default)
38
+ mock(@sanitizer).make_accessible(:b, :default)
39
+ subject.attr_accessible :a, :b
40
+ end
41
+
42
+ it 'should set the active authorizer' do
43
+ subject._active_authorizer = nil
44
+ subject.attr_accessible :a, :b
45
+ subject._active_authorizer.should == @sanitizer
46
+ end
47
+
48
+ end
49
+
50
+ context 'attr_protected overrides' do
51
+
52
+ before :each do
53
+ @sanitizer = ScopedAttrAccessible::Sanitizer.new
54
+ stub(subject).scoped_attr_sanitizer { @sanitizer }
55
+ end
56
+
57
+ it 'should call make_protected' do
58
+ mock(@sanitizer).make_protected(:a, :x)
59
+ subject.attr_protected :a, :scope => :x
60
+ end
61
+
62
+ it 'should default the scope' do
63
+ mock(@sanitizer).make_protected(:a, :default)
64
+ subject.attr_protected :a
65
+ end
66
+
67
+ it 'should call it correctly when given multiple scopes' do
68
+ mock(@sanitizer).make_protected(:a, :admin)
69
+ mock(@sanitizer).make_protected(:a, :normal)
70
+ subject.attr_protected :a, :scope => [:admin, :normal]
71
+ end
72
+
73
+ it 'should call make_protected for each attribute' do
74
+ mock(@sanitizer).make_protected(:a, :default)
75
+ mock(@sanitizer).make_protected(:b, :default)
76
+ subject.attr_protected :a, :b
77
+ end
78
+
79
+ it 'should set the active authorizer' do
80
+ subject._active_authorizer = nil
81
+ subject.attr_protected :a, :b
82
+ subject._active_authorizer.should == @sanitizer
83
+ end
84
+
85
+ end
86
+
87
+ context 'getting the current sanitizer' do
88
+
89
+ it 'should create a new sanitizer if not present' do
90
+ subject._scoped_attr_sanitizer = nil
91
+ @sanitizer = ScopedAttrAccessible::Sanitizer.new
92
+ mock(ScopedAttrAccessible::Sanitizer).new { @sanitizer }
93
+ subject.scoped_attr_sanitizer.should equal(@sanitizer)
94
+ end
95
+
96
+ it 'should memoize the sanitizer' do
97
+ sanitizer = subject.scoped_attr_sanitizer
98
+ sanitizer_two = subject.scoped_attr_sanitizer
99
+ sanitizer.should equal(sanitizer)
100
+ end
101
+
102
+ end
103
+
104
+ context 'current sanitizer scope' do
105
+
106
+ it 'should let you get the default sanitizer scope' do
107
+ subject.current_sanitizer_scope.should be_nil
108
+ end
109
+
110
+ it 'should let you set the sanitizer scope' do
111
+ subject.current_sanitizer_scope = :a
112
+ subject.current_sanitizer_scope.should == :a
113
+ subject.current_sanitizer_scope = :b
114
+ subject.current_sanitizer_scope.should == :b
115
+ end
116
+
117
+ it 'should let you temporary change the sanitizer scope' do
118
+ subject.current_sanitizer_scope = :before
119
+ inner_called = false
120
+ subject.with_sanitizer_scope :after do
121
+ inner_called = true
122
+ subject.current_sanitizer_scope.should == :after
123
+ end
124
+ inner_called.should be_true
125
+ end
126
+
127
+ it 'should reset the scope after a temporary change' do
128
+ subject.current_sanitizer_scope = :before
129
+ inner_called = false
130
+ subject.with_sanitizer_scope :after do
131
+ inner_called = true
132
+ end
133
+ inner_called.should be_true
134
+ subject.current_sanitizer_scope.should == :before
135
+ end
136
+
137
+ end
138
+
139
+ context 'instance level scoping' do
140
+
141
+ before :each do
142
+ subject.current_sanitizer_scope = :class_level
143
+ subject_instance.current_sanitizer_scope = :instance_level
144
+ end
145
+
146
+ it 'should use the instance scope if present' do
147
+ subject_instance.current_sanitizer_scope.should == :instance_level
148
+ end
149
+
150
+ it 'should fallback to the class scope if the instance scope is not present' do
151
+ subject_instance.current_sanitizer_scope = nil
152
+ subject_instance.current_sanitizer_scope.should == :class_level
153
+ end
154
+
155
+ it 'should fallback to default if no scope is set' do
156
+ subject_instance.current_sanitizer_scope = nil
157
+ subject.current_sanitizer_scope = nil
158
+ subject_instance.current_sanitizer_scope.should == :default
159
+ end
160
+
161
+ it 'should let you assign the instance level scope' do
162
+ subject_instance.current_sanitizer_scope = :after
163
+ subject_instance.current_sanitizer_scope.should == :after
164
+ end
165
+
166
+ end
167
+
168
+ context 'recognizers and converters' do
169
+
170
+ before :each do
171
+ @sanitizer = ScopedAttrAccessible::Sanitizer.new
172
+ stub(subject).scoped_attr_sanitizer { @sanitizer }
173
+ end
174
+
175
+ it 'should call the sanitizers methods when defining a converter' do
176
+ mock(@sanitizer).define_converter
177
+ subject.sanitizer_scope_converter {}
178
+ end
179
+
180
+ it 'should call the sanitizers methods when defining a recognizer' do
181
+ mock(@sanitizer).define_recognizer(:admin)
182
+ subject.sanitizer_scope_recognizer(:admin) { }
183
+ end
184
+
185
+ end
186
+
187
+ context 'hooking into the process' do
188
+
189
+ subject do
190
+ Class.new do
191
+ include ActiveModel::MassAssignmentSecurity
192
+
193
+ attr_accessible :a, :b, :c
194
+ attr_accessible :a, :b, :c, :d, :scope => :owner
195
+ attr_accessible :all, :scope => :admin
196
+ attr_protected :c, :d, :scope => :guy_who_deletes_stuff
197
+
198
+ def sanitized_keys_from(hash)
199
+ sanitize_for_mass_assignment(hash).keys
200
+ end
201
+ end
202
+ end
203
+
204
+ let(:subject_instance) { subject.new }
205
+
206
+ let :example_attributes do
207
+ {'a' => 'a', 'b' => 'c', 'c' => 'c', 'd' => 'd', 'e' => 'e'}
208
+ end
209
+
210
+ it 'should return the correct attributes for the default scope' do
211
+ subject_instance.current_sanitizer_scope = nil
212
+ subject.current_sanitizer_scope = nil
213
+ subject_instance.sanitized_keys_from(example_attributes).should == %w(a b c)
214
+ end
215
+
216
+ it 'should return the correct attributes for the owner scope' do
217
+ subject_instance.current_sanitizer_scope = :owner
218
+ subject_instance.sanitized_keys_from(example_attributes).should == %w(a b c d)
219
+ end
220
+
221
+ it 'should return the correct attributes for the admin scope' do
222
+ subject_instance.current_sanitizer_scope = :admin
223
+ subject_instance.sanitized_keys_from(example_attributes).should == %w(a b c d e)
224
+ end
225
+
226
+ it 'should return the correct attributes for an unknown scope' do
227
+ subject_instance.current_sanitizer_scope = :unknown_scope
228
+ subject_instance.sanitized_keys_from(example_attributes).should == %w(a b c d e)
229
+ end
230
+
231
+ it 'should return the correct attributes for the guy_who_deletes_stuff scope' do
232
+ subject_instance.current_sanitizer_scope = :guy_who_deletes_stuff
233
+ subject_instance.sanitized_keys_from(example_attributes).should == %w(a b e)
234
+ end
235
+
236
+ end
237
+
238
+ context 'tying it all together' do
239
+
240
+ subject do
241
+ Class.new do
242
+ include ActiveModel::MassAssignmentSecurity
243
+
244
+ attr_accessible :a, :b, :c
245
+ attr_accessible :a, :b, :c, :d, :scope => :owner
246
+ attr_accessible :all, :scope => :admin
247
+ attr_protected :c, :d, :scope => :guy_who_deletes_stuff
248
+
249
+ sanitizer_scope_recognizer :owner do |record, value|
250
+ value.is_a?(Numeric)
251
+ end
252
+
253
+ sanitizer_scope_converter do |record, value|
254
+ return :admin if value == "hola"
255
+ return :guy_who_deletes_stuff if record.name == "Bob"
256
+ end
257
+
258
+ attr_accessor :name
259
+
260
+ def sanitized_keys_from(hash)
261
+ sanitize_for_mass_assignment(hash).keys
262
+ end
263
+ end
264
+ end
265
+
266
+ let(:subject_instance) { subject.new }
267
+
268
+ let :example_attributes do
269
+ {'a' => 'a', 'b' => 'c', 'c' => 'c', 'd' => 'd', 'e' => 'e'}
270
+ end
271
+
272
+ before :each do
273
+ subject_instance.current_sanitizer_scope = nil
274
+ subject.current_sanitizer_scope = nil
275
+
276
+ end
277
+
278
+ it 'should return the correct attributes for the default scope' do
279
+ subject_instance.sanitized_keys_from(example_attributes).should == %w(a b c)
280
+ end
281
+
282
+ it 'should return the correct attributes for the owner scope' do
283
+ subject_instance.current_sanitizer_scope = 12
284
+ subject_instance.sanitized_keys_from(example_attributes).should == %w(a b c d)
285
+ end
286
+
287
+ it 'should return the correct attributes for the admin scope' do
288
+ subject_instance.current_sanitizer_scope = "hola"
289
+ subject_instance.sanitized_keys_from(example_attributes).should == %w(a b c d e)
290
+ end
291
+
292
+ it 'should return the correct attributes for the guy_who_deletes_stuff scope' do
293
+ subject_instance.name = "Bob"
294
+ subject_instance.current_sanitizer_scope = "trigger-it"
295
+ subject_instance.sanitized_keys_from(example_attributes).should == %w(a b e)
296
+ subject_instance.name = nil
297
+ end
298
+
299
+ it 'should fallback if with a different sanitizer' do
300
+ klass = Class.new(subject)
301
+ klass._active_authorizer = nil
302
+ instance = klass.new
303
+ instance.sanitized_keys_from(example_attributes).should == %w(a b c d e)
304
+ end
305
+
306
+ it 'should not overwrite children class sanitizers' do
307
+ klass = Class.new(subject)
308
+ klass.attr_accessible :e, :scope => :owner
309
+ subject.scoped_attr_sanitizer.sanitize_with_scope(example_attributes, :owner, nil).should only_have_attributes('a', 'b', 'c', 'd')
310
+ end
311
+
312
+ end
313
+
314
+ end
@@ -0,0 +1,185 @@
1
+ require 'spec_helper'
2
+
3
+ describe ScopedAttrAccessible::Sanitizer do
4
+
5
+ let :complex_sanitizer_one do
6
+ ScopedAttrAccessible::Sanitizer.new.tap do |s|
7
+ s.make_protected :a, :default
8
+ s.make_protected :b, :default
9
+ s.make_accessible :all, :default
10
+ s.make_protected :a, :admin
11
+ s.make_accessible :all, :admin
12
+ end
13
+ end
14
+
15
+ let :complex_sanitizer_two do
16
+ ScopedAttrAccessible::Sanitizer.new.tap do |s|
17
+ s.make_protected :d, :admin
18
+ s.make_accessible :all, :admin
19
+ s.make_accessible :a, :default
20
+ s.make_accessible :b, :default
21
+ end
22
+ end
23
+
24
+ let :example_attributes_one do
25
+ {:a => 'a', :b => 'c', :c => 'c', :d => 'd', :e => 'e'}
26
+ end
27
+
28
+ let :example_attributes_two do
29
+ {'a' => 'a', 'b' => 'c', 'c' => 'c', 'd' => 'd'}
30
+ end
31
+
32
+ context 'normalizing scopes' do
33
+
34
+ subject { ScopedAttrAccessible::Sanitizer.new }
35
+
36
+ before :each do
37
+ subject.define_recognizer :a do |context, object|
38
+ object.to_s == "a" || context == "test context a"
39
+ end
40
+ subject.define_recognizer :b do |context, object|
41
+ object.to_s == "b" || context == "test context b"
42
+ end
43
+ subject.define_converter do |context, object|
44
+ return :number if object.is_a?(Numeric)
45
+ return :admin if !object.nil? && %(admin darcy).include?(object)
46
+ return :awesome if context == "another test context"
47
+ end
48
+ end
49
+
50
+ it 'should return the argument if passed a symbol' do
51
+ subject.normalize_scope(:a, nil).should == :a
52
+ subject.normalize_scope(:another, nil).should == :another
53
+ end
54
+
55
+ it 'should let you provide optional context' do
56
+ subject.normalize_scope(nil, "test context a").should == :a
57
+ subject.normalize_scope(nil, "test context b").should == :b
58
+ subject.normalize_scope(nil, "another test context").should == :awesome
59
+ end
60
+
61
+ it 'should use recognizers in an attempt to find a scope' do
62
+ subject.normalize_scope("a", nil).should == :a
63
+ subject.normalize_scope("b", nil).should == :b
64
+ end
65
+
66
+ it 'should use converters in an attempt to find a scope' do
67
+ subject.normalize_scope(2, nil).should == :number
68
+ subject.normalize_scope(3.0, nil).should == :number
69
+ subject.normalize_scope('admin', nil).should == :admin
70
+ subject.normalize_scope('darcy', nil).should == :admin
71
+ end
72
+
73
+ it 'should return default when the item is unknown' do
74
+ subject.normalize_scope("unknown item here", nil).should == :default
75
+ end
76
+
77
+ end
78
+
79
+ context 'marking attributes availability' do
80
+
81
+ def allow(*args)
82
+ be_allow(*args)
83
+ end
84
+
85
+ let :empty_sanitizer do
86
+ ScopedAttrAccessible::Sanitizer.new
87
+ end
88
+
89
+ let :accessible_sanitizer do
90
+ ScopedAttrAccessible::Sanitizer.new.tap do |s|
91
+ s.make_accessible :a, :default
92
+ s.make_accessible :b, :default
93
+ s.make_accessible :c, :admin
94
+ end
95
+ end
96
+
97
+ let :protected_sanitizer do
98
+ ScopedAttrAccessible::Sanitizer.new.tap do |s|
99
+ s.make_protected :a, :default
100
+ s.make_protected :b, :default
101
+ s.make_protected :c, :admin
102
+ end
103
+ end
104
+
105
+ it 'should return true by default an empty list' do
106
+ empty_sanitizer.should allow(:a)
107
+ empty_sanitizer.should allow(:b)
108
+ empty_sanitizer.should allow(:c)
109
+ empty_sanitizer.should allow(:d)
110
+ empty_sanitizer.should allow(:a, :admin)
111
+ empty_sanitizer.should allow(:b, :admin)
112
+ empty_sanitizer.should allow(:c, :admin)
113
+ empty_sanitizer.should allow(:d, :admin)
114
+ end
115
+
116
+ it 'should return false by default with an unknown attribute and an accessible list' do
117
+ accessible_sanitizer.should_not allow(:d)
118
+ accessible_sanitizer.should_not allow(:d, :admin)
119
+ accessible_sanitizer.should_not allow(:c)
120
+ accessible_sanitizer.should_not allow(:a, :admin)
121
+ accessible_sanitizer.should_not allow(:b, :admin)
122
+ end
123
+
124
+ it 'should return false when it is protected' do
125
+ protected_sanitizer.should_not allow(:a, :default)
126
+ protected_sanitizer.should_not allow(:b, :default)
127
+ protected_sanitizer.should_not allow(:c, :admin)
128
+ end
129
+
130
+ it 'should work with a string' do
131
+ complex_sanitizer_two.should allow('a')
132
+ complex_sanitizer_two.should allow('b')
133
+ complex_sanitizer_two.should_not allow('d', :admin)
134
+ complex_sanitizer_two.should allow('a', :admin)
135
+ end
136
+
137
+ it 'should return true if it is in the accessible' do
138
+ accessible_sanitizer.should allow(:a)
139
+ accessible_sanitizer.should allow(:b)
140
+ accessible_sanitizer.should allow(:c, :admin)
141
+ accessible_sanitizer.should_not allow(:d)
142
+ accessible_sanitizer.should_not allow(:d, :admin)
143
+ end
144
+
145
+ it 'should return true if all are accessible' do
146
+ complex_sanitizer_one.should_not allow(:a)
147
+ complex_sanitizer_one.should_not allow(:b)
148
+ complex_sanitizer_one.should allow(:c)
149
+ complex_sanitizer_one.should allow(:d)
150
+ complex_sanitizer_one.should_not allow(:a, :admin)
151
+ complex_sanitizer_one.should allow(:b, :admin)
152
+ complex_sanitizer_one.should allow(:c, :admin)
153
+ complex_sanitizer_one.should allow(:d, :admin)
154
+ end
155
+
156
+ end
157
+
158
+ context 'sanitize_with_scope' do
159
+
160
+ it 'should remove protected attributes' do
161
+ complex_sanitizer_one.sanitize_with_scope(example_attributes_one, :default, nil).should only_have_attributes('c', 'd', 'e')
162
+ complex_sanitizer_one.sanitize_with_scope(example_attributes_one, :admin, nil).should only_have_attributes('b', 'c', 'd', 'e')
163
+ complex_sanitizer_two.sanitize_with_scope(example_attributes_one, :default, nil).should only_have_attributes('a', 'b')
164
+ end
165
+
166
+ end
167
+
168
+ context 'sanitize' do
169
+
170
+ subject { ScopedAttrAccessible::Sanitizer.new }
171
+
172
+ it 'should call with the default scope' do
173
+ mock(subject).sanitize_with_scope example_attributes_one, :default, anything
174
+ subject.sanitize example_attributes_one
175
+ end
176
+
177
+ it 'should let you provide a context' do
178
+ my_context = 42
179
+ mock(subject).sanitize_with_scope example_attributes_one, :default, my_context
180
+ subject.sanitize example_attributes_one, my_context
181
+ end
182
+
183
+ end
184
+
185
+ end
@@ -0,0 +1,15 @@
1
+ require 'spec_helper'
2
+ require 'active_model'
3
+
4
+ describe ScopedAttrAccessible do
5
+
6
+ it 'should automatically mix it in when hijacked' do
7
+ ScopedAttrAccessible.mixin!
8
+ klass = Class.new { include ActiveModel::MassAssignmentSecurity }
9
+ klass.ancestors.include?(ActiveModel::MassAssignmentSecurity).should be_true
10
+ klass.ancestors.include?(ScopedAttrAccessible::ActiveModelMixin).should be_true
11
+ klass.should respond_to(:with_sanitizer_scope)
12
+ klass.new.should respond_to(:current_sanitizer_scope)
13
+ end
14
+
15
+ end
@@ -0,0 +1,16 @@
1
+ ENV["RAILS_ENV"] ||= 'test'
2
+ $LOAD_PATH.unshift Pathname(__FILE__).dirname.dirname.join("lib").to_s
3
+
4
+ require 'bundler/setup'
5
+ Bundler.setup
6
+ Bundler.require :default, :development
7
+
8
+ require 'scoped_attr_accessible'
9
+ require 'rspec'
10
+
11
+ Dir[Pathname(__FILE__).dirname.join("support/**/*.rb")].each { |f| require f }
12
+
13
+ RSpec.configure do |config|
14
+ config.mock_with :rr
15
+ config.include CustomMatchers
16
+ end
@@ -0,0 +1,26 @@
1
+ module CustomMatchers
2
+ class OnlyHaveAttributesMatcher
3
+
4
+ def initialize(*attrs)
5
+ @attributes = attrs.map(&:to_s)
6
+ end
7
+
8
+ def matches?(target)
9
+ @target_attributes = target.keys.map(&:to_s)
10
+ @target_attributes == @attributes
11
+ end
12
+
13
+ def failure_message_for_should
14
+ "expected to only allow attributes #{@attributes.join(", ")}, allowed #{@target_attributes.join(", ")}"
15
+ end
16
+
17
+ def failure_message_for_should_not
18
+ "expected to not only allow attributes #{@attributes.join(", ")}, allowed #{@target_attributes.join(", ")}"
19
+ end
20
+
21
+ end
22
+
23
+ def only_have_attributes(*attrs)
24
+ OnlyHaveAttributesMatcher.new(*attrs)
25
+ end
26
+ end
metadata ADDED
@@ -0,0 +1,122 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: scoped_attr_accessible
3
+ version: !ruby/object:Gem::Version
4
+ hash: 27
5
+ prerelease: false
6
+ segments:
7
+ - 0
8
+ - 1
9
+ - 0
10
+ version: 0.1.0
11
+ platform: ruby
12
+ authors:
13
+ - Darcy Laycock
14
+ - Mario Visic
15
+ autorequire:
16
+ bindir: bin
17
+ cert_chain: []
18
+
19
+ date: 2010-10-11 00:00:00 +08:00
20
+ default_executable:
21
+ dependencies:
22
+ - !ruby/object:Gem::Dependency
23
+ name: activemodel
24
+ prerelease: false
25
+ requirement: &id001 !ruby/object:Gem::Requirement
26
+ none: false
27
+ requirements:
28
+ - - ~>
29
+ - !ruby/object:Gem::Version
30
+ hash: 7
31
+ segments:
32
+ - 3
33
+ - 0
34
+ version: "3.0"
35
+ type: :runtime
36
+ version_requirements: *id001
37
+ - !ruby/object:Gem::Dependency
38
+ name: rspec
39
+ prerelease: false
40
+ requirement: &id002 !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ~>
44
+ - !ruby/object:Gem::Version
45
+ hash: 3
46
+ segments:
47
+ - 2
48
+ - 0
49
+ version: "2.0"
50
+ type: :development
51
+ version_requirements: *id002
52
+ description: |-
53
+ scoped_attr_accessible is a plugin that makes it easy to scope the `attr_accessible` and `attr_protected`
54
+ methods on any library using ActiveModel's MassAssignmentSecurity module.
55
+ email: team+darcy+mario@thefrontiergroup.com.au
56
+ executables: []
57
+
58
+ extensions: []
59
+
60
+ extra_rdoc_files:
61
+ - LICENSE
62
+ - README.md
63
+ files:
64
+ - .bundle/config
65
+ - .gitignore
66
+ - .rspec
67
+ - .rvmrc
68
+ - Gemfile
69
+ - Gemfile.lock
70
+ - LICENSE
71
+ - README.md
72
+ - Rakefile
73
+ - VERSION
74
+ - autotest/discover.rb
75
+ - lib/scoped_attr_accessible.rb
76
+ - lib/scoped_attr_accessible/active_model_mixin.rb
77
+ - lib/scoped_attr_accessible/sanitizer.rb
78
+ - spec/scoped_attr_accessible/active_model_mixin_spec.rb
79
+ - spec/scoped_attr_accessible/sanitizer_spec.rb
80
+ - spec/scoped_attr_accessible_spec.rb
81
+ - spec/spec_helper.rb
82
+ - spec/support/custom_matchers.rb
83
+ has_rdoc: true
84
+ homepage: http://github.com/thefrontiergroup/scoped_attr_accessible
85
+ licenses: []
86
+
87
+ post_install_message:
88
+ rdoc_options:
89
+ - --charset=UTF-8
90
+ require_paths:
91
+ - lib
92
+ required_ruby_version: !ruby/object:Gem::Requirement
93
+ none: false
94
+ requirements:
95
+ - - ">="
96
+ - !ruby/object:Gem::Version
97
+ hash: 3
98
+ segments:
99
+ - 0
100
+ version: "0"
101
+ required_rubygems_version: !ruby/object:Gem::Requirement
102
+ none: false
103
+ requirements:
104
+ - - ">="
105
+ - !ruby/object:Gem::Version
106
+ hash: 3
107
+ segments:
108
+ - 0
109
+ version: "0"
110
+ requirements: []
111
+
112
+ rubyforge_project:
113
+ rubygems_version: 1.3.7
114
+ signing_key:
115
+ specification_version: 3
116
+ summary: Scoping for attr_accessible and attr_protected on ActiveModel objects.
117
+ test_files:
118
+ - spec/scoped_attr_accessible/active_model_mixin_spec.rb
119
+ - spec/scoped_attr_accessible/sanitizer_spec.rb
120
+ - spec/scoped_attr_accessible_spec.rb
121
+ - spec/spec_helper.rb
122
+ - spec/support/custom_matchers.rb