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 +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
|