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