heimdallr 0.0.1 → 0.0.2
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/.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
|