protector 0.1.0 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
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