protector 0.1.1 → 0.2.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Appraisals +8 -0
- data/Gemfile +5 -1
- data/README.md +16 -7
- data/gemfiles/AR_3.2.gemfile +3 -1
- data/gemfiles/AR_3.2.gemfile.lock +33 -2
- data/gemfiles/AR_4.gemfile +3 -1
- data/gemfiles/AR_4.gemfile.lock +18 -1
- data/gemfiles/Mongoid.gemfile +17 -0
- data/gemfiles/Mongoid.gemfile.lock +112 -0
- data/gemfiles/Sequel.gemfile +18 -0
- data/gemfiles/Sequel.gemfile.lock +103 -0
- data/lib/protector/adapters/active_record/base.rb +15 -17
- data/lib/protector/adapters/active_record/relation.rb +20 -61
- data/lib/protector/adapters/sequel/dataset.rb +66 -0
- data/lib/protector/adapters/sequel/eager_graph_loader.rb +24 -0
- data/lib/protector/adapters/sequel/model.rb +99 -0
- data/lib/protector/adapters/sequel.rb +17 -0
- data/lib/protector/dsl.rb +5 -2
- data/lib/protector/version.rb +1 -1
- data/lib/protector.rb +3 -1
- data/migrations/active_record.rb +0 -1
- data/migrations/sequel.rb +49 -0
- data/perf/{active_record.rb → active_record_perf.rb} +2 -0
- data/perf/perf_helpers/boot.rb +20 -2
- data/perf/sequel_perf.rb +84 -0
- data/spec/lib/adapters/active_record_spec.rb +17 -4
- data/spec/lib/adapters/sequel_spec.rb +156 -0
- data/spec/spec_helpers/adapters/active_record.rb +34 -0
- data/spec/spec_helpers/adapters/sequel.rb +64 -0
- data/spec/spec_helpers/boot.rb +3 -13
- data/spec/spec_helpers/examples/model.rb +20 -20
- metadata +17 -3
@@ -1,8 +1,7 @@
|
|
1
1
|
module Protector
|
2
2
|
module Adapters
|
3
3
|
module ActiveRecord
|
4
|
-
|
5
|
-
# Pathces `ActiveRecord::Relation`
|
4
|
+
# Patches `ActiveRecord::Relation`
|
6
5
|
module Relation
|
7
6
|
extend ActiveSupport::Concern
|
8
7
|
|
@@ -10,9 +9,6 @@ module Protector
|
|
10
9
|
include Protector::DSL::Base
|
11
10
|
|
12
11
|
alias_method_chain :exec_queries, :protector
|
13
|
-
alias_method_chain :eager_loading?, :protector
|
14
|
-
|
15
|
-
attr_accessor :eager_loadable_when_protected
|
16
12
|
|
17
13
|
# AR 3.2 workaround. Come on, guys... SQL parsing :(
|
18
14
|
unless method_defined?(:references_values)
|
@@ -67,86 +63,49 @@ module Protector
|
|
67
63
|
#
|
68
64
|
# Patching includes:
|
69
65
|
#
|
70
|
-
# * turning `includes` into `preload`
|
66
|
+
# * turning `includes` (that are not referenced for eager loading) into `preload`
|
71
67
|
# * delaying built-in preloading to the stage where selection is restricted
|
72
|
-
# * merging current relation with restriction
|
68
|
+
# * merging current relation with restriction (of self and every eager association)
|
73
69
|
def exec_queries_with_protector(*args)
|
70
|
+
return @records if loaded?
|
74
71
|
return exec_queries_without_protector unless @protector_subject
|
75
72
|
|
76
73
|
subject = @protector_subject
|
77
74
|
relation = merge(protector_meta.relation).unrestrict!
|
78
|
-
|
79
75
|
relation = protector_substitute_includes(relation)
|
80
76
|
|
81
|
-
# We should explicitly allow/deny eager loading now that we know
|
82
|
-
# if we can use it
|
83
|
-
relation.eager_loadable_when_protected = relation.includes_values.any?
|
84
|
-
|
85
77
|
# Preserve associations from internal loading. We are going to handle that
|
86
78
|
# ourselves respecting security scopes FTW!
|
87
79
|
associations, relation.preload_values = relation.preload_values, []
|
88
80
|
|
89
|
-
@records = relation.send(:exec_queries).each{|
|
81
|
+
@records = relation.send(:exec_queries).each{|record| record.restrict!(subject)}
|
90
82
|
|
91
83
|
# Now we have @records restricted properly so let's preload associations!
|
92
|
-
associations.each do |
|
93
|
-
::ActiveRecord::Associations::Preloader.new(@records,
|
84
|
+
associations.each do |association|
|
85
|
+
::ActiveRecord::Associations::Preloader.new(@records, association).run
|
94
86
|
end
|
95
87
|
|
96
88
|
@loaded = true
|
97
89
|
@records
|
98
90
|
end
|
99
91
|
|
100
|
-
# Swaps `includes` with `preload`
|
101
|
-
#
|
92
|
+
# Swaps `includes` with `preload` whether it's not referenced or merges
|
93
|
+
# security scope of proper class otherwise
|
102
94
|
def protector_substitute_includes(relation)
|
103
95
|
subject = @protector_subject
|
104
96
|
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
# We can not allow join-based eager loading for scoped associations
|
110
|
-
# since actual filtering can differ for host model and joined relation.
|
111
|
-
# Therefore we turn all `includes` into `preloads`.
|
112
|
-
includes.each do |iv|
|
113
|
-
protector_expand_include(iv).each do |ive|
|
114
|
-
# First-level associations can stay JOINed if restriction scope
|
115
|
-
# is absent. Checking deep associations would make us to check
|
116
|
-
# every parent. This should probably be done sometimes :\
|
117
|
-
meta = ive[0].protector_meta.evaluate(ive[0], subject) unless ive[1].is_a?(Hash)
|
118
|
-
|
119
|
-
# We leave unscoped restrictions as `includes`
|
120
|
-
# but turn scoped ones into `preloads`
|
121
|
-
if meta && !meta.scoped?
|
122
|
-
relation.includes!(ive[1])
|
123
|
-
else
|
124
|
-
if relation.references_values.include?(ive[0].table_name)
|
125
|
-
if relation.respond_to?(:joins!)
|
126
|
-
relation.joins!(ive[1])
|
127
|
-
else
|
128
|
-
relation = relation.joins(ive[1])
|
129
|
-
end
|
130
|
-
end
|
131
|
-
|
132
|
-
# AR 3.2 Y U NO HAVE BANG RELATION MODIFIERS
|
133
|
-
relation.preload_values << ive[1]
|
134
|
-
false
|
135
|
-
end
|
97
|
+
if eager_loading?
|
98
|
+
protector_expand_inclusion(includes_values + eager_load_values).each do |klass, path|
|
99
|
+
relation = relation.merge(klass.protector_meta.evaluate(klass, subject).relation)
|
136
100
|
end
|
101
|
+
else
|
102
|
+
relation.preload_values += includes_values
|
103
|
+
relation.includes_values = []
|
137
104
|
end
|
138
105
|
|
139
106
|
relation
|
140
107
|
end
|
141
108
|
|
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
|
-
|
150
109
|
# Indexes `includes` format by actual entity class
|
151
110
|
#
|
152
111
|
# Turns `{foo: :bar}` into `[[Foo, :foo], [Bar, {foo: :bar}]`
|
@@ -155,13 +114,13 @@ module Protector
|
|
155
114
|
# @param [Array] results Resulting set
|
156
115
|
# @param [Array] base Association path ([:foo, :bar])
|
157
116
|
# @param [Class] klass Base class
|
158
|
-
def
|
117
|
+
def protector_expand_inclusion(inclusion, results=[], base=[], klass=@klass)
|
159
118
|
if inclusion.is_a?(Hash)
|
160
|
-
|
119
|
+
protector_expand_inclusion_hash(inclusion, results, base, klass)
|
161
120
|
else
|
162
121
|
Array(inclusion).each do |i|
|
163
122
|
if i.is_a?(Hash)
|
164
|
-
|
123
|
+
protector_expand_inclusion_hash(i, results, base, klass)
|
165
124
|
else
|
166
125
|
results << [
|
167
126
|
klass.reflect_on_association(i.to_sym).klass,
|
@@ -176,7 +135,7 @@ module Protector
|
|
176
135
|
|
177
136
|
private
|
178
137
|
|
179
|
-
def
|
138
|
+
def protector_expand_inclusion_hash(inclusion, results=[], base=[], klass=@klass)
|
180
139
|
inclusion.each do |key, value|
|
181
140
|
model = klass.reflect_on_association(key.to_sym).klass
|
182
141
|
value = [value] unless value.is_a?(Array)
|
@@ -184,7 +143,7 @@ module Protector
|
|
184
143
|
|
185
144
|
value.each do |v|
|
186
145
|
if v.is_a?(Hash)
|
187
|
-
|
146
|
+
protector_expand_inclusion_hash(v, results, nest)
|
188
147
|
else
|
189
148
|
results << [
|
190
149
|
model.reflect_on_association(v.to_sym).klass,
|
@@ -0,0 +1,66 @@
|
|
1
|
+
module Protector
|
2
|
+
module Adapters
|
3
|
+
module Sequel
|
4
|
+
# Patches `Sequel::Dataset`
|
5
|
+
module Dataset extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
# Wrapper for the Dataset `row_proc` adding restriction function
|
8
|
+
class Restrictor
|
9
|
+
attr_accessor :subject
|
10
|
+
attr_accessor :mutator
|
11
|
+
|
12
|
+
def initialize(subject, mutator)
|
13
|
+
@subject = subject
|
14
|
+
@mutator = mutator
|
15
|
+
end
|
16
|
+
|
17
|
+
# Mutate entity through `row_proc` if available and then protect
|
18
|
+
#
|
19
|
+
# @param entity [Object] Entity coming from Dataset
|
20
|
+
def call(entity)
|
21
|
+
entity = mutator.call(entity) if mutator
|
22
|
+
return entity if !entity.respond_to?(:restrict!)
|
23
|
+
entity.restrict!(@subject)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
included do |klass|
|
28
|
+
include Protector::DSL::Base
|
29
|
+
|
30
|
+
alias_method_chain :each, :protector
|
31
|
+
end
|
32
|
+
|
33
|
+
# Gets {Protector::DSL::Meta::Box} of this dataset
|
34
|
+
def protector_meta
|
35
|
+
model.protector_meta.evaluate(model, @protector_subject)
|
36
|
+
end
|
37
|
+
|
38
|
+
# Substitutes `row_proc` with {Protector} and injects protection scope
|
39
|
+
def each_with_protector(*args, &block)
|
40
|
+
return each_without_protector(*args, &block) if !@protector_subject
|
41
|
+
|
42
|
+
relation = protector_defend_graph(clone, @protector_subject)
|
43
|
+
relation = relation.instance_eval(&protector_meta.scope_proc) if protector_meta.scoped?
|
44
|
+
|
45
|
+
relation.row_proc = Restrictor.new(@protector_subject, relation.row_proc)
|
46
|
+
relation.each_without_protector(*args, &block)
|
47
|
+
end
|
48
|
+
|
49
|
+
# Injects protection scope for every joined graph association
|
50
|
+
def protector_defend_graph(relation, subject)
|
51
|
+
return relation unless @opts[:eager_graph]
|
52
|
+
|
53
|
+
@opts[:eager_graph][:reflections].each do |association, reflection|
|
54
|
+
model = reflection[:cache][:class] if reflection[:cache].is_a?(Hash) && reflection[:cache][:class]
|
55
|
+
model = reflection[:class_name].constantize unless model
|
56
|
+
meta = model.protector_meta.evaluate(model, subject)
|
57
|
+
|
58
|
+
relation = relation.instance_eval(&meta.scope_proc) if meta.scoped?
|
59
|
+
end
|
60
|
+
|
61
|
+
relation
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Protector
|
2
|
+
module Adapters
|
3
|
+
module Sequel
|
4
|
+
# Patches `Sequel::Model::Associations::EagerGraphLoader`
|
5
|
+
module EagerGraphLoader extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
included do
|
8
|
+
alias_method_chain :initialize, :protector
|
9
|
+
end
|
10
|
+
|
11
|
+
def initialize_with_protector(dataset)
|
12
|
+
initialize_without_protector(dataset)
|
13
|
+
|
14
|
+
if dataset.protector_subject
|
15
|
+
@row_procs.each do |k,v|
|
16
|
+
@row_procs[k] = Dataset::Restrictor.new(dataset.protector_subject, v)
|
17
|
+
@ta_map[k][1] = @row_procs[k] if @ta_map.has_key?(k)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
module Protector
|
2
|
+
module Adapters
|
3
|
+
module Sequel
|
4
|
+
# Patches `Sequel::Model`
|
5
|
+
module Model extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
included do
|
8
|
+
include Protector::DSL::Base
|
9
|
+
include Protector::DSL::Entry
|
10
|
+
|
11
|
+
# Drops {Protector::DSL::Meta::Box} cache when subject changes
|
12
|
+
def restrict!(*args)
|
13
|
+
@protector_meta = nil
|
14
|
+
super
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
module ClassMethods
|
19
|
+
# Gets default restricted `Dataset`
|
20
|
+
def restrict!(subject)
|
21
|
+
dataset.clone.restrict! subject
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# Storage for {Protector::DSL::Meta::Box}
|
26
|
+
def protector_meta
|
27
|
+
@protector_meta ||= self.class.protector_meta.evaluate(
|
28
|
+
self.class,
|
29
|
+
@protector_subject,
|
30
|
+
self.class.columns,
|
31
|
+
self
|
32
|
+
)
|
33
|
+
end
|
34
|
+
|
35
|
+
# Checks if current model can be selected in the context of current subject
|
36
|
+
def visible?
|
37
|
+
protector_meta.relation.where(pk_hash).any?
|
38
|
+
end
|
39
|
+
|
40
|
+
# Checks if current model can be created in the context of current subject
|
41
|
+
def creatable?
|
42
|
+
fields = HashWithIndifferentAccess[keys.map{|x| [x.to_s, @values[x]]}]
|
43
|
+
protector_meta.creatable?(fields)
|
44
|
+
end
|
45
|
+
|
46
|
+
# Checks if current model can be updated in the context of current subject
|
47
|
+
def updatable?
|
48
|
+
fields = HashWithIndifferentAccess[changed_columns.map{|x| [x.to_s, @values[x]]}]
|
49
|
+
protector_meta.updatable?(fields)
|
50
|
+
end
|
51
|
+
|
52
|
+
# Checks if current model can be destroyed in the context of current subject
|
53
|
+
def destroyable?
|
54
|
+
protector_meta.destroyable?
|
55
|
+
end
|
56
|
+
|
57
|
+
# Basic security validations
|
58
|
+
def validate
|
59
|
+
super
|
60
|
+
return unless @protector_subject
|
61
|
+
method = new? ? :creatable? : :updatable?
|
62
|
+
errors.add(:base, I18n.t('protector.invalid')) unless __send__(method)
|
63
|
+
end
|
64
|
+
|
65
|
+
# Destroy availability check
|
66
|
+
def before_destroy
|
67
|
+
return false if @protector_subject && !destroyable?
|
68
|
+
super
|
69
|
+
end
|
70
|
+
|
71
|
+
# Security-checking attributes reader
|
72
|
+
#
|
73
|
+
# @param name [Symbol] Name of attribute to read
|
74
|
+
def [](name)
|
75
|
+
if (
|
76
|
+
!@protector_subject ||
|
77
|
+
name == self.class.primary_key ||
|
78
|
+
(self.class.primary_key.is_a?(Array) && self.class.primary_key.include?(name)) ||
|
79
|
+
protector_meta.readable?(name.to_s)
|
80
|
+
)
|
81
|
+
@values[name]
|
82
|
+
else
|
83
|
+
nil
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
# This is used whenever we fetch data
|
88
|
+
def _associated_dataset(*args)
|
89
|
+
super.restrict!(@protector_subject)
|
90
|
+
end
|
91
|
+
|
92
|
+
# This is used whenever we call counters and existance checkers
|
93
|
+
def _dataset(*args)
|
94
|
+
super.restrict!(@protector_subject)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'protector/adapters/sequel/model'
|
2
|
+
require 'protector/adapters/sequel/dataset'
|
3
|
+
require 'protector/adapters/sequel/eager_graph_loader'
|
4
|
+
|
5
|
+
module Protector
|
6
|
+
module Adapters
|
7
|
+
# Sequel adapter
|
8
|
+
module Sequel
|
9
|
+
# YIP YIP! Monkey-Patch the Sequel.
|
10
|
+
def self.activate!
|
11
|
+
::Sequel::Model.send :include, Protector::Adapters::Sequel::Model
|
12
|
+
::Sequel::Dataset.send :include, Protector::Adapters::Sequel::Dataset
|
13
|
+
::Sequel::Model::Associations::EagerGraphLoader.send :include, Protector::Adapters::Sequel::EagerGraphLoader
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
data/lib/protector/dsl.rb
CHANGED
@@ -5,7 +5,7 @@ module Protector
|
|
5
5
|
|
6
6
|
# Single DSL evaluation result
|
7
7
|
class Box
|
8
|
-
attr_accessor :access, :relation, :destroyable
|
8
|
+
attr_accessor :access, :scope_proc, :relation, :destroyable
|
9
9
|
|
10
10
|
# @param model [Class] The class of protected entity
|
11
11
|
# @param fields [Array<String>] All the fields the model has
|
@@ -50,7 +50,8 @@ module Protector
|
|
50
50
|
# scope { none }
|
51
51
|
# end
|
52
52
|
def scope(&block)
|
53
|
-
@
|
53
|
+
@scope_proc = block
|
54
|
+
@relation = @model.instance_eval(&block)
|
54
55
|
end
|
55
56
|
|
56
57
|
# Enables action for given fields.
|
@@ -198,6 +199,8 @@ module Protector
|
|
198
199
|
# @param fields [Array<String>] All the fields the model has
|
199
200
|
# @param entry [Object] An instance of the model
|
200
201
|
def evaluate(model, subject, fields=[], entry=nil)
|
202
|
+
raise "Unprotected entity detected: use `restrict` method to protect it." unless subject
|
203
|
+
|
201
204
|
Box.new(model, fields, subject, entry, blocks)
|
202
205
|
end
|
203
206
|
end
|
data/lib/protector/version.rb
CHANGED
data/lib/protector.rb
CHANGED
@@ -4,7 +4,9 @@ require "i18n"
|
|
4
4
|
require "protector/version"
|
5
5
|
require "protector/dsl"
|
6
6
|
require "protector/adapters/active_record"
|
7
|
+
require "protector/adapters/sequel"
|
7
8
|
|
8
9
|
I18n.load_path << Dir[File.join File.expand_path(File.dirname(__FILE__)), '..', 'locales', '*.yml']
|
9
10
|
|
10
|
-
Protector::Adapters::ActiveRecord.activate! if defined?(ActiveRecord)
|
11
|
+
Protector::Adapters::ActiveRecord.activate! if defined?(ActiveRecord)
|
12
|
+
Protector::Adapters::Sequel.activate! if defined?(Sequel)
|
data/migrations/active_record.rb
CHANGED
@@ -0,0 +1,49 @@
|
|
1
|
+
### Connection
|
2
|
+
|
3
|
+
DB = if RUBY_PLATFORM == 'java'
|
4
|
+
Jdbc::SQLite3.load_driver
|
5
|
+
Sequel.connect('jdbc:sqlite::memory:')
|
6
|
+
else
|
7
|
+
Sequel.sqlite
|
8
|
+
end
|
9
|
+
|
10
|
+
Sequel::Model.instance_eval do
|
11
|
+
def none
|
12
|
+
where('1 = 0')
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
### Tables
|
17
|
+
|
18
|
+
[:dummies, :fluffies, :bobbies].each do |m|
|
19
|
+
DB.create_table m do
|
20
|
+
primary_key :id
|
21
|
+
String :string
|
22
|
+
Integer :number
|
23
|
+
Text :text
|
24
|
+
Integer :dummy_id
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
DB.create_table :loonies do
|
29
|
+
Integer :fluffy_id
|
30
|
+
String :string
|
31
|
+
end
|
32
|
+
|
33
|
+
### Classes
|
34
|
+
|
35
|
+
class Dummy < Sequel::Model
|
36
|
+
one_to_many :fluffies
|
37
|
+
one_to_many :bobbies
|
38
|
+
end
|
39
|
+
|
40
|
+
class Fluffy < Sequel::Model
|
41
|
+
many_to_one :dummy
|
42
|
+
one_to_one :loony
|
43
|
+
end
|
44
|
+
|
45
|
+
class Bobby < Sequel::Model
|
46
|
+
end
|
47
|
+
|
48
|
+
class Loony < Sequel::Model
|
49
|
+
end
|
data/perf/perf_helpers/boot.rb
CHANGED
@@ -2,7 +2,7 @@ class Perf
|
|
2
2
|
def self.load(adapter)
|
3
3
|
perf = Perf.new(adapter.camelize)
|
4
4
|
base = Pathname.new(File.expand_path '../..', __FILE__)
|
5
|
-
file = base.join(adapter+'.rb').to_s
|
5
|
+
file = base.join(adapter+'_perf.rb').to_s
|
6
6
|
perf.instance_eval File.read(file), file
|
7
7
|
perf.run!
|
8
8
|
end
|
@@ -11,6 +11,7 @@ class Perf
|
|
11
11
|
@blocks = {}
|
12
12
|
@adapter = adapter
|
13
13
|
@activated = false
|
14
|
+
@profiling = {}
|
14
15
|
end
|
15
16
|
|
16
17
|
def migrate
|
@@ -32,15 +33,22 @@ class Perf
|
|
32
33
|
@activation = block
|
33
34
|
end
|
34
35
|
|
35
|
-
def benchmark(subject, &block)
|
36
|
+
def benchmark(subject, options={}, &block)
|
36
37
|
@blocks[subject] = block
|
37
38
|
end
|
38
39
|
|
40
|
+
def benchmark!(subject, options={min_percent: 4}, &block)
|
41
|
+
@profiling[subject] = options
|
42
|
+
benchmark(subject, &block)
|
43
|
+
end
|
44
|
+
|
39
45
|
def activated?
|
40
46
|
@activated
|
41
47
|
end
|
42
48
|
|
43
49
|
def run!
|
50
|
+
require 'ruby-prof' if @profiling.any?
|
51
|
+
|
44
52
|
results = {}
|
45
53
|
|
46
54
|
results[:off] = run_state('disabled', :red)
|
@@ -65,11 +73,21 @@ class Perf
|
|
65
73
|
|
66
74
|
def run_state(state, color)
|
67
75
|
data = {}
|
76
|
+
prof = @profiling
|
68
77
|
|
69
78
|
print_block "Protector #{state.send color}" do
|
70
79
|
@blocks.each do |s, b|
|
80
|
+
RubyProf.start if prof.include?(s)
|
81
|
+
|
71
82
|
data[s] = Benchmark.realtime(&b)
|
72
83
|
print_result s, data[s].to_s
|
84
|
+
|
85
|
+
if prof.include?(s)
|
86
|
+
result = RubyProf.stop
|
87
|
+
|
88
|
+
printer = RubyProf::FlatPrinter.new(result)
|
89
|
+
printer.print(STDOUT, prof[s])
|
90
|
+
end
|
73
91
|
end
|
74
92
|
end
|
75
93
|
|
data/perf/sequel_perf.rb
ADDED
@@ -0,0 +1,84 @@
|
|
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 { where }
|
19
|
+
can :view, :string
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
Fluffy.instance_eval do
|
24
|
+
protect do
|
25
|
+
scope { where }
|
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.where
|
53
|
+
1000.times { scope.any? }
|
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.where
|
70
|
+
scope = scope.restrict!('!') if activated?
|
71
|
+
200.times { scope.to_a }
|
72
|
+
end
|
73
|
+
|
74
|
+
benchmark 'Select with eager loading' do
|
75
|
+
scope = Dummy.eager(:fluffies)
|
76
|
+
scope = scope.restrict!('!') if activated?
|
77
|
+
200.times { scope.to_a }
|
78
|
+
end
|
79
|
+
|
80
|
+
benchmark 'Select with filtered eager loading' do
|
81
|
+
scope = Dummy.eager_graph(fluffies: :loony)
|
82
|
+
scope = scope.restrict!('!') if activated?
|
83
|
+
200.times { scope.to_a }
|
84
|
+
end
|
@@ -7,6 +7,19 @@ if defined?(ActiveRecord)
|
|
7
7
|
before(:all) do
|
8
8
|
load 'migrations/active_record.rb'
|
9
9
|
|
10
|
+
module ProtectionCase
|
11
|
+
extend ActiveSupport::Concern
|
12
|
+
|
13
|
+
included do |klass|
|
14
|
+
protect do |x|
|
15
|
+
scope{ where('1=0') } if x == '-'
|
16
|
+
scope{ where(klass.table_name => {number: 999}) } if x == '+'
|
17
|
+
|
18
|
+
can :view, :dummy_id unless x == '-'
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
10
23
|
[Dummy, Fluffy].each{|c| c.send :include, ProtectionCase}
|
11
24
|
|
12
25
|
Dummy.create! string: 'zomgstring', number: 999, text: 'zomgtext'
|
@@ -127,7 +140,7 @@ if defined?(ActiveRecord)
|
|
127
140
|
|
128
141
|
context "joined to filtered association" do
|
129
142
|
it "scopes" do
|
130
|
-
d = Dummy.restrict!('+').includes(:fluffies).where(fluffies: {
|
143
|
+
d = Dummy.restrict!('+').includes(:fluffies).where(fluffies: {string: 'zomgstring'})
|
131
144
|
d.length.should == 2
|
132
145
|
d.first.fluffies.length.should == 1
|
133
146
|
end
|
@@ -136,18 +149,18 @@ if defined?(ActiveRecord)
|
|
136
149
|
context "joined to plain association" do
|
137
150
|
it "scopes" do
|
138
151
|
d = Dummy.restrict!('+').includes(:bobbies, :fluffies).where(
|
139
|
-
bobbies: {
|
152
|
+
bobbies: {string: 'zomgstring'}, fluffies: {string: 'zomgstring'}
|
140
153
|
)
|
141
154
|
d.length.should == 2
|
142
155
|
d.first.fluffies.length.should == 1
|
143
|
-
d.first.bobbies.length.should ==
|
156
|
+
d.first.bobbies.length.should == 2
|
144
157
|
end
|
145
158
|
end
|
146
159
|
|
147
160
|
context "with complex include" do
|
148
161
|
it "scopes" do
|
149
162
|
d = Dummy.restrict!('+').includes(fluffies: :loony).where(
|
150
|
-
fluffies: {
|
163
|
+
fluffies: {string: 'zomgstring'},
|
151
164
|
loonies: {string: 'zomgstring'}
|
152
165
|
)
|
153
166
|
d.length.should == 2
|