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 +4 -4
- data/.yardopts +1 -0
- data/Gemfile +1 -0
- data/README.md +2 -0
- data/Rakefile +13 -0
- data/gemfiles/AR_3.2.gemfile +1 -0
- data/gemfiles/AR_3.2.gemfile.lock +2 -0
- data/gemfiles/AR_4.gemfile +1 -0
- data/gemfiles/AR_4.gemfile.lock +2 -0
- data/lib/protector/adapters/active_record.rb +2 -0
- data/lib/protector/adapters/active_record/association.rb +3 -0
- data/lib/protector/adapters/active_record/base.rb +18 -1
- data/lib/protector/adapters/active_record/preloader.rb +5 -0
- data/lib/protector/adapters/active_record/relation.rb +39 -17
- data/lib/protector/dsl.rb +113 -25
- data/lib/protector/version.rb +2 -1
- data/migrations/active_record.rb +48 -0
- data/perf/active_record.rb +90 -0
- data/perf/perf_helpers/boot.rb +95 -0
- data/spec/lib/adapters/active_record_spec.rb +53 -113
- data/spec/lib/dsl_spec.rb +3 -3
- data/spec/spec_helpers/adapters/active_record.rb +25 -0
- data/spec/spec_helpers/boot.rb +14 -2
- data/spec/spec_helpers/{model.rb → examples/model.rb} +42 -18
- metadata +10 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4d35910dd47bbaec6c4fa77c49edb1643b7ab614
|
4
|
+
data.tar.gz: 6d7e0b11f3c750004281dc432372d8e6e9b01cad
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 41f684019bb41d363579165474abfc9b280bd9bbaad66bd034b4735fe1a1acb8404941dd9d3f269969e44945fa42b612bb393b508f3be2ed147b8a9878830e58
|
7
|
+
data.tar.gz: 2cbf62c1a0da1f6ae6e15d97677cd5594641bcda24987460648625fdad83a163fdf5f0c6e7c35b2041bbdef65ea3ec040a49fd01f4cc4ddfc1b9e7acd56e24c9
|
data/.yardopts
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--markup=markdown
|
data/Gemfile
CHANGED
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
|
data/gemfiles/AR_3.2.gemfile
CHANGED
@@ -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
|
data/gemfiles/AR_4.gemfile
CHANGED
data/gemfiles/AR_4.gemfile.lock
CHANGED
@@ -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
|
-
#
|
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,
|
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
|
-
|
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
|
data/lib/protector/dsl.rb
CHANGED
@@ -3,16 +3,19 @@ module Protector
|
|
3
3
|
# DSL meta storage and evaluator
|
4
4
|
class Meta
|
5
5
|
|
6
|
-
#
|
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: {}}
|
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
|
-
|
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
|
-
@
|
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
|
-
|
164
|
+
keys = @access[part].keys
|
165
|
+
return false unless keys.length > 0
|
97
166
|
|
98
167
|
if fields
|
99
|
-
return false if (fields.keys -
|
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
|
-
|
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,
|
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
|
-
|
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
|
data/lib/protector/version.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
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
|
data/spec/lib/dsl_spec.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
91
|
+
view: {
|
92
92
|
"field1" => nil,
|
93
93
|
"field2" => nil,
|
94
94
|
"field3" => nil
|
95
95
|
},
|
96
|
-
|
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
|
data/spec/spec_helpers/boot.rb
CHANGED
@@ -1,8 +1,20 @@
|
|
1
|
+
Bundler.require
|
2
|
+
|
1
3
|
require 'protector'
|
4
|
+
require_relative 'examples/model'
|
2
5
|
|
3
|
-
|
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
|
-
|
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
|
-
|
24
|
-
|
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
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
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.
|
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-
|
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
|