heimdallr 0.0.2 → 1.0.0.RC2
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.
- 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:
|