heimdallr 0.0.2 → 1.0.0.RC2
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +21 -23
- data/README.yard.md +20 -22
- data/heimdallr.gemspec +3 -5
- data/lib/heimdallr.rb +14 -11
- data/lib/heimdallr/evaluator.rb +59 -20
- data/lib/heimdallr/{resource.rb → legacy_resource.rb} +72 -39
- data/lib/heimdallr/model.rb +44 -7
- data/lib/heimdallr/proxy/collection.rb +208 -10
- data/lib/heimdallr/proxy/record.rb +105 -31
- metadata +18 -17
data/README.md
CHANGED
@@ -19,17 +19,19 @@ class Article < ActiveRecord::Base
|
|
19
19
|
restrict do |user|
|
20
20
|
if user.admin? || user == self.owner
|
21
21
|
# Administrator or owner can do everything
|
22
|
-
|
23
|
-
|
22
|
+
scope :fetch
|
23
|
+
scope :destroy
|
24
|
+
can [:view, :create, :update]
|
24
25
|
else
|
25
26
|
# Other users can view only non-classified articles...
|
26
|
-
|
27
|
+
scope :fetch, -> { where('secrecy_level < ?', 5) }
|
27
28
|
|
28
29
|
# ... and see all fields except the actual security level...
|
29
30
|
can :view
|
30
31
|
cannot :view, [:secrecy_level]
|
31
32
|
|
32
33
|
# ... and can create them with certain restrictions.
|
34
|
+
can :create, %w(content)
|
33
35
|
can [:create, :update], {
|
34
36
|
owner: user,
|
35
37
|
secrecy_level: { inclusion: { in: 0..4 } }
|
@@ -52,12 +54,17 @@ secure = Article.restrict(johndoe)
|
|
52
54
|
# Use any ARel methods:
|
53
55
|
secure.pluck(:content)
|
54
56
|
# => ["Nothing happens", "Hello World"]
|
55
|
-
secure.find(1).secrecy_level
|
56
|
-
# => nil
|
57
57
|
|
58
58
|
# Everything should be permitted explicitly:
|
59
59
|
secure.first.delete
|
60
60
|
# ! Heimdallr::PermissionError is raised
|
61
|
+
secure.find(1).secrecy_level
|
62
|
+
# ! Heimdallr::PermissionError is raised
|
63
|
+
|
64
|
+
# There is a helper for views to be easily written:
|
65
|
+
view_passed = secure.first.implicit
|
66
|
+
view_passed.secrecy_level
|
67
|
+
# => nil
|
61
68
|
|
62
69
|
# If only a single value is possible, it is inferred automatically:
|
63
70
|
secure.create! content: "My second article"
|
@@ -65,7 +72,7 @@ secure.create! content: "My second article"
|
|
65
72
|
|
66
73
|
# ... and cannot be changed:
|
67
74
|
secure.create! owner: admin, content: "I'm a haxx0r"
|
68
|
-
# !
|
75
|
+
# ! Heimdallr::PermissionError is raised
|
69
76
|
|
70
77
|
# You can use any valid ActiveRecord validators, too:
|
71
78
|
secure.create! content: "Top Secret", secrecy_level: 10
|
@@ -78,26 +85,17 @@ secure.find 2
|
|
78
85
|
# -- No, it is not.
|
79
86
|
```
|
80
87
|
|
81
|
-
The DSL is described in documentation for [Heimdallr::Model](http://rubydoc.info/gems/heimdallr/0.0.
|
82
|
-
|
83
|
-
Note that Heimdallr is designed with three goals in mind, in the following order:
|
84
|
-
|
85
|
-
* Preventing malicious modifications
|
86
|
-
* Preventing information leaks
|
87
|
-
* Being convenient to use
|
88
|
-
|
89
|
-
Due to the last one, not all methods will raise an exception on invalid access; some will silently drop the offending
|
90
|
-
attribute or simply return `nil`. This is clearly described in the documentation, done intentionally and isn't
|
91
|
-
going to change.
|
88
|
+
The DSL is described in documentation for [Heimdallr::Model](http://rubydoc.info/gems/heimdallr/1.0.0.RC2/Heimdallr/Model).
|
92
89
|
|
93
|
-
|
94
|
-
|
90
|
+
Ideology
|
91
|
+
--------
|
95
92
|
|
96
|
-
Heimdallr
|
97
|
-
|
98
|
-
|
93
|
+
Heimdallr aims to make security explicit, but nevertheless convenient. It does not allow one to call any
|
94
|
+
implicit operations which may be used maliciously; instead, it forces you to explicitly call `#insecure`
|
95
|
+
method which returns the underlying object. This single point of entry is easily recognizable with code.
|
99
96
|
|
100
|
-
|
97
|
+
Heimdallr would raise exceptions in all cases of forbidden or potentially unsecure access except for attribute
|
98
|
+
reading to allow for writing uncrufted code in templates (particularly [JBuilder](http://github.com/rails/jbuilder) ones).
|
101
99
|
|
102
100
|
Compatibility
|
103
101
|
-------------
|
data/README.yard.md
CHANGED
@@ -19,17 +19,19 @@ class Article < ActiveRecord::Base
|
|
19
19
|
restrict do |user|
|
20
20
|
if user.admin? || user == self.owner
|
21
21
|
# Administrator or owner can do everything
|
22
|
-
|
23
|
-
|
22
|
+
scope :fetch
|
23
|
+
scope :destroy
|
24
|
+
can [:view, :create, :update]
|
24
25
|
else
|
25
26
|
# Other users can view only non-classified articles...
|
26
|
-
|
27
|
+
scope :fetch, -> { where('secrecy_level < ?', 5) }
|
27
28
|
|
28
29
|
# ... and see all fields except the actual security level...
|
29
30
|
can :view
|
30
31
|
cannot :view, [:secrecy_level]
|
31
32
|
|
32
33
|
# ... and can create them with certain restrictions.
|
34
|
+
can :create, %w(content)
|
33
35
|
can [:create, :update], {
|
34
36
|
owner: user,
|
35
37
|
secrecy_level: { inclusion: { in: 0..4 } }
|
@@ -52,12 +54,17 @@ secure = Article.restrict(johndoe)
|
|
52
54
|
# Use any ARel methods:
|
53
55
|
secure.pluck(:content)
|
54
56
|
# => ["Nothing happens", "Hello World"]
|
55
|
-
secure.find(1).secrecy_level
|
56
|
-
# => nil
|
57
57
|
|
58
58
|
# Everything should be permitted explicitly:
|
59
59
|
secure.first.delete
|
60
60
|
# ! Heimdallr::PermissionError is raised
|
61
|
+
secure.find(1).secrecy_level
|
62
|
+
# ! Heimdallr::PermissionError is raised
|
63
|
+
|
64
|
+
# There is a helper for views to be easily written:
|
65
|
+
view_passed = secure.first.implicit
|
66
|
+
view_passed.secrecy_level
|
67
|
+
# => nil
|
61
68
|
|
62
69
|
# If only a single value is possible, it is inferred automatically:
|
63
70
|
secure.create! content: "My second article"
|
@@ -65,7 +72,7 @@ secure.create! content: "My second article"
|
|
65
72
|
|
66
73
|
# ... and cannot be changed:
|
67
74
|
secure.create! owner: admin, content: "I'm a haxx0r"
|
68
|
-
# !
|
75
|
+
# ! Heimdallr::PermissionError is raised
|
69
76
|
|
70
77
|
# You can use any valid ActiveRecord validators, too:
|
71
78
|
secure.create! content: "Top Secret", secrecy_level: 10
|
@@ -80,24 +87,15 @@ secure.find 2
|
|
80
87
|
|
81
88
|
The DSL is described in documentation for {Heimdallr::Model}.
|
82
89
|
|
83
|
-
|
84
|
-
|
85
|
-
* Preventing malicious modifications
|
86
|
-
* Preventing information leaks
|
87
|
-
* Being convenient to use
|
88
|
-
|
89
|
-
Due to the last one, not all methods will raise an exception on invalid access; some will silently drop the offending
|
90
|
-
attribute or simply return `nil`. This is clearly described in the documentation, done intentionally and isn't
|
91
|
-
going to change.
|
92
|
-
|
93
|
-
REST interface
|
94
|
-
--------------
|
90
|
+
Ideology
|
91
|
+
--------
|
95
92
|
|
96
|
-
Heimdallr
|
97
|
-
|
98
|
-
|
93
|
+
Heimdallr aims to make security explicit, but nevertheless convenient. It does not allow one to call any
|
94
|
+
implicit operations which may be used maliciously; instead, it forces you to explicitly call `#insecure`
|
95
|
+
method which returns the underlying object. This single point of entry is easily recognizable with code.
|
99
96
|
|
100
|
-
|
97
|
+
Heimdallr would raise exceptions in all cases of forbidden or potentially unsecure access except for attribute
|
98
|
+
reading to allow for writing uncrufted code in templates (particularly [JBuilder](http://github.com/rails/jbuilder) ones).
|
101
99
|
|
102
100
|
Compatibility
|
103
101
|
-------------
|
data/heimdallr.gemspec
CHANGED
@@ -3,17 +3,15 @@ $:.push File.expand_path("../lib", __FILE__)
|
|
3
3
|
|
4
4
|
Gem::Specification.new do |s|
|
5
5
|
s.name = "heimdallr"
|
6
|
-
s.version = "0.0.
|
7
|
-
s.authors = ["Peter Zotov"]
|
8
|
-
s.email = ["whitequark@whitequark.org"]
|
6
|
+
s.version = "1.0.0.RC2"
|
7
|
+
s.authors = ["Peter Zotov", "Boris Staal"]
|
8
|
+
s.email = ["whitequark@whitequark.org", "boris@roundlake.ru"]
|
9
9
|
s.homepage = "http://github.com/roundlake/heimdallr"
|
10
10
|
s.summary = %q{Heimdallr is an ActiveModel extension which provides object- and field-level access control.}
|
11
11
|
s.description = %q{Heimdallr aims to provide an easy to configure and efficient object- and field-level access
|
12
12
|
control solution, reusing proven patterns from gems like CanCan and allowing one to manage permissions in a very
|
13
13
|
fine-grained manner.}
|
14
14
|
|
15
|
-
s.rubyforge_project = "heimdallr"
|
16
|
-
|
17
15
|
s.files = `git ls-files`.split("\n")
|
18
16
|
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
19
17
|
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
data/lib/heimdallr.rb
CHANGED
@@ -1,15 +1,7 @@
|
|
1
1
|
require "active_support"
|
2
|
+
require "active_support/core_ext/module/delegation"
|
2
3
|
require "active_model"
|
3
4
|
|
4
|
-
require "heimdallr/version"
|
5
|
-
|
6
|
-
require "heimdallr/proxy/collection"
|
7
|
-
require "heimdallr/proxy/record"
|
8
|
-
require "heimdallr/validator"
|
9
|
-
require "heimdallr/evaluator"
|
10
|
-
require "heimdallr/model"
|
11
|
-
require "heimdallr/resource"
|
12
|
-
|
13
5
|
# See {file:README.yard}.
|
14
6
|
module Heimdallr
|
15
7
|
class << self
|
@@ -33,9 +25,10 @@ module Heimdallr
|
|
33
25
|
#
|
34
26
|
# @return [Boolean]
|
35
27
|
attr_accessor :allow_insecure_associations
|
36
|
-
self.allow_insecure_associations = false
|
37
28
|
end
|
38
29
|
|
30
|
+
self.allow_insecure_associations = false
|
31
|
+
|
39
32
|
# {PermissionError} is raised when a security policy prevents
|
40
33
|
# a called operation from being executed.
|
41
34
|
class PermissionError < StandardError; end
|
@@ -43,4 +36,14 @@ module Heimdallr
|
|
43
36
|
# {InsecureOperationError} is raised when a potentially unsafe
|
44
37
|
# operation is about to be executed.
|
45
38
|
class InsecureOperationError < StandardError; end
|
46
|
-
|
39
|
+
|
40
|
+
# Heimdallr uses proxies to control access to restricted scopes and collections.
|
41
|
+
module Proxy; end
|
42
|
+
end
|
43
|
+
|
44
|
+
require "heimdallr/proxy/collection"
|
45
|
+
require "heimdallr/proxy/record"
|
46
|
+
require "heimdallr/validator"
|
47
|
+
require "heimdallr/evaluator"
|
48
|
+
require "heimdallr/model"
|
49
|
+
require "heimdallr/legacy_resource"
|
data/lib/heimdallr/evaluator.rb
CHANGED
@@ -6,18 +6,20 @@ module Heimdallr
|
|
6
6
|
# is whitelisting safe actions, not blacklisting unsafe ones. This is by design
|
7
7
|
# and is not going to change.
|
8
8
|
#
|
9
|
+
# The field +#id+ is whitelisted by default.
|
10
|
+
#
|
9
11
|
# The DSL consists of three functions: {#scope}, {#can} and {#cannot}.
|
10
12
|
class Evaluator
|
11
13
|
attr_reader :allowed_fields, :fixtures, :validators
|
12
14
|
|
13
|
-
# Create a new Evaluator for the
|
15
|
+
# Create a new Evaluator for the +ActiveRecord+-derived class +model_class+,
|
14
16
|
# and use +block+ to infer restrictions for any security context passed.
|
15
17
|
def initialize(model_class, block)
|
16
18
|
@model_class, @block = model_class, block
|
17
19
|
|
18
20
|
@scopes = {}
|
19
21
|
@allowed_fields = {}
|
20
|
-
@
|
22
|
+
@validators = {}
|
21
23
|
@fixtures = {}
|
22
24
|
end
|
23
25
|
|
@@ -36,7 +38,7 @@ module Heimdallr
|
|
36
38
|
# This form accepts an implicit lambda.
|
37
39
|
#
|
38
40
|
# @example
|
39
|
-
# scope :fetch do
|
41
|
+
# scope :fetch do |user|
|
40
42
|
# if user.manager?
|
41
43
|
# scoped
|
42
44
|
# else
|
@@ -72,15 +74,18 @@ module Heimdallr
|
|
72
74
|
# @param [Symbol, Array<Symbol>] actions one or more action names
|
73
75
|
# @param [Hash<Hash, Object>] fields field restrictions
|
74
76
|
def can(actions, fields=@model_class.attribute_names)
|
75
|
-
Array(actions).each do |action|
|
77
|
+
Array(actions).map(&:to_sym).each do |action|
|
76
78
|
case fields
|
77
79
|
when Hash # a list of validations
|
78
|
-
@allowed_fields[action] += fields.keys
|
79
|
-
@
|
80
|
-
|
80
|
+
@allowed_fields[action] += fields.keys.map(&:to_sym)
|
81
|
+
@validators[action] += create_validators(fields)
|
82
|
+
|
83
|
+
fixtures = extract_fixtures(fields)
|
84
|
+
@fixtures[action] = @fixtures[action].merge fixtures
|
85
|
+
@allowed_fields[action] -= fixtures.keys
|
81
86
|
|
82
87
|
else # an array or a field name
|
83
|
-
@allowed_fields[action] += Array(fields)
|
88
|
+
@allowed_fields[action] += Array(fields).map(&:to_sym)
|
84
89
|
end
|
85
90
|
end
|
86
91
|
end
|
@@ -91,8 +96,8 @@ module Heimdallr
|
|
91
96
|
# @param [Symbol, Array<Symbol>] actions one or more action names
|
92
97
|
# @param [Array<Symbol>] fields field list
|
93
98
|
def cannot(actions, fields)
|
94
|
-
Array(actions).each do |action|
|
95
|
-
@allowed_fields[action] -= fields
|
99
|
+
Array(actions).map(&:to_sym).each do |action|
|
100
|
+
@allowed_fields[action] -= fields.map(&:to_sym)
|
96
101
|
@fixtures.delete_at *fields
|
97
102
|
end
|
98
103
|
end
|
@@ -104,25 +109,59 @@ module Heimdallr
|
|
104
109
|
# @param scope name of the scope
|
105
110
|
# @param basic_scope the scope to which scope +name+ will be applied. Defaults to +:fetch+.
|
106
111
|
#
|
107
|
-
# @return ActiveRecord scope
|
108
|
-
|
109
|
-
|
110
|
-
|
112
|
+
# @return +ActiveRecord+ scope.
|
113
|
+
#
|
114
|
+
# @raise [RuntimeError] if the scope is not defined
|
115
|
+
def request_scope(name=:fetch, basic_scope=nil)
|
116
|
+
unless @scopes.has_key?(name)
|
117
|
+
raise RuntimeError, "The #{name.inspect} scope does not exist"
|
118
|
+
end
|
119
|
+
|
120
|
+
if name == :fetch && basic_scope.nil?
|
121
|
+
@model_class.instance_exec(&@scopes[:fetch])
|
111
122
|
else
|
112
|
-
basic_scope.instance_exec(&@scopes[name])
|
123
|
+
(basic_scope || request_scope(:fetch)).instance_exec(&@scopes[name])
|
113
124
|
end
|
114
125
|
end
|
115
126
|
|
127
|
+
# Check if any explicit restrictions were defined for +action+.
|
128
|
+
# +can :create, []+ _is_ an explicit restriction for action +:create+.
|
129
|
+
#
|
130
|
+
# @return Boolean
|
131
|
+
def can?(action)
|
132
|
+
@allowed_fields.include? action
|
133
|
+
end
|
134
|
+
|
135
|
+
# Return a Hash to be mixed in in +reflect_on_security+ methods of {Proxy::Collection}
|
136
|
+
# and {Proxy::Record}.
|
137
|
+
def reflection
|
138
|
+
{
|
139
|
+
operations: [ :view, :create, :update ].select { |op| can? op }
|
140
|
+
}
|
141
|
+
end
|
142
|
+
|
116
143
|
# Compute the restrictions for a given +context+. Invokes a +block+ passed to the
|
117
144
|
# +initialize+ once.
|
145
|
+
#
|
146
|
+
# @raise [RuntimeError] if the evaluated block did not define a set of valid restrictions
|
118
147
|
def evaluate(context)
|
119
148
|
if context != @last_context
|
120
149
|
@scopes = {}
|
121
150
|
@allowed_fields = Hash.new { [] }
|
122
151
|
@validators = Hash.new { [] }
|
123
|
-
@fixtures = Hash.new {
|
152
|
+
@fixtures = Hash.new { {} }
|
153
|
+
|
154
|
+
@allowed_fields[:view] += [ :id ]
|
124
155
|
|
125
|
-
instance_exec context,
|
156
|
+
instance_exec context, &@block
|
157
|
+
|
158
|
+
unless @scopes[:fetch]
|
159
|
+
raise RuntimeError, "A :fetch scope must be defined"
|
160
|
+
end
|
161
|
+
|
162
|
+
@allowed_fields.each do |action, fields|
|
163
|
+
fields.uniq!
|
164
|
+
end
|
126
165
|
|
127
166
|
[@scopes, @allowed_fields, @validators, @fixtures].
|
128
167
|
map(&:freeze)
|
@@ -139,7 +178,7 @@ module Heimdallr
|
|
139
178
|
#
|
140
179
|
# @return [Array<ActiveModel::Validator>]
|
141
180
|
def create_validators(fields)
|
142
|
-
validators =
|
181
|
+
validators = []
|
143
182
|
|
144
183
|
fields.each do |attribute, validations|
|
145
184
|
next unless validations.is_a? Hash
|
@@ -153,7 +192,7 @@ module Heimdallr
|
|
153
192
|
raise ArgumentError, "Unknown validator: '#{key}'"
|
154
193
|
end
|
155
194
|
|
156
|
-
validators
|
195
|
+
validators << validator.new(_parse_validates_options(options).merge(:attributes => [ attribute ]))
|
157
196
|
end
|
158
197
|
end
|
159
198
|
|
@@ -167,7 +206,7 @@ module Heimdallr
|
|
167
206
|
fields.each do |attribute, options|
|
168
207
|
next if options.is_a? Hash
|
169
208
|
|
170
|
-
fixtures[attribute] = options
|
209
|
+
fixtures[attribute.to_sym] = options
|
171
210
|
end
|
172
211
|
|
173
212
|
fixtures
|
@@ -1,5 +1,8 @@
|
|
1
1
|
module Heimdallr
|
2
|
-
#
|
2
|
+
#
|
3
|
+
# @deprecated Will be removed ASAP. Please don't use it in favour of http://github.com/roundlake/heimdallr-resource/
|
4
|
+
#
|
5
|
+
# Heimdallr {LegacyResource} is a boilerplate for simple creation of REST endpoints, most of which are
|
3
6
|
# quite similar and thus may share a lot of code.
|
4
7
|
#
|
5
8
|
# The minimal controller possible would be:
|
@@ -28,19 +31,21 @@ module Heimdallr
|
|
28
31
|
# Resource only works with ActiveRecord.
|
29
32
|
#
|
30
33
|
# See also {Resource::ClassMethods}.
|
31
|
-
module
|
34
|
+
module LegacyResource
|
32
35
|
# @group Actions
|
33
36
|
|
34
37
|
# +GET /resources+
|
35
38
|
#
|
36
39
|
# This action does nothing by itself, but it has a +load_all_resources+ filter attached.
|
37
40
|
def index
|
41
|
+
render_data
|
38
42
|
end
|
39
43
|
|
40
44
|
# +GET /resource/1+
|
41
45
|
#
|
42
46
|
# This action does nothing by itself, but it has a +load_one_resource+ filter attached.
|
43
47
|
def show
|
48
|
+
render_data
|
44
49
|
end
|
45
50
|
|
46
51
|
# +GET /resources/new+
|
@@ -61,16 +66,15 @@ module Heimdallr
|
|
61
66
|
# This action creates one or more records from the passed parameters.
|
62
67
|
# It can accept both arrays of attribute hashes and single attribute hashes.
|
63
68
|
#
|
64
|
-
# After the creation, it calls {#
|
69
|
+
# After the creation, it calls {#render_data}.
|
65
70
|
#
|
66
71
|
# See also {#load_referenced_resources} and {#with_objects_from_params}.
|
67
72
|
def create
|
68
|
-
with_objects_from_params do |
|
69
|
-
|
70
|
-
create!(attributes)
|
73
|
+
with_objects_from_params(replace: true) do |object, attributes|
|
74
|
+
restricted_model.create(attributes)
|
71
75
|
end
|
72
76
|
|
73
|
-
|
77
|
+
render_data verify: true
|
74
78
|
end
|
75
79
|
|
76
80
|
# +GET /resources/1/edit+
|
@@ -79,7 +83,7 @@ module Heimdallr
|
|
79
83
|
# See also {#new}.
|
80
84
|
def edit
|
81
85
|
render :json => {
|
82
|
-
:fields => model.
|
86
|
+
:fields => model.restrictions(security_context).allowed_fields[:update]
|
83
87
|
}
|
84
88
|
end
|
85
89
|
|
@@ -90,15 +94,15 @@ module Heimdallr
|
|
90
94
|
# and expects them to be in the order corresponding to the order of actual
|
91
95
|
# attribute hashes.
|
92
96
|
#
|
93
|
-
# After the updating, it calls {#
|
97
|
+
# After the updating, it calls {#render_data}.
|
94
98
|
#
|
95
99
|
# See also {#load_referenced_resources} and {#with_objects_from_params}.
|
96
100
|
def update
|
97
|
-
with_objects_from_params do |
|
98
|
-
|
101
|
+
with_objects_from_params do |object, attributes|
|
102
|
+
object.update_attributes attributes
|
99
103
|
end
|
100
104
|
|
101
|
-
|
105
|
+
render_data verify: true
|
102
106
|
end
|
103
107
|
|
104
108
|
# +DELETE /resources/1,2+
|
@@ -108,8 +112,8 @@ module Heimdallr
|
|
108
112
|
#
|
109
113
|
# See also {#load_referenced_resources}.
|
110
114
|
def destroy
|
111
|
-
|
112
|
-
|
115
|
+
with_objects_from_params do |object, attributes|
|
116
|
+
object.destroy
|
113
117
|
end
|
114
118
|
|
115
119
|
render :json => {}, :status => :ok
|
@@ -158,34 +162,50 @@ module Heimdallr
|
|
158
162
|
self.model.scoped
|
159
163
|
end
|
160
164
|
|
165
|
+
# Return the scoped and restricted model. By default this method
|
166
|
+
# restricts the result of {#scoped_model} with +security_context+,
|
167
|
+
# which is expected to be defined on this class or its ancestors.
|
168
|
+
def restricted_model
|
169
|
+
scoped_model.restrict(security_context, implicit: true)
|
170
|
+
end
|
171
|
+
|
161
172
|
# Loads all resources in the current scope to +@resources+.
|
162
173
|
#
|
163
174
|
# Is automatically applied to {#index}.
|
164
175
|
def load_all_resources
|
165
|
-
@
|
166
|
-
|
167
|
-
|
168
|
-
# Loads one resource from the current scope, referenced by <code>params[:id]</code>,
|
169
|
-
# to +@resource+.
|
170
|
-
#
|
171
|
-
# Is automatically applied to {#show}.
|
172
|
-
def load_one_resource
|
173
|
-
@resource = scoped_model.find(params[:id])
|
176
|
+
@multiple_resources = true
|
177
|
+
@resources = restricted_model
|
174
178
|
end
|
175
179
|
|
176
180
|
# Loads several resources from the current scope, referenced by <code>params[:id]</code>
|
177
181
|
# with a comma-separated string like "1,2,3", to +@resources+.
|
178
182
|
#
|
179
|
-
# Is automatically applied to {#update} and {#destroy}.
|
183
|
+
# Is automatically applied to {#show}, {#update} and {#destroy}.
|
180
184
|
def load_referenced_resources
|
181
|
-
|
185
|
+
if params[:id][0] == '*'
|
186
|
+
@multiple_resources = true
|
187
|
+
@resources = restricted_model.find(params[:id][1..-1].split(','))
|
188
|
+
else
|
189
|
+
@multiple_resources = false
|
190
|
+
@resource = restricted_model.find(params[:id])
|
191
|
+
end
|
182
192
|
end
|
183
193
|
|
184
194
|
# Render a modified collection in {#create}, {#update} and similar actions.
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
195
|
+
def render_data(options={})
|
196
|
+
if @multiple_resources
|
197
|
+
if options[:verify] && @resources.any?(&:invalid?)
|
198
|
+
render :json => { errors: @resources.map(&:errors) }, :status => :unprocessable_entity
|
199
|
+
else
|
200
|
+
render :action => :index
|
201
|
+
end
|
202
|
+
else
|
203
|
+
if options[:verify] && @resource.invalid?
|
204
|
+
render :json => @resource.errors, :status => :unprocessable_entity
|
205
|
+
else
|
206
|
+
render :action => :show
|
207
|
+
end
|
208
|
+
end
|
189
209
|
end
|
190
210
|
|
191
211
|
# Fetch one or several objects passed in +params+ and yield them to a block,
|
@@ -194,14 +214,28 @@ module Heimdallr
|
|
194
214
|
# @yield [attributes, index]
|
195
215
|
# @yieldparam [Hash] attributes
|
196
216
|
# @yieldparam [Integer] index
|
197
|
-
def with_objects_from_params
|
217
|
+
def with_objects_from_params(options={})
|
198
218
|
model.transaction do
|
199
|
-
if
|
200
|
-
|
219
|
+
if @multiple_resources
|
220
|
+
begin
|
221
|
+
name = model.name.underscore.pluralize
|
222
|
+
if params[name].is_a? Hash
|
223
|
+
enumerator = params[name].keys.each
|
224
|
+
else
|
225
|
+
enumerator = params[name].each_index
|
226
|
+
end
|
227
|
+
|
228
|
+
result = enumerator.map do |index|
|
229
|
+
yield(@resources[index.to_i], params[name][index])
|
230
|
+
end
|
231
|
+
ensure
|
232
|
+
@resources = result if options[:replace]
|
233
|
+
end
|
201
234
|
else
|
202
|
-
|
203
|
-
|
204
|
-
|
235
|
+
begin
|
236
|
+
result = yield(@resource, params[model.name.underscore])
|
237
|
+
ensure
|
238
|
+
@resource = result if options[:replace]
|
205
239
|
end
|
206
240
|
end
|
207
241
|
end
|
@@ -232,7 +266,7 @@ module Heimdallr
|
|
232
266
|
# For convenience, you can pass additional actions to register with default filters in
|
233
267
|
# +options+. It is also possible to use +append_before_filter+.
|
234
268
|
#
|
235
|
-
# @param [Class] model
|
269
|
+
# @param [Class] model an +ActiveRecord+-derived model class
|
236
270
|
# @option options [Array<Symbol>] :index
|
237
271
|
# Additional actions to be covered by {Heimdallr::Resource#load_all_resources}.
|
238
272
|
# @option options [Array<Symbol>] :member
|
@@ -242,9 +276,8 @@ module Heimdallr
|
|
242
276
|
def resource_for(model, options={})
|
243
277
|
@model = model.to_s.camelize.constantize
|
244
278
|
|
245
|
-
before_filter :load_all_resources,
|
246
|
-
before_filter :
|
247
|
-
before_filter :load_referenced_resources, only: [ :update, :destroy ].concat(options[:collection] || [])
|
279
|
+
before_filter :load_all_resources, only: [ :index ].concat(options[:all] || [])
|
280
|
+
before_filter :load_referenced_resources, only: [ :show, :update, :destroy ].concat(options[:referenced] || [])
|
248
281
|
end
|
249
282
|
end
|
250
283
|
end
|
data/lib/heimdallr/model.rb
CHANGED
@@ -10,6 +10,9 @@ module Heimdallr
|
|
10
10
|
# end
|
11
11
|
# end
|
12
12
|
#
|
13
|
+
# {Heimdallr::Model} should be included prior to any other modules, as it may omit
|
14
|
+
# some named scopes defined by those if it is not.
|
15
|
+
#
|
13
16
|
# @todo Improve description
|
14
17
|
module Model
|
15
18
|
extend ActiveSupport::Concern
|
@@ -28,11 +31,11 @@ module Heimdallr
|
|
28
31
|
# @param [Object] context security context
|
29
32
|
# @param [Symbol] action kind of actions which will be performed
|
30
33
|
# @return [Proxy::Collection]
|
31
|
-
def restrict(context=nil, &block)
|
34
|
+
def restrict(context=nil, options={}, &block)
|
32
35
|
if block
|
33
|
-
@restrictions = Evaluator.new(self,
|
36
|
+
@restrictions = Evaluator.new(self, block)
|
34
37
|
else
|
35
|
-
Proxy::Collection.new(context, restrictions(context).request_scope)
|
38
|
+
Proxy::Collection.new(context, restrictions(context).request_scope, options)
|
36
39
|
end
|
37
40
|
end
|
38
41
|
|
@@ -42,13 +45,40 @@ module Heimdallr
|
|
42
45
|
def restrictions(context)
|
43
46
|
@restrictions.evaluate(context)
|
44
47
|
end
|
48
|
+
|
49
|
+
# @api private
|
50
|
+
#
|
51
|
+
# An internal attribute to store the list of user-defined name scopes.
|
52
|
+
# It is required because ActiveRecord does not provide any introspection for
|
53
|
+
# named scopes.
|
54
|
+
attr_accessor :heimdallr_scopes
|
55
|
+
|
56
|
+
# An interceptor for named scopes which adds them to {#heimdallr_scopes} list.
|
57
|
+
def scope(name, *args)
|
58
|
+
self.heimdallr_scopes ||= []
|
59
|
+
self.heimdallr_scopes.push name
|
60
|
+
|
61
|
+
super
|
62
|
+
end
|
63
|
+
|
64
|
+
# @api private
|
65
|
+
#
|
66
|
+
# An internal attribute to store the list of user-defined relation-like methods
|
67
|
+
# which return ActiveRecord family objects and can be automatically restricted.
|
68
|
+
attr_accessor :heimdallr_relations
|
69
|
+
|
70
|
+
# A DSL method for defining relation-like methods.
|
71
|
+
def heimdallr_relation(*methods)
|
72
|
+
self.heimdallr_relations ||= []
|
73
|
+
self.heimdallr_relations += methods.map(&:to_sym)
|
74
|
+
end
|
45
75
|
end
|
46
76
|
|
47
77
|
# Return a secure proxy object for this record.
|
48
78
|
#
|
49
79
|
# @return [Record::Proxy]
|
50
|
-
def restrict(context,
|
51
|
-
Record
|
80
|
+
def restrict(context, options={})
|
81
|
+
Proxy::Record.new(context, self, options)
|
52
82
|
end
|
53
83
|
|
54
84
|
# @api private
|
@@ -57,9 +87,16 @@ module Heimdallr
|
|
57
87
|
# the context in which this object is currently being saved.
|
58
88
|
attr_accessor :heimdallr_validators
|
59
89
|
|
90
|
+
# @api private
|
91
|
+
#
|
92
|
+
# An internal method to run Heimdallr security validators, when applicable.
|
93
|
+
def heimdallr_validations
|
94
|
+
validates_with Heimdallr::Validator
|
95
|
+
end
|
96
|
+
|
60
97
|
def self.included(klass)
|
61
|
-
klass.
|
62
|
-
|
98
|
+
klass.class_eval do
|
99
|
+
validate :heimdallr_validations
|
63
100
|
end
|
64
101
|
end
|
65
102
|
end
|
@@ -2,27 +2,225 @@ module Heimdallr
|
|
2
2
|
# A security-aware proxy for +ActiveRecord+ scopes. This class validates all the
|
3
3
|
# method calls and either forwards them to the encapsulated scope or raises
|
4
4
|
# an exception.
|
5
|
+
#
|
6
|
+
# There are two kinds of collection proxies, explicit and implicit, which instantiate
|
7
|
+
# the corresponding types of record proxies. See also {Proxy::Record}.
|
5
8
|
class Proxy::Collection
|
9
|
+
include Enumerable
|
10
|
+
|
6
11
|
# Create a collection proxy.
|
7
|
-
#
|
8
|
-
#
|
9
|
-
|
10
|
-
|
12
|
+
#
|
13
|
+
# The +scope+ is expected to be already restricted with +:fetch+ scope.
|
14
|
+
#
|
15
|
+
# @param context security context
|
16
|
+
# @param scope proxified scope
|
17
|
+
# @option options [Boolean] implicit proxy type
|
18
|
+
def initialize(context, scope, options={})
|
19
|
+
@context, @scope, @options = context, scope, options
|
11
20
|
|
12
|
-
@restrictions = @
|
21
|
+
@restrictions = @scope.restrictions(context)
|
13
22
|
end
|
14
23
|
|
15
24
|
# Collections cannot be restricted twice.
|
16
25
|
#
|
17
26
|
# @raise [RuntimeError]
|
18
|
-
def restrict(
|
27
|
+
def restrict(*args)
|
19
28
|
raise RuntimeError, "Collections cannot be restricted twice"
|
20
29
|
end
|
21
30
|
|
22
|
-
#
|
23
|
-
# @
|
24
|
-
|
25
|
-
|
31
|
+
# @private
|
32
|
+
# @macro [attach] delegate_as_constructor
|
33
|
+
# A proxy for +$1+ method which adds fixtures to the attribute list and
|
34
|
+
# returns a restricted record.
|
35
|
+
def self.delegate_as_constructor(name, method)
|
36
|
+
class_eval(<<-EOM, __FILE__, __LINE__)
|
37
|
+
def #{name}(attributes={})
|
38
|
+
record = @restrictions.request_scope(:fetch).new.restrict(@context, @options)
|
39
|
+
record.#{method}(attributes.merge(@restrictions.fixtures[:create]))
|
40
|
+
record
|
41
|
+
end
|
42
|
+
EOM
|
43
|
+
end
|
44
|
+
|
45
|
+
# @private
|
46
|
+
# @macro [attach] delegate_as_scope
|
47
|
+
# A proxy for +$1+ method which returns a restricted scope.
|
48
|
+
def self.delegate_as_scope(name)
|
49
|
+
class_eval(<<-EOM, __FILE__, __LINE__)
|
50
|
+
def #{name}(*args)
|
51
|
+
Proxy::Collection.new(@context, @scope.#{name}(*args), @options)
|
52
|
+
end
|
53
|
+
EOM
|
54
|
+
end
|
55
|
+
|
56
|
+
# @private
|
57
|
+
# @macro [attach] delegate_as_destroyer
|
58
|
+
# A proxy for +$1+ method which works on a +:delete+ scope.
|
59
|
+
def self.delegate_as_destroyer(name)
|
60
|
+
class_eval(<<-EOM, __FILE__, __LINE__)
|
61
|
+
def #{name}(*args)
|
62
|
+
@restrictions.request_scope(:delete, @scope).#{name}(*args)
|
63
|
+
end
|
64
|
+
EOM
|
65
|
+
end
|
66
|
+
|
67
|
+
# @private
|
68
|
+
# @macro [attach] delegate_as_record
|
69
|
+
# A proxy for +$1+ method which returns a restricted record.
|
70
|
+
def self.delegate_as_record(name)
|
71
|
+
class_eval(<<-EOM, __FILE__, __LINE__)
|
72
|
+
def #{name}(*args)
|
73
|
+
@scope.#{name}(*args).restrict(@context, @options)
|
74
|
+
end
|
75
|
+
EOM
|
76
|
+
end
|
77
|
+
|
78
|
+
# @private
|
79
|
+
# @macro [attach] delegate_as_records
|
80
|
+
# A proxy for +$1+ method which returns an array of restricted records.
|
81
|
+
def self.delegate_as_records(name)
|
82
|
+
class_eval(<<-EOM, __FILE__, __LINE__)
|
83
|
+
def #{name}(*args)
|
84
|
+
@scope.#{name}(*args).map do |element|
|
85
|
+
element.restrict(@context, @options)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
EOM
|
89
|
+
end
|
90
|
+
|
91
|
+
# @private
|
92
|
+
# @macro [attach] delegate_as_value
|
93
|
+
# A proxy for +$1+ method which returns a raw value.
|
94
|
+
def self.delegate_as_value(name)
|
95
|
+
class_eval(<<-EOM, __FILE__, __LINE__)
|
96
|
+
def #{name}(*args)
|
97
|
+
@scope.#{name}(*args)
|
98
|
+
end
|
99
|
+
EOM
|
100
|
+
end
|
101
|
+
|
102
|
+
delegate_as_constructor :build, :assign_attributes
|
103
|
+
delegate_as_constructor :new, :assign_attributes
|
104
|
+
delegate_as_constructor :create, :update_attributes
|
105
|
+
delegate_as_constructor :create!, :update_attributes!
|
106
|
+
|
107
|
+
delegate_as_scope :scoped
|
108
|
+
delegate_as_scope :uniq
|
109
|
+
delegate_as_scope :where
|
110
|
+
delegate_as_scope :joins
|
111
|
+
delegate_as_scope :includes
|
112
|
+
delegate_as_scope :eager_load
|
113
|
+
delegate_as_scope :preload
|
114
|
+
delegate_as_scope :lock
|
115
|
+
delegate_as_scope :limit
|
116
|
+
delegate_as_scope :offset
|
117
|
+
delegate_as_scope :order
|
118
|
+
delegate_as_scope :reorder
|
119
|
+
delegate_as_scope :reverse_order
|
120
|
+
delegate_as_scope :extending
|
121
|
+
|
122
|
+
delegate_as_value :empty?
|
123
|
+
delegate_as_value :any?
|
124
|
+
delegate_as_value :many?
|
125
|
+
delegate_as_value :include?
|
126
|
+
delegate_as_value :exists?
|
127
|
+
delegate_as_value :size
|
128
|
+
delegate_as_value :length
|
129
|
+
|
130
|
+
delegate_as_value :calculate
|
131
|
+
delegate_as_value :count
|
132
|
+
delegate_as_value :average
|
133
|
+
delegate_as_value :sum
|
134
|
+
delegate_as_value :maximum
|
135
|
+
delegate_as_value :minimum
|
136
|
+
delegate_as_value :pluck
|
137
|
+
|
138
|
+
delegate_as_destroyer :delete
|
139
|
+
delegate_as_destroyer :delete_all
|
140
|
+
delegate_as_destroyer :destroy
|
141
|
+
delegate_as_destroyer :destroy_all
|
142
|
+
|
143
|
+
delegate_as_record :first
|
144
|
+
delegate_as_record :first!
|
145
|
+
delegate_as_record :last
|
146
|
+
delegate_as_record :last!
|
147
|
+
|
148
|
+
delegate_as_records :all
|
149
|
+
delegate_as_records :to_a
|
150
|
+
delegate_as_records :to_ary
|
151
|
+
|
152
|
+
# A proxy for +find+ which restricts the returned record or records.
|
153
|
+
#
|
154
|
+
# @return [Proxy::Record, Array<Proxy::Record>]
|
155
|
+
def find(*args)
|
156
|
+
result = @scope.find(*args)
|
157
|
+
|
158
|
+
if result.is_a? Enumerable
|
159
|
+
result.map do |element|
|
160
|
+
element.restrict(@context, @options)
|
161
|
+
end
|
162
|
+
else
|
163
|
+
result.restrict(@context, @options)
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
# A proxy for +each+ which restricts the yielded records.
|
168
|
+
#
|
169
|
+
# @yield [record]
|
170
|
+
# @yieldparam [Proxy::Record] record
|
171
|
+
def each
|
172
|
+
@scope.each do |record|
|
173
|
+
yield record.restrict(@context, @options)
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
# Wraps a scope or a record in a corresponding proxy.
|
178
|
+
def method_missing(method, *args)
|
179
|
+
if method =~ /^find_all_by/
|
180
|
+
@scope.send(method, *args).map do |element|
|
181
|
+
element.restrict(@context, @options)
|
182
|
+
end
|
183
|
+
elsif method =~ /^find_by/
|
184
|
+
@scope.send(method, *args).restrict(@context, @options)
|
185
|
+
elsif @scope.heimdallr_scopes && @scope.heimdallr_scopes.include?(method)
|
186
|
+
Proxy::Collection.new(@context, @scope.send(method, *args), @options)
|
187
|
+
elsif @scope.respond_to? method
|
188
|
+
raise InsecureOperationError,
|
189
|
+
"Potentially insecure method #{method} was called"
|
190
|
+
else
|
191
|
+
super
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
# Return the underlying scope.
|
196
|
+
#
|
197
|
+
# @return ActiveRecord scope
|
198
|
+
def insecure
|
199
|
+
@scope
|
200
|
+
end
|
201
|
+
|
202
|
+
# Describes the proxy and proxified scope.
|
203
|
+
#
|
204
|
+
# @return [String]
|
205
|
+
def inspect
|
206
|
+
"#<Heimdallr::Proxy::Collection: #{@scope.to_sql}>"
|
207
|
+
end
|
208
|
+
|
209
|
+
# Return the associated security metadata. The returned hash will contain keys
|
210
|
+
# +:context+, +:scope+ and +:options+, corresponding to the parameters in
|
211
|
+
# {#initialize}, and +:model+, representing the model class.
|
212
|
+
#
|
213
|
+
# Such a name was deliberately selected for this method in order to reduce namespace
|
214
|
+
# pollution.
|
215
|
+
#
|
216
|
+
# @return [Hash]
|
217
|
+
def reflect_on_security
|
218
|
+
{
|
219
|
+
model: @scope,
|
220
|
+
context: @context,
|
221
|
+
scope: @scope,
|
222
|
+
options: @options
|
223
|
+
}.merge(@restrictions.reflection)
|
26
224
|
end
|
27
225
|
end
|
28
226
|
end
|
@@ -5,12 +5,18 @@ module Heimdallr
|
|
5
5
|
#
|
6
6
|
# The #touch method call isn't considered a security threat and as such, it is
|
7
7
|
# forwarded to the underlying object directly.
|
8
|
+
#
|
9
|
+
# Record proxies can be of two types, implicit and explicit. Implicit proxies
|
10
|
+
# return +nil+ on access to methods forbidden by the current security context;
|
11
|
+
# explicit proxies raise an {Heimdallr::PermissionError} instead.
|
8
12
|
class Proxy::Record
|
9
13
|
# Create a record proxy.
|
10
|
-
#
|
11
|
-
# @param
|
12
|
-
|
13
|
-
|
14
|
+
#
|
15
|
+
# @param context security context
|
16
|
+
# @param object proxified record
|
17
|
+
# @option options [Boolean] implicit proxy type
|
18
|
+
def initialize(context, record, options={})
|
19
|
+
@context, @record, @options = context, record, options
|
14
20
|
|
15
21
|
@restrictions = @record.class.restrictions(context)
|
16
22
|
end
|
@@ -37,28 +43,34 @@ module Heimdallr
|
|
37
43
|
# A proxy for +attributes+ method which removes all attributes
|
38
44
|
# without +:view+ permission.
|
39
45
|
def attributes
|
40
|
-
@
|
46
|
+
@record.attributes.tap do |attributes|
|
47
|
+
attributes.keys.each do |key|
|
48
|
+
unless @restrictions.allowed_fields[:view].include? key.to_sym
|
49
|
+
attributes[key] = nil
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
41
53
|
end
|
42
54
|
|
43
|
-
# A proxy for +update_attributes+ method
|
44
|
-
#
|
55
|
+
# A proxy for +update_attributes+ method.
|
56
|
+
# See also {#save}.
|
45
57
|
#
|
46
58
|
# @raise [Heimdallr::PermissionError]
|
47
59
|
def update_attributes(attributes, options={})
|
48
60
|
@record.with_transaction_returning_status do
|
49
61
|
@record.assign_attributes(attributes, options)
|
50
|
-
|
62
|
+
save
|
51
63
|
end
|
52
64
|
end
|
53
65
|
|
54
|
-
# A proxy for +update_attributes!+ method
|
55
|
-
#
|
66
|
+
# A proxy for +update_attributes!+ method.
|
67
|
+
# See also {#save!}.
|
56
68
|
#
|
57
69
|
# @raise [Heimdallr::PermissionError]
|
58
|
-
def update_attributes(attributes, options={})
|
70
|
+
def update_attributes!(attributes, options={})
|
59
71
|
@record.with_transaction_returning_status do
|
60
72
|
@record.assign_attributes(attributes, options)
|
61
|
-
|
73
|
+
save!
|
62
74
|
end
|
63
75
|
end
|
64
76
|
|
@@ -93,13 +105,35 @@ module Heimdallr
|
|
93
105
|
class_eval(<<-EOM, __FILE__, __LINE__)
|
94
106
|
def #{method}
|
95
107
|
scope = @restrictions.request_scope(:delete)
|
96
|
-
if scope.where({ @record.primary_key => @record.to_key }).count != 0
|
108
|
+
if scope.where({ @record.class.primary_key => @record.to_key }).count != 0
|
97
109
|
@record.#{method}
|
98
110
|
end
|
99
111
|
end
|
100
112
|
EOM
|
101
113
|
end
|
102
114
|
|
115
|
+
# @method valid?
|
116
|
+
# @macro delegate
|
117
|
+
delegate :valid?, :to => :@record
|
118
|
+
|
119
|
+
# @method invalid?
|
120
|
+
# @macro delegate
|
121
|
+
delegate :invalid?, :to => :@record
|
122
|
+
|
123
|
+
# @method errors
|
124
|
+
# @macro delegate
|
125
|
+
delegate :errors, :to => :@record
|
126
|
+
|
127
|
+
# @method assign_attributes
|
128
|
+
# @macro delegate
|
129
|
+
delegate :assign_attributes, :to => :@record
|
130
|
+
|
131
|
+
# Class name of the underlying model.
|
132
|
+
# @return [String]
|
133
|
+
def class_name
|
134
|
+
@record.class.name
|
135
|
+
end
|
136
|
+
|
103
137
|
# Records cannot be restricted twice.
|
104
138
|
#
|
105
139
|
# @raise [RuntimeError]
|
@@ -131,28 +165,36 @@ module Heimdallr
|
|
131
165
|
suffix = nil
|
132
166
|
end
|
133
167
|
|
134
|
-
if defined?(ActiveRecord) && @record.is_a?(ActiveRecord::
|
135
|
-
association = @record.class.reflect_on_association(method)
|
168
|
+
if (defined?(ActiveRecord) && @record.is_a?(ActiveRecord::Reflection) &&
|
169
|
+
association = @record.class.reflect_on_association(method)) ||
|
170
|
+
(!@record.class.heimdallr_relations.nil? &&
|
171
|
+
@record.class.heimdallr_relations.include?(normalized_method))
|
136
172
|
referenced = @record.send(method, *args)
|
137
173
|
|
138
|
-
if referenced.
|
139
|
-
|
174
|
+
if referenced.nil?
|
175
|
+
nil
|
176
|
+
elsif referenced.respond_to? :restrict
|
177
|
+
referenced.restrict(@context, @options)
|
140
178
|
elsif Heimdallr.allow_insecure_associations
|
141
179
|
referenced
|
142
180
|
else
|
143
181
|
raise Heimdallr::InsecureOperationError,
|
144
|
-
"Attempt to fetch insecure association #{method}. Try #insecure
|
182
|
+
"Attempt to fetch insecure association #{method}. Try #insecure"
|
145
183
|
end
|
146
184
|
elsif @record.respond_to? method
|
147
|
-
if [nil, '?'].include?(suffix)
|
148
|
-
|
149
|
-
|
150
|
-
@
|
185
|
+
if [nil, '?'].include?(suffix)
|
186
|
+
if @restrictions.allowed_fields[:view].include?(normalized_method)
|
187
|
+
@record.send method, *args, &block
|
188
|
+
elsif @options[:implicit]
|
189
|
+
nil
|
190
|
+
else
|
191
|
+
raise Heimdallr::PermissionError, "Attempt to fetch non-whitelisted attribute #{method}"
|
192
|
+
end
|
151
193
|
elsif suffix == '='
|
152
194
|
@record.send method, *args
|
153
195
|
else
|
154
196
|
raise Heimdallr::PermissionError,
|
155
|
-
"Non-whitelisted method #{method} is called for #{@record.inspect}
|
197
|
+
"Non-whitelisted method #{method} is called for #{@record.inspect} "
|
156
198
|
end
|
157
199
|
else
|
158
200
|
super
|
@@ -166,16 +208,30 @@ module Heimdallr
|
|
166
208
|
@record
|
167
209
|
end
|
168
210
|
|
211
|
+
# Return an implicit variant of this proxy.
|
212
|
+
#
|
213
|
+
# @return [Heimdallr::Proxy::Record]
|
214
|
+
def implicit
|
215
|
+
Proxy::Record.new(@context, @record, @options.merge(implicit: true))
|
216
|
+
end
|
217
|
+
|
218
|
+
# Return an explicit variant of this proxy.
|
219
|
+
#
|
220
|
+
# @return [Heimdallr::Proxy::Record]
|
221
|
+
def explicit
|
222
|
+
Proxy::Record.new(@context, @record, @options.merge(implicit: false))
|
223
|
+
end
|
224
|
+
|
169
225
|
# Describes the proxy and proxified object.
|
170
226
|
#
|
171
227
|
# @return [String]
|
172
228
|
def inspect
|
173
|
-
"#<Heimdallr::Proxy
|
229
|
+
"#<Heimdallr::Proxy::Record: #{@record.inspect}>"
|
174
230
|
end
|
175
231
|
|
176
232
|
# Return the associated security metadata. The returned hash will contain keys
|
177
|
-
# +:context
|
178
|
-
# {#initialize}.
|
233
|
+
# +:context+, +:record+, +:options+, corresponding to the parameters in
|
234
|
+
# {#initialize}, and +:model+, representing the model class.
|
179
235
|
#
|
180
236
|
# Such a name was deliberately selected for this method in order to reduce namespace
|
181
237
|
# pollution.
|
@@ -183,9 +239,11 @@ module Heimdallr
|
|
183
239
|
# @return [Hash]
|
184
240
|
def reflect_on_security
|
185
241
|
{
|
242
|
+
model: @record.class,
|
186
243
|
context: @context,
|
187
|
-
|
188
|
-
|
244
|
+
record: @record,
|
245
|
+
options: @options
|
246
|
+
}.merge(@restrictions.reflection)
|
189
247
|
end
|
190
248
|
|
191
249
|
protected
|
@@ -203,10 +261,11 @@ module Heimdallr
|
|
203
261
|
action = :update
|
204
262
|
end
|
205
263
|
|
206
|
-
|
207
|
-
|
264
|
+
allowed_fields = @restrictions.allowed_fields[action]
|
265
|
+
fixtures = @restrictions.fixtures[action]
|
266
|
+
validators = @restrictions.validators[action]
|
208
267
|
|
209
|
-
@record.changed.each do |attribute|
|
268
|
+
@record.changed.map(&:to_sym).each do |attribute|
|
210
269
|
value = @record.send attribute
|
211
270
|
|
212
271
|
if fixtures.has_key? attribute
|
@@ -214,6 +273,9 @@ module Heimdallr
|
|
214
273
|
raise Heimdallr::PermissionError,
|
215
274
|
"Attribute #{attribute} value (#{value}) is not equal to a fixture (#{fixtures[attribute]})"
|
216
275
|
end
|
276
|
+
elsif !allowed_fields.include? attribute
|
277
|
+
raise Heimdallr::PermissionError,
|
278
|
+
"Attribute #{attribute} is not allowed to change"
|
217
279
|
end
|
218
280
|
end
|
219
281
|
|
@@ -231,6 +293,18 @@ module Heimdallr
|
|
231
293
|
raise Heimdallr::InsecureOperationError,
|
232
294
|
"Saving while omitting validation would omit security validations too"
|
233
295
|
end
|
296
|
+
|
297
|
+
if @record.new_record?
|
298
|
+
unless @restrictions.can? :create
|
299
|
+
raise Heimdallr::InsecureOperationError,
|
300
|
+
"Creating was not explicitly allowed"
|
301
|
+
end
|
302
|
+
else
|
303
|
+
unless @restrictions.can? :update
|
304
|
+
raise Heimdallr::InsecureOperationError,
|
305
|
+
"Updating was not explicitly allowed"
|
306
|
+
end
|
307
|
+
end
|
234
308
|
end
|
235
309
|
end
|
236
310
|
end
|
metadata
CHANGED
@@ -1,19 +1,20 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: heimdallr
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
5
|
-
prerelease:
|
4
|
+
version: 1.0.0.RC2
|
5
|
+
prerelease: 6
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
8
8
|
- Peter Zotov
|
9
|
+
- Boris Staal
|
9
10
|
autorequire:
|
10
11
|
bindir: bin
|
11
12
|
cert_chain: []
|
12
|
-
date: 2012-02
|
13
|
+
date: 2012-04-02 00:00:00.000000000 Z
|
13
14
|
dependencies:
|
14
15
|
- !ruby/object:Gem::Dependency
|
15
16
|
name: activesupport
|
16
|
-
requirement: &
|
17
|
+
requirement: &76679020 !ruby/object:Gem::Requirement
|
17
18
|
none: false
|
18
19
|
requirements:
|
19
20
|
- - ! '>='
|
@@ -21,10 +22,10 @@ dependencies:
|
|
21
22
|
version: 3.0.0
|
22
23
|
type: :runtime
|
23
24
|
prerelease: false
|
24
|
-
version_requirements: *
|
25
|
+
version_requirements: *76679020
|
25
26
|
- !ruby/object:Gem::Dependency
|
26
27
|
name: activemodel
|
27
|
-
requirement: &
|
28
|
+
requirement: &76678260 !ruby/object:Gem::Requirement
|
28
29
|
none: false
|
29
30
|
requirements:
|
30
31
|
- - ! '>='
|
@@ -32,10 +33,10 @@ dependencies:
|
|
32
33
|
version: 3.0.0
|
33
34
|
type: :runtime
|
34
35
|
prerelease: false
|
35
|
-
version_requirements: *
|
36
|
+
version_requirements: *76678260
|
36
37
|
- !ruby/object:Gem::Dependency
|
37
38
|
name: rspec
|
38
|
-
requirement: &
|
39
|
+
requirement: &76677800 !ruby/object:Gem::Requirement
|
39
40
|
none: false
|
40
41
|
requirements:
|
41
42
|
- - ! '>='
|
@@ -43,10 +44,10 @@ dependencies:
|
|
43
44
|
version: '0'
|
44
45
|
type: :development
|
45
46
|
prerelease: false
|
46
|
-
version_requirements: *
|
47
|
+
version_requirements: *76677800
|
47
48
|
- !ruby/object:Gem::Dependency
|
48
49
|
name: activerecord
|
49
|
-
requirement: &
|
50
|
+
requirement: &79365140 !ruby/object:Gem::Requirement
|
50
51
|
none: false
|
51
52
|
requirements:
|
52
53
|
- - ! '>='
|
@@ -54,12 +55,13 @@ dependencies:
|
|
54
55
|
version: '0'
|
55
56
|
type: :development
|
56
57
|
prerelease: false
|
57
|
-
version_requirements: *
|
58
|
+
version_requirements: *79365140
|
58
59
|
description: ! "Heimdallr aims to provide an easy to configure and efficient object-
|
59
60
|
and field-level access\n control solution, reusing proven patterns from gems like
|
60
61
|
CanCan and allowing one to manage permissions in a very\n fine-grained manner."
|
61
62
|
email:
|
62
63
|
- whitequark@whitequark.org
|
64
|
+
- boris@roundlake.ru
|
63
65
|
executables: []
|
64
66
|
extensions: []
|
65
67
|
extra_rdoc_files: []
|
@@ -75,10 +77,10 @@ files:
|
|
75
77
|
- heimdallr.gemspec
|
76
78
|
- lib/heimdallr.rb
|
77
79
|
- lib/heimdallr/evaluator.rb
|
80
|
+
- lib/heimdallr/legacy_resource.rb
|
78
81
|
- lib/heimdallr/model.rb
|
79
82
|
- lib/heimdallr/proxy/collection.rb
|
80
83
|
- lib/heimdallr/proxy/record.rb
|
81
|
-
- lib/heimdallr/resource.rb
|
82
84
|
- lib/heimdallr/validator.rb
|
83
85
|
- spec/proxy_spec.rb
|
84
86
|
- spec/spec_helper.rb
|
@@ -97,15 +99,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
97
99
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
98
100
|
none: false
|
99
101
|
requirements:
|
100
|
-
- - ! '
|
102
|
+
- - ! '>'
|
101
103
|
- !ruby/object:Gem::Version
|
102
|
-
version:
|
104
|
+
version: 1.3.1
|
103
105
|
requirements: []
|
104
|
-
rubyforge_project:
|
105
|
-
rubygems_version: 1.8.
|
106
|
+
rubyforge_project:
|
107
|
+
rubygems_version: 1.8.17
|
106
108
|
signing_key:
|
107
109
|
specification_version: 3
|
108
110
|
summary: Heimdallr is an ActiveModel extension which provides object- and field-level
|
109
111
|
access control.
|
110
112
|
test_files: []
|
111
|
-
has_rdoc:
|