protector 0.1.0 → 0.1.1

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 70b9f27e548ca491e3d9e72033d82ccc6c1ef9a6
4
- data.tar.gz: 174de9ede88655d225d7281e2d633cfd9c113cd4
3
+ metadata.gz: 4d35910dd47bbaec6c4fa77c49edb1643b7ab614
4
+ data.tar.gz: 6d7e0b11f3c750004281dc432372d8e6e9b01cad
5
5
  SHA512:
6
- metadata.gz: 7f86cb799de05ecb102b5c413492024be29a3d5b27a40e479c7e94e9afcfc7d47f8203e4e62d99f9ed406fa2943880acca7f79449341f3a6fc8ac480b8e59b59
7
- data.tar.gz: 4864da9f17564ee319ed2faa7891a7a5e30f336bd7a9966ffda20aeae96337e02148ebf196bd0f504ad14f4487ff84b3ede8e7580a8e6f1ac045779ba33176c8
6
+ metadata.gz: 41f684019bb41d363579165474abfc9b280bd9bbaad66bd034b4735fe1a1acb8404941dd9d3f269969e44945fa42b612bb393b508f3be2ed147b8a9878830e58
7
+ data.tar.gz: 2cbf62c1a0da1f6ae6e15d97677cd5594641bcda24987460648625fdad83a163fdf5f0c6e7c35b2041bbdef65ea3ec040a49fd01f4cc4ddfc1b9e7acd56e24c9
@@ -0,0 +1 @@
1
+ --markup=markdown
data/Gemfile CHANGED
@@ -2,6 +2,7 @@ source 'https://rubygems.org'
2
2
  gemspec
3
3
 
4
4
  gem 'rake'
5
+ gem 'colored'
5
6
  gem 'pry'
6
7
  gem 'rspec'
7
8
  gem 'guard'
data/README.md CHANGED
@@ -111,6 +111,8 @@ Protector is aware of associations. All the associations retrieved from restrict
111
111
 
112
112
  The access to `belongs_to` kind of association depends on corresponding foreign key readability.
113
113
 
114
+ Remember however that auto-restriction is only enabled for reading. Passing a model (or an array of those) to an association will not auto-restrict it. You should handle it manually.
115
+
114
116
  ## Eager Loading
115
117
 
116
118
  To take a long story short: it works and you are very likely to never notice changes it introduces to the process.
data/Rakefile CHANGED
@@ -11,3 +11,16 @@ desc 'Test the plugin under all supported Rails versions.'
11
11
  task :all => ["appraisal:install"] do |t|
12
12
  exec('rake appraisal spec')
13
13
  end
14
+
15
+ task :perf do
16
+ require 'protector'
17
+
18
+ Bundler.require
19
+
20
+ %w(ActiveRecord DataMapper Mongoid Sequel).each do |a|
21
+ if (a.constantize rescue nil)
22
+ load "perf/perf_helpers/boot.rb"
23
+ Perf.load a.underscore
24
+ end
25
+ end
26
+ end
@@ -3,6 +3,7 @@
3
3
  source "https://rubygems.org"
4
4
 
5
5
  gem "rake"
6
+ gem "colored"
6
7
  gem "pry"
7
8
  gem "rspec"
8
9
  gem "guard"
@@ -30,6 +30,7 @@ GEM
30
30
  arel (3.0.2)
31
31
  builder (3.0.4)
32
32
  coderay (1.0.9)
33
+ colored (1.2)
33
34
  diff-lcs (1.2.4)
34
35
  ffi (1.8.1)
35
36
  formatador (0.2.4)
@@ -80,6 +81,7 @@ DEPENDENCIES
80
81
  activerecord (= 3.2.9)
81
82
  activerecord-jdbcsqlite3-adapter!
82
83
  appraisal
84
+ colored
83
85
  guard
84
86
  guard-rspec
85
87
  jdbc-sqlite3
@@ -3,6 +3,7 @@
3
3
  source "https://rubygems.org"
4
4
 
5
5
  gem "rake"
6
+ gem "colored"
6
7
  gem "pry"
7
8
  gem "rspec"
8
9
  gem "guard"
@@ -40,6 +40,7 @@ GEM
40
40
  atomic (1.1.9-java)
41
41
  builder (3.1.4)
42
42
  coderay (1.0.9)
43
+ colored (1.2)
43
44
  diff-lcs (1.2.4)
44
45
  ffi (1.8.1)
45
46
  ffi (1.8.1-java)
@@ -103,6 +104,7 @@ DEPENDENCIES
103
104
  activerecord (= 4.0.0.rc1)
104
105
  activerecord-jdbcsqlite3-adapter!
105
106
  appraisal
107
+ colored
106
108
  guard
107
109
  guard-rspec
108
110
  jdbc-sqlite3
@@ -5,7 +5,9 @@ require 'protector/adapters/active_record/preloader'
5
5
 
6
6
  module Protector
7
7
  module Adapters
8
+ # ActiveRecord adapter
8
9
  module ActiveRecord
10
+ # YIP YIP! Monkey-Patch the ActiveRecord.
9
11
  def self.activate!
10
12
  ::ActiveRecord::Base.send :include, Protector::Adapters::ActiveRecord::Base
11
13
  ::ActiveRecord::Relation.send :include, Protector::Adapters::ActiveRecord::Relation
@@ -1,10 +1,12 @@
1
1
  module Protector
2
2
  module Adapters
3
3
  module ActiveRecord
4
+ # Patches `ActiveRecord::Associations::SingularAssociation` and `ActiveRecord::Associations::CollectionAssociation`
4
5
  module Association
5
6
  extend ActiveSupport::Concern
6
7
 
7
8
  included do
9
+ # AR 4 has renamed `scoped` to `scope`
8
10
  if method_defined?(:scope)
9
11
  alias_method_chain :scope, :protector
10
12
  else
@@ -13,6 +15,7 @@ module Protector
13
15
  end
14
16
  end
15
17
 
18
+ # Wraps every association with current subject
16
19
  def scope_with_protector(*args)
17
20
  scope_without_protector(*args).restrict!(owner.protector_subject)
18
21
  end
@@ -1,6 +1,7 @@
1
1
  module Protector
2
2
  module Adapters
3
3
  module ActiveRecord
4
+ # Pathces `ActiveRecord::Base`
4
5
  module Base
5
6
  extend ActiveSupport::Concern
6
7
 
@@ -8,6 +9,10 @@ module Protector
8
9
  include Protector::DSL::Base
9
10
  include Protector::DSL::Entry
10
11
 
12
+ ObjectSpace.each_object(Class).each do |c|
13
+ c.undefine_attribute_methods if c < self
14
+ end
15
+
11
16
  validate(on: :create) do
12
17
  return unless @protector_subject
13
18
  errors[:base] << I18n.t('protector.invalid') unless creatable?
@@ -23,6 +28,12 @@ module Protector
23
28
  destroyable?
24
29
  end
25
30
 
31
+ # Drops {Protector::DSL::Meta::Box} cache when subject changes
32
+ def restrict!(*args)
33
+ @protector_meta = nil
34
+ super
35
+ end
36
+
26
37
  if Gem::Version.new(::ActiveRecord::VERSION::STRING) < Gem::Version.new('4.0.0.rc1')
27
38
  def self.restrict!(subject)
28
39
  scoped.restrict!(subject)
@@ -43,6 +54,7 @@ module Protector
43
54
  end
44
55
 
45
56
  module ClassMethods
57
+ # Wraps every `.field` method with a check against {Protector::DSL::Meta::Box#readable?}
46
58
  def define_method_attribute(name)
47
59
  super
48
60
 
@@ -63,12 +75,13 @@ module Protector
63
75
  end
64
76
  end
65
77
 
78
+ # Storage for {Protector::DSL::Meta::Box}
66
79
  def protector_meta
67
80
  unless @protector_subject
68
81
  raise "Unprotected entity detected: use `restrict` method to protect it."
69
82
  end
70
83
 
71
- self.class.protector_meta.evaluate(
84
+ @protector_meta ||= self.class.protector_meta.evaluate(
72
85
  self.class,
73
86
  @protector_subject,
74
87
  self.class.column_names,
@@ -76,22 +89,26 @@ module Protector
76
89
  )
77
90
  end
78
91
 
92
+ # Checks if current model can be selected in the context of current subject
79
93
  def visible?
80
94
  protector_meta.relation.where(
81
95
  self.class.primary_key => id
82
96
  ).any?
83
97
  end
84
98
 
99
+ # Checks if current model can be created in the context of current subject
85
100
  def creatable?
86
101
  fields = HashWithIndifferentAccess[changed.map{|x| [x, read_attribute(x)]}]
87
102
  protector_meta.creatable?(fields)
88
103
  end
89
104
 
105
+ # Checks if current model can be updated in the context of current subject
90
106
  def updatable?
91
107
  fields = HashWithIndifferentAccess[changed.map{|x| [x, read_attribute(x)]}]
92
108
  protector_meta.updatable?(fields)
93
109
  end
94
110
 
111
+ # Checks if current model can be destroyed in the context of current subject
95
112
  def destroyable?
96
113
  protector_meta.destroyable?
97
114
  end
@@ -1,10 +1,13 @@
1
1
  module Protector
2
2
  module Adapters
3
3
  module ActiveRecord
4
+ # Patches `ActiveRecord::Associations::Preloader`
4
5
  module Preloader extend ActiveSupport::Concern
5
6
 
7
+ # Patches `ActiveRecord::Associations::Preloader::Association`
6
8
  module Association extend ActiveSupport::Concern
7
9
  included do
10
+ # AR 4 has renamed `scoped` to `scope`
8
11
  if method_defined?(:scope)
9
12
  alias_method_chain :scope, :protector
10
13
  else
@@ -13,12 +16,14 @@ module Protector
13
16
  end
14
17
  end
15
18
 
19
+ # Gets current subject of preloading association
16
20
  def protector_subject
17
21
  # Owners are always loaded from the single source
18
22
  # having same protector_subject
19
23
  owners.first.protector_subject
20
24
  end
21
25
 
26
+ # Restricts preloading association scope with subject of the owner
22
27
  def scope_with_protector(*args)
23
28
  return scope_without_protector unless protector_subject
24
29
 
@@ -1,6 +1,8 @@
1
1
  module Protector
2
2
  module Adapters
3
3
  module ActiveRecord
4
+
5
+ # Pathces `ActiveRecord::Relation`
4
6
  module Relation
5
7
  extend ActiveSupport::Concern
6
8
 
@@ -27,34 +29,47 @@ module Protector
27
29
  end
28
30
  end
29
31
 
32
+ # Gets {Protector::DSL::Meta::Box} of this relation
30
33
  def protector_meta
31
34
  # We don't seem to require columns here as well
32
35
  # @klass.protector_meta.evaluate(@klass, @protector_subject, @klass.column_names)
33
36
  @klass.protector_meta.evaluate(@klass, @protector_subject)
34
37
  end
35
38
 
39
+ # @note Unscoped relation drops properties and therefore should be re-restricted
36
40
  def unscoped
37
41
  super.restrict!(@protector_subject)
38
42
  end
39
43
 
44
+ # @note This is here cause `NullRelation` can return `nil` from `count`
40
45
  def count(*args)
41
46
  super || 0
42
47
  end
43
48
 
49
+ # @note This is here cause `NullRelation` can return `nil` from `sum`
44
50
  def sum(*args)
45
51
  super || 0
46
52
  end
47
53
 
54
+ # Merges current relation with restriction and calls real `calculate`
48
55
  def calculate(*args)
49
56
  return super unless @protector_subject
50
57
  merge(protector_meta.relation).unrestrict!.calculate *args
51
58
  end
52
59
 
60
+ # Merges current relation with restriction and calls real `exists?`
53
61
  def exists?(*args)
54
62
  return super unless @protector_subject
55
63
  merge(protector_meta.relation).unrestrict!.exists? *args
56
64
  end
57
65
 
66
+ # Patches current relation to fulfill restriction and call real `exec_queries`
67
+ #
68
+ # Patching includes:
69
+ #
70
+ # * turning `includes` into `preload`
71
+ # * delaying built-in preloading to the stage where selection is restricted
72
+ # * merging current relation with restriction
58
73
  def exec_queries_with_protector(*args)
59
74
  return exec_queries_without_protector unless @protector_subject
60
75
 
@@ -82,20 +97,18 @@ module Protector
82
97
  @records
83
98
  end
84
99
 
85
- #
86
- # This method swaps `includes` with `preload` and adds JOINs
87
- # to any table referenced from `where` (or manually with `reference`)
88
- #
100
+ # Swaps `includes` with `preload` and adds JOINs to any table referenced
101
+ # from `where` (or manually with `reference`)
89
102
  def protector_substitute_includes(relation)
90
103
  subject = @protector_subject
104
+
105
+ # Note that `includes_values` shares reference across relation diffs so
106
+ # it can not be modified safely and should be copied instead
91
107
  includes, relation.includes_values = relation.includes_values, []
92
108
 
93
109
  # We can not allow join-based eager loading for scoped associations
94
110
  # since actual filtering can differ for host model and joined relation.
95
111
  # Therefore we turn all `includes` into `preloads`.
96
- #
97
- # Note that `includes_values` shares reference across relation diffs so
98
- # it has to be COPIED not modified
99
112
  includes.each do |iv|
100
113
  protector_expand_include(iv).each do |ive|
101
114
  # First-level associations can stay JOINed if restriction scope
@@ -126,10 +139,22 @@ module Protector
126
139
  relation
127
140
  end
128
141
 
129
- #
142
+ # Checks whether current object can be eager loaded respecting
143
+ # protector flags
144
+ def eager_loading_with_protector?
145
+ flag = eager_loading_without_protector?
146
+ flag &&= !!@eager_loadable_when_protected unless @eager_loadable_when_protected.nil?
147
+ flag
148
+ end
149
+
130
150
  # Indexes `includes` format by actual entity class
131
- # Turns {foo: :bar} into [[Foo, :foo], [Bar, {foo: :bar}]
132
151
  #
152
+ # Turns `{foo: :bar}` into `[[Foo, :foo], [Bar, {foo: :bar}]`
153
+ #
154
+ # @param [Symbol, Array, Hash] inclusion Inclusion description in the AR format
155
+ # @param [Array] results Resulting set
156
+ # @param [Array] base Association path ([:foo, :bar])
157
+ # @param [Class] klass Base class
133
158
  def protector_expand_include(inclusion, results=[], base=[], klass=@klass)
134
159
  if inclusion.is_a?(Hash)
135
160
  protector_expand_include_hash(inclusion, results, base, klass)
@@ -149,18 +174,21 @@ module Protector
149
174
  results
150
175
  end
151
176
 
177
+ private
178
+
152
179
  def protector_expand_include_hash(inclusion, results=[], base=[], klass=@klass)
153
180
  inclusion.each do |key, value|
154
181
  model = klass.reflect_on_association(key.to_sym).klass
155
182
  value = [value] unless value.is_a?(Array)
183
+ nest = [key]+base
156
184
 
157
185
  value.each do |v|
158
186
  if v.is_a?(Hash)
159
- protector_expand_include_hash(v, results, [key]+base)
187
+ protector_expand_include_hash(v, results, nest)
160
188
  else
161
189
  results << [
162
190
  model.reflect_on_association(v.to_sym).klass,
163
- ([key]+base).inject(v){|a, n| { n => a } }
191
+ nest.inject(v){|a, n| { n => a } }
164
192
  ]
165
193
  end
166
194
  end
@@ -168,12 +196,6 @@ module Protector
168
196
  results << [model, base.inject(key){|a, n| { n => a } }]
169
197
  end
170
198
  end
171
-
172
- def eager_loading_with_protector?
173
- flag = eager_loading_without_protector?
174
- flag &&= !!@eager_loadable_when_protected unless @eager_loadable_when_protected.nil?
175
- flag
176
- end
177
199
  end
178
200
  end
179
201
  end
@@ -3,16 +3,19 @@ module Protector
3
3
  # DSL meta storage and evaluator
4
4
  class Meta
5
5
 
6
- # Evaluation result moved out of Meta to make it thread-safe
7
- # and incapsulate better
6
+ # Single DSL evaluation result
8
7
  class Box
9
8
  attr_accessor :access, :relation, :destroyable
10
9
 
10
+ # @param model [Class] The class of protected entity
11
+ # @param fields [Array<String>] All the fields the model has
12
+ # @param subject [Object] Restriction subject
13
+ # @param entry [Object] An instance of the model
14
+ # @param blocks [Array<Proc>] An array of `protect` blocks
11
15
  def initialize(model, fields, subject, entry, blocks)
12
16
  @model = model
13
17
  @fields = fields
14
- @access = {update: {}, view: {}, create: {}}.with_indifferent_access
15
- @access_keys = {}.with_indifferent_access
18
+ @access = {update: {}, view: {}, create: {}}
16
19
  @relation = false
17
20
  @destroyable = false
18
21
 
@@ -26,37 +29,86 @@ module Protector
26
29
  instance_exec &b
27
30
  end
28
31
  end
29
-
30
- @access.each{|k,v| @access_keys[k] = v.keys}
31
32
  end
32
33
 
34
+ # Checks whether protection with given subject
35
+ # has the selection scope defined
33
36
  def scoped?
34
37
  !!@relation
35
38
  end
36
39
 
40
+ # @group Protection DSL
41
+
42
+ # Activates the scope that selections will
43
+ # be filtered with
44
+ #
45
+ # @yield Calls given model methods before the selection
46
+ #
47
+ # @example
48
+ # protect do
49
+ # # You can select nothing!
50
+ # scope { none }
51
+ # end
37
52
  def scope(&block)
38
53
  @relation = @model.instance_eval(&block)
39
54
  end
40
55
 
56
+ # Enables action for given fields.
57
+ #
58
+ # Built-in possible actions are: `:view`, `:update`, `:create`.
59
+ # You can pass any other actions you want to use with {#can?} afterwards.
60
+ #
61
+ # **The method enables action for every field if `fields` splat is empty.**
62
+ # Use {#cannot} to exclude some of them afterwards.
63
+ #
64
+ # The list of fields can be given as a Hash. In this form you can pass `Range`
65
+ # or `Proc` as a value. First will make Protector check against value inclusion.
66
+ # The latter will make it evaluate given lambda (which is supposed to return true or false
67
+ # determining if the value should validate or not).
68
+ #
69
+ # @param action [Symbol] Action to allow
70
+ # @param fields [String, Hash, Array] Splat of fields to allow action with
71
+ #
72
+ # @see #can?
73
+ #
74
+ # @example
75
+ # protect do
76
+ # can :view # Can view any field
77
+ # can :view, 'f1' # Can view `f1` field
78
+ # can :view, %w(f2 f3) # Can view `f2`, `f3` fields
79
+ # can :update, f1: 1..2 # Can update f1 field with values between 1 and 2
80
+ #
81
+ # # Can create f1 field with value equal to 'olo'
82
+ # can :create, f1: lambda{|x| x == 'olo'}
83
+ # end
41
84
  def can(action, *fields)
42
85
  return @destroyable = true if action == :destroy
43
- return unless @access[action]
86
+ @access[action] = {} unless @access[action]
44
87
 
45
88
  if fields.size == 0
46
- @fields.each{|f| @access[action][f] = nil}
89
+ @fields.each{|f| @access[action][f.to_s] = nil}
47
90
  else
48
91
  fields.each do |a|
49
92
  if a.is_a?(Array)
50
- a.each{|f| @access[action][f] = nil}
93
+ a.each{|f| @access[action][f.to_s] = nil}
51
94
  elsif a.is_a?(Hash)
52
- @access[action].merge!(a)
95
+ @access[action].merge!(a.stringify_keys)
53
96
  else
54
- @access[action][a] = nil
97
+ @access[action][a.to_s] = nil
55
98
  end
56
99
  end
57
100
  end
58
101
  end
59
102
 
103
+ # Disables action for given fields.
104
+ #
105
+ # Works similar (but oppositely) to {#can}.
106
+ #
107
+ # @param action [Symbol] Action to disallow
108
+ # @param fields [String, Hash, Array] Splat of fields to disallow action with
109
+ #
110
+ # @see #can
111
+ # @see #can?
60
112
  def cannot(action, *fields)
61
113
  return @destroyable = false if action == :destroy
62
114
  return unless @access[action]
@@ -66,37 +118,54 @@ module Protector
66
118
  else
67
119
  fields.each do |a|
68
120
  if a.is_a?(Array)
69
- a.each{|f| @access[action].delete(f)}
121
+ a.each{|f| @access[action].delete(f.to_s)}
70
122
  else
71
- @access[action].delete(a)
123
+ @access[action].delete(a.to_s)
72
124
  end
73
125
  end
74
126
  end
75
127
  end
76
128
 
129
+ # @endgroup
130
+
131
+ # Checks whether given field of a model is readable in context of current subject
77
132
  def readable?(field)
78
- @access_keys[:view].include?(field.to_s)
133
+ @access[:view].has_key?(field)
79
134
  end
80
135
 
136
+ # Checks whether you can create a model with given field in context of current subject
81
137
  def creatable?(fields=false)
82
138
  modifiable? :create, fields
83
139
  end
84
140
 
141
+ # Checks whether you can update a model with given field in context of current subject
85
142
  def updatable?(fields=false)
86
143
  modifiable? :update, fields
87
144
  end
88
145
 
146
+ # Checks whether you can destroy a model in context of current subject
89
147
  def destroyable?
90
148
  @destroyable
91
149
  end
92
150
 
151
+ # Check whether you can perform custom action for given fields (or generally if no `field` given)
152
+ #
153
+ # @param [Symbol] action Action to check against
154
+ # @param [String] field Field to check against
155
+ def can?(action, field=false)
156
+ return false unless @access[action]
157
+ return !@access[action].empty? if field === false
158
+ @access[action].has_key?(field)
159
+ end
160
+
93
161
  private
94
162
 
95
163
  def modifiable?(part, fields)
96
- return false unless @access_keys[part].length > 0
164
+ keys = @access[part].keys
165
+ return false unless keys.length > 0
97
166
 
98
167
  if fields
99
- return false if (fields.keys - @access_keys[part]).length > 0
168
+ return false if (fields.keys - keys).length > 0
100
169
 
101
170
  fields.each do |k,v|
102
171
  case x = @access[part][k]
@@ -112,12 +181,24 @@ module Protector
112
181
  end
113
182
  end
114
183
 
184
+ # Storage for `protect` blocks
185
+ def blocks
186
+ @blocks ||= []
187
+ end
188
+
189
+ # Register another protection block
115
190
  def <<(block)
116
- (@blocks ||= []) << block
191
+ blocks << block
117
192
  end
118
193
 
194
+ # Calculate protection at the context of subject
195
+ #
196
+ # @param model [Class] The class of protected entity
197
+ # @param subject [Object] Restriction subject
198
+ # @param fields [Array<String>] All the fields the model has
199
+ # @param entry [Object] An instance of the model
119
200
  def evaluate(model, subject, fields=[], entry=nil)
120
- Box.new(model, fields, subject, entry, @blocks)
201
+ Box.new(model, fields, subject, entry, blocks)
121
202
  end
122
203
  end
123
204
 
@@ -128,11 +209,15 @@ module Protector
128
209
  attr_reader :protector_subject
129
210
  end
130
211
 
212
+ # Assigns restriction subject
213
+ #
214
+ # @param [Object] subject Subject to restrict against
131
215
  def restrict!(subject)
132
216
  @protector_subject = subject
133
217
  self
134
218
  end
135
219
 
220
+ # Clears restriction subject
136
221
  def unrestrict!
137
222
  @protector_subject = nil
138
223
  self
@@ -142,15 +227,18 @@ module Protector
142
227
  module Entry
143
228
  extend ActiveSupport::Concern
144
229
 
145
- included do
146
- class <<self
147
- attr_reader :protector_meta
148
- end
149
- end
150
-
151
230
  module ClassMethods
231
+ # Registers protection DSL block
232
+ # @yield [subject, instance] Evaluates conditions described in terms of {Protector::DSL::Meta::Box}.
233
+ # @yieldparam subject [Object] Subject that object was restricted with
234
+ # @yieldparam instance [Object] Reference to the object being restricted (can be nil)
152
235
  def protect(&block)
153
- (@protector_meta ||= Meta.new) << block
236
+ protector_meta << block
237
+ end
238
+
239
+ # Storage of {Protector::DSL::Meta}
240
+ def protector_meta
241
+ @protector_meta ||= Meta.new
154
242
  end
155
243
  end
156
244
  end
@@ -1,3 +1,4 @@
1
1
  module Protector
2
- VERSION = "0.1.0"
2
+ # Gem version
3
+ VERSION = "0.1.1"
3
4
  end
@@ -0,0 +1,48 @@
1
+ ### Connection
2
+
3
+ ActiveRecord::Schema.verbose = false
4
+ ActiveRecord::Base.establish_connection adapter: "sqlite3", database: ":memory:"
5
+
6
+ ActiveRecord::Base.instance_eval do
7
+ unless method_defined?(:none)
8
+ def none
9
+ where('1 = 0')
10
+ end
11
+ end
12
+
13
+ def every
14
+ where(nil)
15
+ end
16
+ end
17
+
18
+ ### Tables
19
+
20
+ [:dummies, :fluffies, :bobbies].each do |m|
21
+ ActiveRecord::Migration.create_table m do |t|
22
+ t.string :string
23
+ t.integer :number
24
+ t.text :text
25
+ t.belongs_to :dummy
26
+ t.timestamps
27
+ end
28
+ end
29
+
30
+ ActiveRecord::Migration.create_table(:loonies){|t| t.belongs_to :fluffy; t.string :string }
31
+
32
+ ### Classes
33
+
34
+ class Dummy < ActiveRecord::Base
35
+ has_many :fluffies
36
+ has_many :bobbies
37
+ end
38
+
39
+ class Fluffy < ActiveRecord::Base
40
+ belongs_to :dummy
41
+ has_one :loony
42
+ end
43
+
44
+ class Bobby < ActiveRecord::Base
45
+ end
46
+
47
+ class Loony < ActiveRecord::Base
48
+ end
@@ -0,0 +1,90 @@
1
+ migrate
2
+
3
+ seed do
4
+ 500.times do
5
+ d = Dummy.create! string: 'zomgstring', number: [999,777].sample, text: 'zomgtext'
6
+
7
+ 2.times do
8
+ f = Fluffy.create! string: 'zomgstring', number: [999,777].sample, text: 'zomgtext', dummy_id: d.id
9
+ b = Bobby.create! string: 'zomgstring', number: [999,777].sample, text: 'zomgtext', dummy_id: d.id
10
+ l = Loony.create! string: 'zomgstring', fluffy_id: f.id
11
+ end
12
+ end
13
+ end
14
+
15
+ activate do
16
+ Dummy.instance_eval do
17
+ protect do
18
+ scope { every }
19
+ can :view, :string
20
+ end
21
+ end
22
+
23
+ Fluffy.instance_eval do
24
+ protect do
25
+ scope { every }
26
+ can :view
27
+ end
28
+ end
29
+
30
+ # Define attributes methods
31
+ Dummy.first
32
+ end
33
+
34
+ benchmark 'Read from unprotected model (100k)' do
35
+ d = Dummy.first
36
+ 100_000.times { d.string }
37
+ end
38
+
39
+ benchmark 'Read open field (100k)' do
40
+ d = Dummy.first
41
+ d = d.restrict!('!') if activated?
42
+ 100_000.times { d.string }
43
+ end
44
+
45
+ benchmark 'Read nil field (100k)' do
46
+ d = Dummy.first
47
+ d = d.restrict!('!') if activated?
48
+ 100_000.times { d.text }
49
+ end
50
+
51
+ benchmark 'Check existance' do
52
+ scope = activated? ? Dummy.restrict!('!') : Dummy.every
53
+ 1000.times { scope.exists? }
54
+ end
55
+
56
+ benchmark 'Count' do
57
+ scope = Dummy.limit(1)
58
+ scope = scope.restrict!('!') if activated?
59
+ 1000.times { scope.count }
60
+ end
61
+
62
+ benchmark 'Select one' do
63
+ scope = Dummy.limit(1)
64
+ scope = scope.restrict!('!') if activated?
65
+ 1000.times { scope.to_a }
66
+ end
67
+
68
+ benchmark 'Select many' do
69
+ scope = Dummy.every
70
+ scope = scope.restrict!('!') if activated?
71
+ 1000.times { scope.to_a }
72
+ end
73
+
74
+ benchmark 'Select with eager loading' do
75
+ scope = Dummy.includes(:fluffies)
76
+ scope = scope.restrict!('!') if activated?
77
+ 1000.times { scope.to_a }
78
+ end
79
+
80
+ benchmark 'Select with filtered eager loading' do
81
+ scope = Dummy.includes(:fluffies).where(fluffies: {number: 999})
82
+ scope = scope.restrict!('!') if activated?
83
+ 1000.times { scope.to_a }
84
+ end
85
+
86
+ benchmark 'Select with mixed eager loading' do
87
+ scope = Dummy.includes(:fluffies, :bobbies).where(fluffies: {number: 999})
88
+ scope = scope.restrict!('!') if activated?
89
+ 1000.times { scope.to_a }
90
+ end
@@ -0,0 +1,95 @@
1
+ class Perf
2
+ def self.load(adapter)
3
+ perf = Perf.new(adapter.camelize)
4
+ base = Pathname.new(File.expand_path '../..', __FILE__)
5
+ file = base.join(adapter+'.rb').to_s
6
+ perf.instance_eval File.read(file), file
7
+ perf.run!
8
+ end
9
+
10
+ def initialize(adapter)
11
+ @blocks = {}
12
+ @adapter = adapter
13
+ @activated = false
14
+ end
15
+
16
+ def migrate
17
+ puts
18
+ print "Running with #{@adapter}: migrating... ".yellow
19
+
20
+ load "migrations/#{@adapter.underscore}.rb"
21
+
22
+ puts "Done.".yellow
23
+ end
24
+
25
+ def seed
26
+ print "Seeding... ".yellow
27
+ yield if block_given?
28
+ puts "Done".yellow
29
+ end
30
+
31
+ def activate(&block)
32
+ @activation = block
33
+ end
34
+
35
+ def benchmark(subject, &block)
36
+ @blocks[subject] = block
37
+ end
38
+
39
+ def activated?
40
+ @activated
41
+ end
42
+
43
+ def run!
44
+ results = {}
45
+
46
+ results[:off] = run_state('disabled', :red)
47
+
48
+ Protector::Adapters.const_get(@adapter).activate!
49
+ @activation.call
50
+ @activated = true
51
+
52
+ results[:on] = run_state('enabled', :green)
53
+
54
+ print_block "Total".blue do
55
+ results[:off].keys.each do |k|
56
+ off = results[:off][k]
57
+ on = results[:on][k]
58
+
59
+ print_result k, sprintf("%8s / %8s (%s)", off, on, (on / off).round(2))
60
+ end
61
+ end
62
+ end
63
+
64
+ private
65
+
66
+ def run_state(state, color)
67
+ data = {}
68
+
69
+ print_block "Protector #{state.send color}" do
70
+ @blocks.each do |s, b|
71
+ data[s] = Benchmark.realtime(&b)
72
+ print_result s, data[s].to_s
73
+ end
74
+ end
75
+
76
+ data
77
+ end
78
+
79
+ def print_result(title, time)
80
+ print title.yellow
81
+ print "..."
82
+ puts sprintf("%#{100-title.length-3}s", time)
83
+ end
84
+
85
+ def print_block(title)
86
+ puts
87
+ puts title
88
+ puts "-"*100
89
+
90
+ yield
91
+
92
+ puts "-"*100
93
+ puts
94
+ end
95
+ end
@@ -1,84 +1,13 @@
1
1
  require 'spec_helpers/boot'
2
2
 
3
3
  if defined?(ActiveRecord)
4
-
5
- RSpec::Matchers.define :invalidate do
6
- match do |actual|
7
- actual.save.should == false
8
- actual.errors[:base].should == ["Access denied"]
9
- end
10
- end
11
-
12
- RSpec::Matchers.define :validate do
13
- match do |actual|
14
- actual.class.transaction do
15
- actual.save.should == true
16
- raise ActiveRecord::Rollback
17
- end
18
-
19
- true
20
- end
21
- end
22
-
23
- def log!
24
- around(:each) do |e|
25
- ActiveRecord::Base.logger = Logger.new(STDOUT)
26
- e.run
27
- ActiveRecord::Base.logger = nil
28
- end
29
- end
4
+ load 'spec_helpers/adapters/active_record.rb'
30
5
 
31
6
  describe Protector::Adapters::ActiveRecord do
32
7
  before(:all) do
33
- ActiveRecord::Schema.verbose = false
34
- ActiveRecord::Base.establish_connection adapter: "sqlite3", database: ":memory:"
35
-
36
- [:dummies, :fluffies, :bobbies].each do |m|
37
- ActiveRecord::Migration.create_table m do |t|
38
- t.string :string
39
- t.integer :number
40
- t.text :text
41
- t.belongs_to :dummy
42
- t.timestamps
43
- end
44
- end
45
-
46
- ActiveRecord::Migration.create_table(:loonies){|t| t.belongs_to :fluffy; t.string :string }
47
-
48
- Protector::Adapters::ActiveRecord.activate!
49
-
50
- module Tester extend ActiveSupport::Concern
51
- included do
52
- protect do |x|
53
- scope{ where('1=0') } if x == '-'
54
- scope{ where(number: 999) } if x == '+'
55
-
56
- can :view, :dummy_id unless x == '-'
57
- end
58
-
59
- scope :none, where('1 = 0') unless respond_to?(:none)
60
- end
61
- end
62
-
63
- class Dummy < ActiveRecord::Base
64
- include Tester
65
- has_many :fluffies
66
- has_many :bobbies
67
- end
68
-
69
- class Fluffy < ActiveRecord::Base
70
- include Tester
71
- belongs_to :dummy
72
- has_one :loony
73
- end
74
-
75
- class Bobby < ActiveRecord::Base
76
- protect do; end
77
- end
8
+ load 'migrations/active_record.rb'
78
9
 
79
- class Loony < ActiveRecord::Base
80
- protect do; end
81
- end
10
+ [Dummy, Fluffy].each{|c| c.send :include, ProtectionCase}
82
11
 
83
12
  Dummy.create! string: 'zomgstring', number: 999, text: 'zomgtext'
84
13
  Dummy.create! string: 'zomgstring', number: 999, text: 'zomgtext'
@@ -95,6 +24,9 @@ if defined?(ActiveRecord)
95
24
  Fluffy.all.each{|f| Loony.create! fluffy_id: f.id, string: 'zomgstring' }
96
25
  end
97
26
 
27
+ #
28
+ # Model instance
29
+ #
98
30
  describe Protector::Adapters::ActiveRecord::Base do
99
31
  let(:dummy) do
100
32
  Class.new(ActiveRecord::Base) do
@@ -114,47 +46,11 @@ if defined?(ActiveRecord)
114
46
  end
115
47
 
116
48
  it_behaves_like "a model"
117
-
118
- describe "eager loading" do
119
- it "scopes" do
120
- d = Dummy.restrict!('+').includes(:fluffies)
121
- d.length.should == 2
122
- d.first.fluffies.length.should == 1
123
- end
124
-
125
- context "joined to filtered association" do
126
- it "scopes" do
127
- d = Dummy.restrict!('+').includes(:fluffies).where(fluffies: {number: 777})
128
- d.length.should == 2
129
- d.first.fluffies.length.should == 1
130
- end
131
- end
132
-
133
- context "joined to plain association" do
134
- it "scopes" do
135
- d = Dummy.restrict!('+').includes(:bobbies, :fluffies).where(
136
- bobbies: {number: 777}, fluffies: {number: 777}
137
- )
138
- d.length.should == 2
139
- d.first.fluffies.length.should == 1
140
- d.first.bobbies.length.should == 1
141
- end
142
- end
143
-
144
- context "with complex include" do
145
- it "scopes" do
146
- d = Dummy.restrict!('+').includes(fluffies: :loony).where(
147
- fluffies: {number: 777},
148
- loonies: {string: 'zomgstring'}
149
- )
150
- d.length.should == 2
151
- d.first.fluffies.length.should == 1
152
- d.first.fluffies.first.loony.should be_a_kind_of(Loony)
153
- end
154
- end
155
- end
156
49
  end
157
50
 
51
+ #
52
+ # Model scope
53
+ #
158
54
  describe Protector::Adapters::ActiveRecord::Relation do
159
55
  it "includes" do
160
56
  Dummy.none.ancestors.should include(Protector::Adapters::ActiveRecord::Base)
@@ -217,6 +113,50 @@ if defined?(ActiveRecord)
217
113
  end
218
114
  end
219
115
  end
116
+
117
+ #
118
+ # Eager loading
119
+ #
120
+ describe Protector::Adapters::ActiveRecord::Preloader do
121
+ describe "eager loading" do
122
+ it "scopes" do
123
+ d = Dummy.restrict!('+').includes(:fluffies)
124
+ d.length.should == 2
125
+ d.first.fluffies.length.should == 1
126
+ end
127
+
128
+ context "joined to filtered association" do
129
+ it "scopes" do
130
+ d = Dummy.restrict!('+').includes(:fluffies).where(fluffies: {number: 777})
131
+ d.length.should == 2
132
+ d.first.fluffies.length.should == 1
133
+ end
134
+ end
135
+
136
+ context "joined to plain association" do
137
+ it "scopes" do
138
+ d = Dummy.restrict!('+').includes(:bobbies, :fluffies).where(
139
+ bobbies: {number: 777}, fluffies: {number: 777}
140
+ )
141
+ d.length.should == 2
142
+ d.first.fluffies.length.should == 1
143
+ d.first.bobbies.length.should == 1
144
+ end
145
+ end
146
+
147
+ context "with complex include" do
148
+ it "scopes" do
149
+ d = Dummy.restrict!('+').includes(fluffies: :loony).where(
150
+ fluffies: {number: 777},
151
+ loonies: {string: 'zomgstring'}
152
+ )
153
+ d.length.should == 2
154
+ d.first.fluffies.length.should == 1
155
+ d.first.fluffies.first.loony.should be_a_kind_of(Loony)
156
+ end
157
+ end
158
+ end
159
+ end
220
160
  end
221
161
 
222
162
  end
@@ -81,19 +81,19 @@ describe Protector::DSL do
81
81
  it "sets access" do
82
82
  data = @meta.evaluate(nil, 'user', %w(field1 field2 field3 field4 field5), 'entry')
83
83
  data.access.should == {
84
- "update" => {
84
+ update: {
85
85
  "field1" => nil,
86
86
  "field2" => nil,
87
87
  "field3" => nil,
88
88
  "field4" => 0..5,
89
89
  "field5" => l
90
90
  },
91
- "view" => {
91
+ view: {
92
92
  "field1" => nil,
93
93
  "field2" => nil,
94
94
  "field3" => nil
95
95
  },
96
- "create" => {}
96
+ create: {}
97
97
  }
98
98
  end
99
99
 
@@ -0,0 +1,25 @@
1
+ RSpec::Matchers.define :invalidate do
2
+ match do |actual|
3
+ actual.save.should == false
4
+ actual.errors[:base].should == ["Access denied"]
5
+ end
6
+ end
7
+
8
+ RSpec::Matchers.define :validate do
9
+ match do |actual|
10
+ actual.class.transaction do
11
+ actual.save.should == true
12
+ raise ActiveRecord::Rollback
13
+ end
14
+
15
+ true
16
+ end
17
+ end
18
+
19
+ def log!
20
+ around(:each) do |e|
21
+ ActiveRecord::Base.logger = Logger.new(STDOUT)
22
+ e.run
23
+ ActiveRecord::Base.logger = nil
24
+ end
25
+ end
@@ -1,8 +1,20 @@
1
+ Bundler.require
2
+
1
3
  require 'protector'
4
+ require_relative 'examples/model'
2
5
 
3
- Bundler.require
6
+ module ProtectionCase
7
+ extend ActiveSupport::Concern
8
+
9
+ included do
10
+ protect do |x|
11
+ scope{ where('1=0') } if x == '-'
12
+ scope{ where(number: 999) } if x == '+'
4
13
 
5
- require_relative 'model'
14
+ can :view, :dummy_id unless x == '-'
15
+ end
16
+ end
17
+ end
6
18
 
7
19
  RSpec.configure do |config|
8
20
  config.treat_symbols_as_metadata_keys_with_true_values = true
@@ -20,25 +20,13 @@ shared_examples_for "a model" do
20
20
  meta.access[:update].should == fields
21
21
  end
22
22
 
23
- describe "association" do
24
- context "(has_many)" do
25
- it "loads" do
26
- Dummy.first.restrict!('!').fluffies.length.should == 2
27
- Dummy.first.restrict!('+').fluffies.length.should == 1
28
- Dummy.first.restrict!('-').fluffies.empty?.should == true
29
- end
30
- end
31
-
32
- context "(belongs_to)" do
33
- it "passes subject" do
34
- Fluffy.first.restrict!('!').dummy.protector_subject.should == '!'
35
- end
23
+ it "drops meta on restrict" do
24
+ d = Dummy.first
36
25
 
37
- it "loads" do
38
- Fluffy.first.restrict!('!').dummy.should be_a_kind_of(Dummy)
39
- Fluffy.first.restrict!('-').dummy.should == nil
40
- end
41
- end
26
+ d.restrict!('!').protector_meta
27
+ d.instance_variable_get('@protector_meta').should_not == nil
28
+ d.restrict!('!')
29
+ d.instance_variable_get('@protector_meta').should == nil
42
30
  end
43
31
 
44
32
  describe "visibility" do
@@ -51,6 +39,9 @@ shared_examples_for "a model" do
51
39
  end
52
40
  end
53
41
 
42
+ #
43
+ # Reading
44
+ #
54
45
  describe "readability" do
55
46
  it "hides fields" do
56
47
  dummy.instance_eval do
@@ -67,6 +58,9 @@ shared_examples_for "a model" do
67
58
  end
68
59
  end
69
60
 
61
+ #
62
+ # Creating
63
+ #
70
64
  describe "creatability" do
71
65
  context "with empty meta" do
72
66
  before(:each) do
@@ -177,6 +171,9 @@ shared_examples_for "a model" do
177
171
  end
178
172
  end
179
173
 
174
+ #
175
+ # Updating
176
+ #
180
177
  describe "updatability" do
181
178
  context "with empty meta" do
182
179
  before(:each) do
@@ -301,6 +298,9 @@ shared_examples_for "a model" do
301
298
  end
302
299
  end
303
300
 
301
+ #
302
+ # Destroying
303
+ #
304
304
  describe "destroyability" do
305
305
  it "marks blocked" do
306
306
  dummy.instance_eval do
@@ -336,4 +336,28 @@ shared_examples_for "a model" do
336
336
  d.destroyed?.should == true
337
337
  end
338
338
  end
339
+
340
+ #
341
+ # Associations
342
+ #
343
+ describe "association" do
344
+ context "(has_many)" do
345
+ it "loads" do
346
+ Dummy.first.restrict!('!').fluffies.length.should == 2
347
+ Dummy.first.restrict!('+').fluffies.length.should == 1
348
+ Dummy.first.restrict!('-').fluffies.empty?.should == true
349
+ end
350
+ end
351
+
352
+ context "(belongs_to)" do
353
+ it "passes subject" do
354
+ Fluffy.first.restrict!('!').dummy.protector_subject.should == '!'
355
+ end
356
+
357
+ it "loads" do
358
+ Fluffy.first.restrict!('!').dummy.should be_a_kind_of(Dummy)
359
+ Fluffy.first.restrict!('-').dummy.should == nil
360
+ end
361
+ end
362
+ end
339
363
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: protector
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Boris Staal
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2013-05-29 00:00:00.000000000 Z
11
+ date: 2013-06-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -49,6 +49,7 @@ files:
49
49
  - .gitignore
50
50
  - .rspec
51
51
  - .travis.yml
52
+ - .yardopts
52
53
  - Appraisals
53
54
  - Gemfile
54
55
  - LICENSE.txt
@@ -67,11 +68,15 @@ files:
67
68
  - lib/protector/dsl.rb
68
69
  - lib/protector/version.rb
69
70
  - locales/en.yml
71
+ - migrations/active_record.rb
72
+ - perf/active_record.rb
73
+ - perf/perf_helpers/boot.rb
70
74
  - protector.gemspec
71
75
  - spec/lib/adapters/active_record_spec.rb
72
76
  - spec/lib/dsl_spec.rb
77
+ - spec/spec_helpers/adapters/active_record.rb
73
78
  - spec/spec_helpers/boot.rb
74
- - spec/spec_helpers/model.rb
79
+ - spec/spec_helpers/examples/model.rb
75
80
  homepage: https://github.com/inossidabile/protector
76
81
  licenses:
77
82
  - MIT
@@ -100,5 +105,6 @@ summary: 'Protector is a successor to the Heimdallr gem: it hits the same goals
100
105
  test_files:
101
106
  - spec/lib/adapters/active_record_spec.rb
102
107
  - spec/lib/dsl_spec.rb
108
+ - spec/spec_helpers/adapters/active_record.rb
103
109
  - spec/spec_helpers/boot.rb
104
- - spec/spec_helpers/model.rb
110
+ - spec/spec_helpers/examples/model.rb