heimdallr 0.0.1 → 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +2 -0
- data/.yardopts +1 -0
- data/LICENSE +19 -0
- data/README.md +128 -0
- data/README.yard.md +128 -0
- data/Rakefile +20 -0
- data/heimdallr.gemspec +2 -3
- data/lib/heimdallr.rb +39 -2
- data/lib/heimdallr/evaluator.rb +139 -41
- data/lib/heimdallr/model.rb +49 -18
- data/lib/heimdallr/proxy/collection.rb +28 -0
- data/lib/heimdallr/proxy/record.rb +236 -0
- data/lib/heimdallr/resource.rb +220 -80
- data/lib/heimdallr/validator.rb +15 -0
- metadata +19 -13
- data/lib/heimdallr/proxy.rb +0 -61
- data/lib/heimdallr/version.rb +0 -3
data/lib/heimdallr/model.rb
CHANGED
@@ -1,34 +1,65 @@
|
|
1
1
|
module Heimdallr
|
2
|
+
# Heimdallr is attached to your models by including the module and defining the
|
3
|
+
# restrictions in your classes.
|
4
|
+
#
|
5
|
+
# class Article < ActiveRecord::Base
|
6
|
+
# include Heimdallr::Model
|
7
|
+
#
|
8
|
+
# restrict do |context|
|
9
|
+
# # ...
|
10
|
+
# end
|
11
|
+
# end
|
12
|
+
#
|
13
|
+
# @todo Improve description
|
2
14
|
module Model
|
3
15
|
extend ActiveSupport::Concern
|
4
16
|
|
17
|
+
# Class methods for {Heimdallr::Model}. See also +ActiveSupport::Concern+.
|
5
18
|
module ClassMethods
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
19
|
+
# @overload restrict
|
20
|
+
# Define restrictions for a model with a DSL. See {Model} overview
|
21
|
+
# for DSL documentation.
|
22
|
+
#
|
23
|
+
# @yield A passed block is executed in the context of a new {Evaluator}.
|
24
|
+
#
|
25
|
+
# @overload restrict(context, action=:view)
|
26
|
+
# Return a secure collection object for the current scope.
|
27
|
+
#
|
28
|
+
# @param [Object] context security context
|
29
|
+
# @param [Symbol] action kind of actions which will be performed
|
30
|
+
# @return [Proxy::Collection]
|
31
|
+
def restrict(context=nil, &block)
|
32
|
+
if block
|
33
|
+
@restrictions = Evaluator.new(self, &block)
|
34
|
+
else
|
35
|
+
Proxy::Collection.new(context, restrictions(context).request_scope)
|
36
|
+
end
|
12
37
|
end
|
13
38
|
|
39
|
+
# Evaluate the restrictions for a given +context+.
|
40
|
+
#
|
41
|
+
# @return [Evaluator]
|
14
42
|
def restrictions(context)
|
15
43
|
@restrictions.evaluate(context)
|
16
44
|
end
|
17
45
|
end
|
18
46
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
end
|
26
|
-
end
|
47
|
+
# Return a secure proxy object for this record.
|
48
|
+
#
|
49
|
+
# @return [Record::Proxy]
|
50
|
+
def restrict(context, action)
|
51
|
+
Record::Proxy.new(context, self)
|
52
|
+
end
|
27
53
|
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
54
|
+
# @api private
|
55
|
+
#
|
56
|
+
# An internal attribute to store the Heimdallr security validators for
|
57
|
+
# the context in which this object is currently being saved.
|
58
|
+
attr_accessor :heimdallr_validators
|
59
|
+
|
60
|
+
def self.included(klass)
|
61
|
+
klass.const_eval do
|
62
|
+
validates_with Heimdallr::Validator
|
32
63
|
end
|
33
64
|
end
|
34
65
|
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Heimdallr
|
2
|
+
# A security-aware proxy for +ActiveRecord+ scopes. This class validates all the
|
3
|
+
# method calls and either forwards them to the encapsulated scope or raises
|
4
|
+
# an exception.
|
5
|
+
class Proxy::Collection
|
6
|
+
# Create a collection proxy.
|
7
|
+
# @param context security context
|
8
|
+
# @param object proxified scope
|
9
|
+
def initialize(context, scope)
|
10
|
+
@context, @scope = context, scope
|
11
|
+
|
12
|
+
@restrictions = @object.class.restrictions(context)
|
13
|
+
end
|
14
|
+
|
15
|
+
# Collections cannot be restricted twice.
|
16
|
+
#
|
17
|
+
# @raise [RuntimeError]
|
18
|
+
def restrict(context)
|
19
|
+
raise RuntimeError, "Collections cannot be restricted twice"
|
20
|
+
end
|
21
|
+
|
22
|
+
# Dummy method_missing.
|
23
|
+
# @todo Write some actual dispatching logic.
|
24
|
+
def method_missing(method, *args, &block)
|
25
|
+
@scope.send method, *args
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,236 @@
|
|
1
|
+
module Heimdallr
|
2
|
+
# A security-aware proxy for individual records. This class validates all the
|
3
|
+
# method calls and either forwards them to the encapsulated object or raises
|
4
|
+
# an exception.
|
5
|
+
#
|
6
|
+
# The #touch method call isn't considered a security threat and as such, it is
|
7
|
+
# forwarded to the underlying object directly.
|
8
|
+
class Proxy::Record
|
9
|
+
# Create a record proxy.
|
10
|
+
# @param context security context
|
11
|
+
# @param object proxified record
|
12
|
+
def initialize(context, record)
|
13
|
+
@context, @record = context, record
|
14
|
+
|
15
|
+
@restrictions = @record.class.restrictions(context)
|
16
|
+
end
|
17
|
+
|
18
|
+
# @method decrement(field, by=1)
|
19
|
+
# @macro [new] delegate
|
20
|
+
# Delegates to the corresponding method of underlying object.
|
21
|
+
delegate :decrement, :to => :@record
|
22
|
+
|
23
|
+
# @method increment(field, by=1)
|
24
|
+
# @macro delegate
|
25
|
+
delegate :increment, :to => :@record
|
26
|
+
|
27
|
+
# @method toggle(field)
|
28
|
+
# @macro delegate
|
29
|
+
delegate :toggle, :to => :@record
|
30
|
+
|
31
|
+
# @method touch(field)
|
32
|
+
# @macro delegate
|
33
|
+
# This method does not modify any fields except for the timestamp itself
|
34
|
+
# and thus is not considered as a potential security threat.
|
35
|
+
delegate :touch, :to => :@record
|
36
|
+
|
37
|
+
# A proxy for +attributes+ method which removes all attributes
|
38
|
+
# without +:view+ permission.
|
39
|
+
def attributes
|
40
|
+
@restrictions.filter_attributes(:view, @record.attributes)
|
41
|
+
end
|
42
|
+
|
43
|
+
# A proxy for +update_attributes+ method which removes all attributes
|
44
|
+
# without +:update+ permission and invokes +#save+.
|
45
|
+
#
|
46
|
+
# @raise [Heimdallr::PermissionError]
|
47
|
+
def update_attributes(attributes, options={})
|
48
|
+
@record.with_transaction_returning_status do
|
49
|
+
@record.assign_attributes(attributes, options)
|
50
|
+
self.save
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# A proxy for +update_attributes!+ method which removes all attributes
|
55
|
+
# without +:update+ permission and invokes +#save!+.
|
56
|
+
#
|
57
|
+
# @raise [Heimdallr::PermissionError]
|
58
|
+
def update_attributes(attributes, options={})
|
59
|
+
@record.with_transaction_returning_status do
|
60
|
+
@record.assign_attributes(attributes, options)
|
61
|
+
self.save!
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
# A proxy for +save+ method which verifies all of the dirty attributes to
|
66
|
+
# be valid for current security context.
|
67
|
+
#
|
68
|
+
# @raise [Heimdallr::PermissionError]
|
69
|
+
def save(options={})
|
70
|
+
check_save_options options
|
71
|
+
|
72
|
+
check_attributes do
|
73
|
+
@record.save(options)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
# A proxy for +save+ method which verifies all of the dirty attributes to
|
78
|
+
# be valid for current security context and mandates the current record
|
79
|
+
# to be valid.
|
80
|
+
#
|
81
|
+
# @raise [Heimdallr::PermissionError]
|
82
|
+
# @raise [ActiveRecord::RecordInvalid]
|
83
|
+
# @raise [ActiveRecord::RecordNotSaved]
|
84
|
+
def save!(options={})
|
85
|
+
check_save_options options
|
86
|
+
|
87
|
+
check_attributes do
|
88
|
+
@record.save!(options)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
[:delete, :destroy].each do |method|
|
93
|
+
class_eval(<<-EOM, __FILE__, __LINE__)
|
94
|
+
def #{method}
|
95
|
+
scope = @restrictions.request_scope(:delete)
|
96
|
+
if scope.where({ @record.primary_key => @record.to_key }).count != 0
|
97
|
+
@record.#{method}
|
98
|
+
end
|
99
|
+
end
|
100
|
+
EOM
|
101
|
+
end
|
102
|
+
|
103
|
+
# Records cannot be restricted twice.
|
104
|
+
#
|
105
|
+
# @raise [RuntimeError]
|
106
|
+
def restrict(context)
|
107
|
+
raise RuntimeError, "Records cannot be restricted twice"
|
108
|
+
end
|
109
|
+
|
110
|
+
# A whitelisting dispatcher for attribute-related method calls.
|
111
|
+
# Every unknown method is first normalized (that is, stripped of its +?+ or +=+
|
112
|
+
# suffix). Then, if the normalized form is whitelisted, it is passed to the
|
113
|
+
# underlying object as-is. Otherwise, an exception is raised.
|
114
|
+
#
|
115
|
+
# If the underlying object is an instance of ActiveRecord, then all association
|
116
|
+
# accesses are resolved and proxified automatically.
|
117
|
+
#
|
118
|
+
# Note that only the attribute and collection getters and setters are
|
119
|
+
# dispatched through this method. Every other model method should be defined
|
120
|
+
# as an instance method of this class in order to work.
|
121
|
+
#
|
122
|
+
# @raise [Heimdallr::PermissionError] when a non-whitelisted method is accessed
|
123
|
+
# @raise [Heimdallr::InsecureOperationError] when an insecure association is about
|
124
|
+
# to be fetched
|
125
|
+
def method_missing(method, *args, &block)
|
126
|
+
suffix = method.to_s[-1]
|
127
|
+
if %w(? = !).include? suffix
|
128
|
+
normalized_method = method[0..-2].to_sym
|
129
|
+
else
|
130
|
+
normalized_method = method
|
131
|
+
suffix = nil
|
132
|
+
end
|
133
|
+
|
134
|
+
if defined?(ActiveRecord) && @record.is_a?(ActiveRecord::Base) &&
|
135
|
+
association = @record.class.reflect_on_association(method)
|
136
|
+
referenced = @record.send(method, *args)
|
137
|
+
|
138
|
+
if referenced.respond_to? :restrict
|
139
|
+
referenced.restrict(@context)
|
140
|
+
elsif Heimdallr.allow_insecure_associations
|
141
|
+
referenced
|
142
|
+
else
|
143
|
+
raise Heimdallr::InsecureOperationError,
|
144
|
+
"Attempt to fetch insecure association #{method}. Try #insecure."
|
145
|
+
end
|
146
|
+
elsif @record.respond_to? method
|
147
|
+
if [nil, '?'].include?(suffix) &&
|
148
|
+
@restrictions.allowed_fields[:view].include?(normalized_method)
|
149
|
+
# Reading an attribute
|
150
|
+
@record.send method, *args, &block
|
151
|
+
elsif suffix == '='
|
152
|
+
@record.send method, *args
|
153
|
+
else
|
154
|
+
raise Heimdallr::PermissionError,
|
155
|
+
"Non-whitelisted method #{method} is called for #{@record.inspect} on #{@action}."
|
156
|
+
end
|
157
|
+
else
|
158
|
+
super
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
# Return the underlying object.
|
163
|
+
#
|
164
|
+
# @return [ActiveRecord::Base]
|
165
|
+
def insecure
|
166
|
+
@record
|
167
|
+
end
|
168
|
+
|
169
|
+
# Describes the proxy and proxified object.
|
170
|
+
#
|
171
|
+
# @return [String]
|
172
|
+
def inspect
|
173
|
+
"#<Heimdallr::Proxy(#{@action}): #{@record.inspect}>"
|
174
|
+
end
|
175
|
+
|
176
|
+
# Return the associated security metadata. The returned hash will contain keys
|
177
|
+
# +:context+ and +:object+, corresponding to the parameters in
|
178
|
+
# {#initialize}.
|
179
|
+
#
|
180
|
+
# Such a name was deliberately selected for this method in order to reduce namespace
|
181
|
+
# pollution.
|
182
|
+
#
|
183
|
+
# @return [Hash]
|
184
|
+
def reflect_on_security
|
185
|
+
{
|
186
|
+
context: @context,
|
187
|
+
object: @record
|
188
|
+
}
|
189
|
+
end
|
190
|
+
|
191
|
+
protected
|
192
|
+
|
193
|
+
# Raises an exception if any of the changed attributes are not valid
|
194
|
+
# for the current security context.
|
195
|
+
#
|
196
|
+
# @raise [Heimdallr::PermissionError]
|
197
|
+
def check_attributes
|
198
|
+
@record.errors.clear
|
199
|
+
|
200
|
+
if @record.new_record?
|
201
|
+
action = :create
|
202
|
+
else
|
203
|
+
action = :update
|
204
|
+
end
|
205
|
+
|
206
|
+
fixtures = @restrictions.fixtures[action]
|
207
|
+
validators = @restrictions.validators[action]
|
208
|
+
|
209
|
+
@record.changed.each do |attribute|
|
210
|
+
value = @record.send attribute
|
211
|
+
|
212
|
+
if fixtures.has_key? attribute
|
213
|
+
if fixtures[attribute] != value
|
214
|
+
raise Heimdallr::PermissionError,
|
215
|
+
"Attribute #{attribute} value (#{value}) is not equal to a fixture (#{fixtures[attribute]})"
|
216
|
+
end
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
@record.heimdallr_validators = validators
|
221
|
+
|
222
|
+
yield
|
223
|
+
ensure
|
224
|
+
@record.heimdallr_validators = nil
|
225
|
+
end
|
226
|
+
|
227
|
+
# Raises an exception if any of the +options+ intended for use in +save+
|
228
|
+
# methods are potentially unsafe.
|
229
|
+
def check_save_options(options)
|
230
|
+
if options[:validate] == false
|
231
|
+
raise Heimdallr::InsecureOperationError,
|
232
|
+
"Saving while omitting validation would omit security validations too"
|
233
|
+
end
|
234
|
+
end
|
235
|
+
end
|
236
|
+
end
|
data/lib/heimdallr/resource.rb
CHANGED
@@ -1,110 +1,250 @@
|
|
1
1
|
module Heimdallr
|
2
|
+
# Heimdallr {Resource} is a boilerplate for simple creation of REST endpoints, most of which are
|
3
|
+
# quite similar and thus may share a lot of code.
|
4
|
+
#
|
5
|
+
# The minimal controller possible would be:
|
6
|
+
#
|
7
|
+
# class MiceController < ApplicationController
|
8
|
+
# include Heimdallr::Resource
|
9
|
+
#
|
10
|
+
# # Class Mouse must include Heimdallr::Model.
|
11
|
+
# resource_for :mouse
|
12
|
+
# end
|
13
|
+
#
|
14
|
+
# Resource is built with Convention over Configuration principle in mind; that is,
|
15
|
+
# instead of providing complex configuration syntax, Resource consists of a lot of small, easy
|
16
|
+
# to override methods. If some kind of default behavior is undesirable, then one can just override
|
17
|
+
# the relative method in the particular controller or, say, define a module if the changes are
|
18
|
+
# to be shared between several controllers. You are encouraged to explore the source of this class.
|
19
|
+
#
|
20
|
+
# Resource allows to perform efficient operations on collections of objects. The
|
21
|
+
# {#create}, {#update} and {#destroy} actions accept both a single object/ID or an array of
|
22
|
+
# objects/IDs. The cardinal _modus
|
23
|
+
#
|
24
|
+
# Resource expects a method named +security_context+ to be defined either in the controller itself
|
25
|
+
# or, more conveniently, in any of its ancestors, likely +ApplicationController+. This method can
|
26
|
+
# often be aliased to +current_user+.
|
27
|
+
#
|
28
|
+
# Resource only works with ActiveRecord.
|
29
|
+
#
|
30
|
+
# See also {Resource::ClassMethods}.
|
2
31
|
module Resource
|
3
|
-
|
32
|
+
# @group Actions
|
4
33
|
|
5
|
-
|
6
|
-
|
34
|
+
# +GET /resources+
|
35
|
+
#
|
36
|
+
# This action does nothing by itself, but it has a +load_all_resources+ filter attached.
|
37
|
+
def index
|
38
|
+
end
|
7
39
|
|
8
|
-
|
9
|
-
|
40
|
+
# +GET /resource/1+
|
41
|
+
#
|
42
|
+
# This action does nothing by itself, but it has a +load_one_resource+ filter attached.
|
43
|
+
def show
|
44
|
+
end
|
10
45
|
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
46
|
+
# +GET /resources/new+
|
47
|
+
#
|
48
|
+
# This action renders a JSON representation of fields whitelisted for creation.
|
49
|
+
# It does not include any fixtures or validations.
|
50
|
+
#
|
51
|
+
# @example
|
52
|
+
# { 'fields': [ 'topic', 'content' ] }
|
53
|
+
def new
|
54
|
+
render :json => {
|
55
|
+
:fields => model.restrictions(security_context).allowed_fields[:create]
|
56
|
+
}
|
15
57
|
end
|
16
58
|
|
17
|
-
|
18
|
-
|
59
|
+
# +POST /resources+
|
60
|
+
#
|
61
|
+
# This action creates one or more records from the passed parameters.
|
62
|
+
# It can accept both arrays of attribute hashes and single attribute hashes.
|
63
|
+
#
|
64
|
+
# After the creation, it calls {#render_resources}.
|
65
|
+
#
|
66
|
+
# See also {#load_referenced_resources} and {#with_objects_from_params}.
|
67
|
+
def create
|
68
|
+
with_objects_from_params do |attributes, index|
|
69
|
+
scoped_model.restrict(security_context).
|
70
|
+
create!(attributes)
|
19
71
|
end
|
20
72
|
|
21
|
-
|
22
|
-
|
73
|
+
render_resources
|
74
|
+
end
|
75
|
+
|
76
|
+
# +GET /resources/1/edit+
|
77
|
+
#
|
78
|
+
# This action renders a JSON representation of fields whitelisted for updating.
|
79
|
+
# See also {#new}.
|
80
|
+
def edit
|
81
|
+
render :json => {
|
82
|
+
:fields => model.restrict(security_context).allowed_fields[:update]
|
83
|
+
}
|
84
|
+
end
|
23
85
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
86
|
+
# +PUT /resources/1,2+
|
87
|
+
#
|
88
|
+
# This action updates one or more records from the passed parameters.
|
89
|
+
# It expects resource IDs to be passed comma-separated in <tt>params[:id]</tt>,
|
90
|
+
# and expects them to be in the order corresponding to the order of actual
|
91
|
+
# attribute hashes.
|
92
|
+
#
|
93
|
+
# After the updating, it calls {#render_resources}.
|
94
|
+
#
|
95
|
+
# See also {#load_referenced_resources} and {#with_objects_from_params}.
|
96
|
+
def update
|
97
|
+
with_objects_from_params do |attributes, index|
|
98
|
+
@resources[index].update_attributes! attributes
|
28
99
|
end
|
29
100
|
|
30
|
-
|
31
|
-
|
32
|
-
if params.has_key? model.name.underscore
|
33
|
-
scoped_model.new.to_proxy(security_context, :create).
|
34
|
-
update_attributes!(params[model.name.underscore])
|
35
|
-
else
|
36
|
-
@resources.each_with_index do |resource, index|
|
37
|
-
scoped_model.new.to_proxy(security_context, :create).
|
38
|
-
update_attributes!(params[model.name.underscore.pluralize][index])
|
39
|
-
end
|
40
|
-
end
|
41
|
-
end
|
101
|
+
render_resources
|
102
|
+
end
|
42
103
|
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
104
|
+
# +DELETE /resources/1,2+
|
105
|
+
#
|
106
|
+
# This action destroys one or more records. It expects resource IDs to be passed
|
107
|
+
# comma-separated in <tt>params[:id]</tt>.
|
108
|
+
#
|
109
|
+
# See also {#load_referenced_resources}.
|
110
|
+
def destroy
|
111
|
+
model.transaction do
|
112
|
+
@resources.each &:destroy
|
48
113
|
end
|
49
114
|
|
50
|
-
|
51
|
-
|
52
|
-
:fields => model.restrictions(security_context).whitelist[:update]
|
53
|
-
}
|
54
|
-
end
|
115
|
+
render :json => {}, :status => :ok
|
116
|
+
end
|
55
117
|
|
56
|
-
|
57
|
-
model.transaction do
|
58
|
-
if params.has_key? model.name.underscore
|
59
|
-
@resources.first.to_proxy(security_context, :update).
|
60
|
-
update_attributes!(params[model.name.underscore])
|
61
|
-
else
|
62
|
-
@resources.each_with_index do |resource, index|
|
63
|
-
resource.to_proxy(security_context, :update).
|
64
|
-
update_attributes!(params[model.name.underscore.pluralize][index])
|
65
|
-
end
|
66
|
-
end
|
67
|
-
end
|
118
|
+
protected
|
68
119
|
|
69
|
-
|
70
|
-
yield
|
71
|
-
else
|
72
|
-
render_modified_resources
|
73
|
-
end
|
74
|
-
end
|
120
|
+
# @group Configuration
|
75
121
|
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
122
|
+
# Return the associated model class.
|
123
|
+
# @return [Class] associated model
|
124
|
+
def model
|
125
|
+
self.class.model
|
126
|
+
end
|
80
127
|
|
81
|
-
|
82
|
-
|
128
|
+
# Return the appropriately scoped model. By default this method
|
129
|
+
# delegates to +self.model.scoped+; you may override it for nested
|
130
|
+
# resources so that it would only return the nested set.
|
131
|
+
#
|
132
|
+
# For example, this code would not allow user to perform any actions
|
133
|
+
# with a transaction from a wrong account, raising RecordNotFound
|
134
|
+
# instead:
|
135
|
+
#
|
136
|
+
# # transactions_controller.rb
|
137
|
+
# class TransactionsController < ApplicationController
|
138
|
+
# include Heimdallr::Resource
|
139
|
+
#
|
140
|
+
# resource_for :transactions
|
141
|
+
#
|
142
|
+
# protected
|
143
|
+
#
|
144
|
+
# def scoped_model
|
145
|
+
# Account.find(params[:account_id]).transactions
|
146
|
+
# end
|
147
|
+
# end
|
148
|
+
#
|
149
|
+
# # routes.rb
|
150
|
+
# Foo::Application.routes.draw do
|
151
|
+
# resources :accounts do
|
152
|
+
# resources :transactions
|
153
|
+
# end
|
154
|
+
# end
|
155
|
+
#
|
156
|
+
# @return ActiveRecord scope
|
157
|
+
def scoped_model
|
158
|
+
self.model.scoped
|
159
|
+
end
|
83
160
|
|
84
|
-
|
161
|
+
# Loads all resources in the current scope to +@resources+.
|
162
|
+
#
|
163
|
+
# Is automatically applied to {#index}.
|
164
|
+
def load_all_resources
|
165
|
+
@resources = scoped_model
|
166
|
+
end
|
85
167
|
|
86
|
-
|
87
|
-
|
88
|
-
|
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])
|
174
|
+
end
|
89
175
|
|
90
|
-
|
91
|
-
|
92
|
-
|
176
|
+
# Loads several resources from the current scope, referenced by <code>params[:id]</code>
|
177
|
+
# with a comma-separated string like "1,2,3", to +@resources+.
|
178
|
+
#
|
179
|
+
# Is automatically applied to {#update} and {#destroy}.
|
180
|
+
def load_referenced_resources
|
181
|
+
@resources = scoped_model.find(params[:id].split(','))
|
182
|
+
end
|
93
183
|
|
94
|
-
|
95
|
-
|
96
|
-
|
184
|
+
# Render a modified collection in {#create}, {#update} and similar actions.
|
185
|
+
#
|
186
|
+
# By default, it invokes a template for +index+.
|
187
|
+
def render_resources
|
188
|
+
render :action => :index
|
189
|
+
end
|
97
190
|
|
98
|
-
|
99
|
-
|
191
|
+
# Fetch one or several objects passed in +params+ and yield them to a block,
|
192
|
+
# wrapping everything in a transaction.
|
193
|
+
#
|
194
|
+
# @yield [attributes, index]
|
195
|
+
# @yieldparam [Hash] attributes
|
196
|
+
# @yieldparam [Integer] index
|
197
|
+
def with_objects_from_params
|
198
|
+
model.transaction do
|
199
|
+
if params.has_key? model.name.underscore
|
200
|
+
yield params[model.name.underscore], 0
|
201
|
+
else
|
202
|
+
params[model.name.underscore.pluralize].
|
203
|
+
each_with_index do |attributes, index|
|
204
|
+
yield attributes, index
|
205
|
+
end
|
206
|
+
end
|
100
207
|
end
|
208
|
+
end
|
101
209
|
|
102
|
-
|
103
|
-
|
104
|
-
|
210
|
+
extend ActiveSupport::Concern
|
211
|
+
|
212
|
+
# Class methods for {Heimdallr::Resource}. See also +ActiveSupport::Concern+.
|
213
|
+
module ClassMethods
|
214
|
+
# Returns the attached model class.
|
215
|
+
# @return [Class]
|
216
|
+
attr_reader :model
|
217
|
+
|
218
|
+
# Attaches this resource to a model.
|
219
|
+
#
|
220
|
+
# Note that ActiveSupport +before_filter+ _replaces_ the list of actions for specified
|
221
|
+
# filter and not appends to it. For example, the following code will only run +filter_a+
|
222
|
+
# when +bar+ action is invoked:
|
223
|
+
#
|
224
|
+
# class FooController < ApplicationController
|
225
|
+
# before_filter :filter_a, :only => :foo
|
226
|
+
# before_filter :filter_a, :only => :bar
|
227
|
+
#
|
228
|
+
# def foo; end
|
229
|
+
# def bar; end
|
230
|
+
# end
|
231
|
+
#
|
232
|
+
# For convenience, you can pass additional actions to register with default filters in
|
233
|
+
# +options+. It is also possible to use +append_before_filter+.
|
234
|
+
#
|
235
|
+
# @param [Class] model An ActiveModel or ActiveRecord model class.
|
236
|
+
# @option options [Array<Symbol>] :index
|
237
|
+
# Additional actions to be covered by {Heimdallr::Resource#load_all_resources}.
|
238
|
+
# @option options [Array<Symbol>] :member
|
239
|
+
# Additional actions to be covered by {Heimdallr::Resource#load_one_resource}.
|
240
|
+
# @option options [Array<Symbol>] :collection
|
241
|
+
# Additional actions to be covered by {Heimdallr::Resource#load_referenced_resources}.
|
242
|
+
def resource_for(model, options={})
|
243
|
+
@model = model.to_s.camelize.constantize
|
105
244
|
|
106
|
-
|
107
|
-
|
245
|
+
before_filter :load_all_resources, only: [ :index ].concat(options[:all] || [])
|
246
|
+
before_filter :load_one_resource, only: [ :show ].concat(options[:member] || [])
|
247
|
+
before_filter :load_referenced_resources, only: [ :update, :destroy ].concat(options[:collection] || [])
|
108
248
|
end
|
109
249
|
end
|
110
250
|
end
|