strong_presenter 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.md +14 -6
- data/README.md +107 -30
- data/lib/strong_presenter/associable.rb +47 -7
- data/lib/strong_presenter/collection_presenter.rb +16 -0
- data/lib/strong_presenter/permissible.rb +9 -32
- data/lib/strong_presenter/permissions.rb +78 -75
- data/lib/strong_presenter/presenter.rb +2 -3
- data/lib/strong_presenter/presenter_helper_constructor.rb +1 -1
- data/lib/strong_presenter/version.rb +1 -1
- data/spec/dummy/app/controllers/posts_controller.rb +1 -1
- data/spec/dummy/app/mailers/post_mailer.rb +1 -1
- data/spec/strong_presenter/associable_spec.rb +197 -18
- data/spec/strong_presenter/collection_presenter_spec.rb +38 -6
- data/spec/strong_presenter/controller_additions_spec.rb +115 -0
- data/spec/strong_presenter/helper_proxy_spec.rb +53 -0
- data/spec/strong_presenter/permissible_spec.rb +6 -6
- data/spec/strong_presenter/permissions_spec.rb +81 -67
- data/spec/strong_presenter/presenter_spec.rb +83 -1
- data/spec/strong_presenter/view_context/build_strategy_spec.rb +116 -0
- data/spec/strong_presenter/view_context_spec.rb +154 -0
- data/spec/strong_presenter/view_helpers_spec.rb +8 -0
- data/spec/support/shared_examples/view_helpers.rb +39 -0
- data/strong_presenter.gemspec +2 -2
- metadata +74 -100
- checksums.yaml +0 -7
data/CHANGELOG.md
CHANGED
@@ -1,14 +1,22 @@
|
|
1
1
|
# StrongPresenter Changelog
|
2
2
|
|
3
|
-
## 0.2.0
|
3
|
+
## 0.2.0
|
4
4
|
|
5
|
-
|
5
|
+
This release focuses on tightening up security of the permit interface, along with a number of bug fixes.
|
6
6
|
|
7
|
-
-
|
8
|
-
-
|
9
|
-
-
|
7
|
+
- Renamed `permit` to `permit!` in Presenter, CollectionPresenter, Permissible
|
8
|
+
- Renamed `filter` to `select_permitted` in Presenter, CollectionPresenter, Permissible
|
9
|
+
- Permitted paths are only prefixes with wildcard endings if specified explicitly
|
10
|
+
- `[:comments, :*]` permits all the methods in each comment in the comments association, but not further associations
|
11
|
+
- `[:comments, :**]` permits all methods in all associations, no matter how deep (for example it permits `[:comments, :user, :username]`)
|
12
|
+
- Wildcards disabled for tainted paths, tainted paths are only permitted if the exact path has been permitted
|
13
|
+
- a path is tainted if any element in the array is tainted, for example `params[:extra_column].split(',')` is tainted
|
14
|
+
- `permit_all!` class method removed - use an initializer instead
|
15
|
+
- Permissions grouping removed - the permissions of each presenter is independent, with copy on write semantics.
|
16
|
+
- but permitting on a presenter will propagate permissions to the presenters of associations or collection items. This can be inefficient if an object has many associations or the collection has many items. It is recommended that `permit!` is called before associations or collection items are loaded
|
17
|
+
- Added `reload!`, which will reset the cache on association or collection presenters. This might be used if the underlying object has changed.
|
10
18
|
|
11
|
-
## 0.1.0
|
19
|
+
## 0.1.0
|
12
20
|
|
13
21
|
- Copied features from Draper gem (thanks):
|
14
22
|
- spec/spec_helper.rb, spec/integration, spec/dummy - much easier for me, since I have difficulties trying to get a dummy app working.
|
data/README.md
CHANGED
@@ -25,7 +25,7 @@ While there exist other gems for presentation, we hope to provide a more natural
|
|
25
25
|
|
26
26
|
Requires Rails. Rails 3.2+ is supported (probably works on 3.0, 3.1 as well).
|
27
27
|
|
28
|
-
Add this line to your application
|
28
|
+
Add this line to your application's Gemfile:
|
29
29
|
|
30
30
|
gem 'strong_presenter'
|
31
31
|
|
@@ -108,7 +108,7 @@ class UsersPresenter < StrongPresenter::CollectionPresenter
|
|
108
108
|
end
|
109
109
|
```
|
110
110
|
|
111
|
-
It wraps each item in the collection with the corresponding singular presenter inferred from the class name, but it can be set using the `:with` option in the constructor, or by calling `presents_with :user` (for example), in the class definition.
|
111
|
+
It wraps each item in the collection with the corresponding singular presenter inferred from the class name, but it can be set using the `:with` option in the constructor, or by calling `presents_with :user` (for example), in the class definition. The `::Collection` constant of each presenter will automatically be set to the collection presenter with a matching plural name.
|
112
112
|
|
113
113
|
### Model Associations
|
114
114
|
|
@@ -147,9 +147,18 @@ class UserController < ApplicationController
|
|
147
147
|
end
|
148
148
|
```
|
149
149
|
|
150
|
+
Subsequently, in our view, we can use:
|
151
|
+
|
152
|
+
```erb
|
153
|
+
Username: <%= user.username %><br>
|
154
|
+
Email: <%= user.email %><br>
|
155
|
+
```
|
156
|
+
|
150
157
|
If the `:with` option is not passed, it will be inferred by using the model class name. `:only` and `:except` sets the controller actions it applies to.
|
151
158
|
|
152
|
-
### Permit!
|
159
|
+
### Permit! and `present`
|
160
|
+
|
161
|
+
#### Basics
|
153
162
|
|
154
163
|
You can simply use the presenter in the view:
|
155
164
|
|
@@ -157,16 +166,16 @@ You can simply use the presenter in the view:
|
|
157
166
|
Avatar: <%= @user_presenter.avatar %>
|
158
167
|
```
|
159
168
|
|
160
|
-
However, sometimes you will want to display information only if it is permitted. To do this, simply pass the attribute symbols to the `presents` method on the presenter, and it will only display it if it is permitted. This is
|
169
|
+
However, sometimes you will want to display information only if it is permitted. To do this, simply pass the attribute symbols to the `presents` method on the presenter, and it will only display it if it is permitted. This is quite useful because it controls the display of not just the attribute value, but everything that is passed in the block. If no block is given, the results are returned in an array.
|
161
170
|
|
162
171
|
```erb
|
163
172
|
<% fields = { :username => "Username", :name => "Name", :email => "E-mail" } %>
|
164
|
-
<%
|
173
|
+
<% @user_presenter.presents *fields.keys do |key, value| %>
|
165
174
|
<b><%= fields[key] %>:</b> <%= value %><br>
|
166
175
|
<% end %>
|
167
176
|
```
|
168
177
|
|
169
|
-
The `present` method is also available to display a single attribute
|
178
|
+
The `present` method is also available to display a single attribute. If no block is given, the result is returned, ready to display.
|
170
179
|
|
171
180
|
```erb
|
172
181
|
<b>Hello <%= user.present :name %></b><br>
|
@@ -175,13 +184,13 @@ The `present` method is also available to display a single attribute:
|
|
175
184
|
<% end %>
|
176
185
|
```
|
177
186
|
|
178
|
-
To permit the display of the attributes, call `permit
|
187
|
+
To permit the display of the attributes, call `permit!` on the presenter with the attribute symbols in the controller.
|
179
188
|
|
180
189
|
```ruby
|
181
190
|
class UserController < ApplicationController
|
182
191
|
def show
|
183
192
|
@user = User.find(params[:id])
|
184
|
-
@user_presenter = UserPresenter.new(@user).permit :username, :name # but not :email
|
193
|
+
@user_presenter = UserPresenter.new(@user).permit! :username, :name # but not :email
|
185
194
|
end
|
186
195
|
end
|
187
196
|
```
|
@@ -191,8 +200,8 @@ You can also call it when using the `presents` method in the controller:
|
|
191
200
|
```ruby
|
192
201
|
class UserController < ApplicationController
|
193
202
|
presents :user do |presenter|
|
194
|
-
presenter.permit :username, :name
|
195
|
-
presenter.permit :email if current_user.admin?
|
203
|
+
presenter.permit! :username, :name
|
204
|
+
presenter.permit! :email if current_user.admin?
|
196
205
|
end
|
197
206
|
def show
|
198
207
|
@user = User.find(params[:id])
|
@@ -200,15 +209,13 @@ class UserController < ApplicationController
|
|
200
209
|
end
|
201
210
|
```
|
202
211
|
|
203
|
-
|
204
|
-
|
205
|
-
There is also a `filter` method to help you with tables:
|
212
|
+
There is also a `select_permitted` method to help you with tables. For example, we use `select_permitted` below to check which of the columns are visible.
|
206
213
|
|
207
214
|
```erb
|
208
215
|
<% fields = { :username => "Username", :name => "Name", :email => "E-mail" } %>
|
209
216
|
<table>
|
210
217
|
<tr>
|
211
|
-
<% @users_presenter.
|
218
|
+
<% @users_presenter.select_permitted( *fields.keys ) do |key| %>
|
212
219
|
<th><%= fields[key] %></th>
|
213
220
|
<% end %>
|
214
221
|
</tr>
|
@@ -222,9 +229,6 @@ There is also a `filter` method to help you with tables:
|
|
222
229
|
</table>
|
223
230
|
```
|
224
231
|
|
225
|
-
Here, we use filter to check which of the columns are visible, just like `presents` does. It returns an
|
226
|
-
array of only the visible columns, and we use our `fields` hash to label it.
|
227
|
-
|
228
232
|
We can decide what attributes to present based on a GET parameter input, for example:
|
229
233
|
|
230
234
|
```erb
|
@@ -233,31 +237,104 @@ We can decide what attributes to present based on a GET parameter input, for exa
|
|
233
237
|
<% end %>
|
234
238
|
```
|
235
239
|
|
236
|
-
Because of the
|
240
|
+
Because of the we permit each attribute individually, there is no danger that private information will be revealed.
|
241
|
+
|
242
|
+
#### Associations - Permissions Paths
|
243
|
+
|
244
|
+
We can permit association attributes by passing an array of symbols. For example, we might normally get an Article's author using `@article.author.name`. If we permit it:
|
245
|
+
|
246
|
+
```ruby
|
247
|
+
@article_presenter.permit! [:author, :name]
|
248
|
+
```
|
249
|
+
|
250
|
+
then we can use the `present` method to display it:
|
251
|
+
|
252
|
+
```erb
|
253
|
+
<% @article_presenter.present [:author, :name] do |value| %>
|
254
|
+
By <%= value %>
|
255
|
+
<% end %>
|
256
|
+
```
|
257
|
+
|
258
|
+
It is also possible to include arguments. If our presenter method takes 1 argument:
|
259
|
+
|
260
|
+
```ruby
|
261
|
+
class ArticlePresenter < StrongPresenter::Presenter
|
262
|
+
def comment_text(index)
|
263
|
+
object.comments[index].text
|
264
|
+
end
|
265
|
+
end
|
266
|
+
```
|
267
|
+
|
268
|
+
Our view can call the method using:
|
269
|
+
|
270
|
+
```erb
|
271
|
+
<% @article_presenter.permit! [:comment_text, 3] %>
|
272
|
+
<%= @article_presenter.present [:comment_text, 3] # this is displayed %>
|
273
|
+
<%= @article_presenter.present [:comment_text, 4] # this is not %>
|
274
|
+
```
|
275
|
+
|
276
|
+
Basically, if the first element is an association, the next element will be the method name instead, and so on. When the final method is determined, extra elements in the array are passed as arguments.
|
277
|
+
|
278
|
+
When considering whether the permission path (including arguments) is permitted, it is indifferent to the difference between strings and symbols. So permitting a particular string argument will also permit the symbol argument.
|
237
279
|
|
238
|
-
####
|
280
|
+
#### Wildcards
|
239
281
|
|
240
|
-
|
282
|
+
To permit every attribute in a presenter, we can use wildcards. For example, to allow the display of all attributes in the article, we can call:
|
283
|
+
|
284
|
+
```ruby
|
285
|
+
@article_presenter.permit! :*
|
286
|
+
```
|
241
287
|
|
242
|
-
|
243
|
-
|
288
|
+
We can then present any attribute:
|
289
|
+
|
290
|
+
```ruby
|
291
|
+
@article_presenter.present :text
|
292
|
+
```
|
293
|
+
|
294
|
+
However, association attributes will not be permitted, for example, `@article_presenter.author.name`. To allow those, we can use the wildcard in an array:
|
295
|
+
|
296
|
+
```ruby
|
297
|
+
@article_presenter.permit! [:author, :*]
|
244
298
|
```
|
245
299
|
|
246
|
-
|
300
|
+
It is also possible to permit all association attributes:
|
301
|
+
|
302
|
+
```ruby
|
303
|
+
@article_presenter.permit! :**
|
304
|
+
```
|
305
|
+
|
306
|
+
Note that the wildcard can only be used as the last element, so for example, `[:*, :name]` is not treated as a wildcard.
|
307
|
+
|
308
|
+
Instead of the single wildcard `:*`, we can simply call `permit_all!` on the presenter.
|
309
|
+
|
310
|
+
Any attribute permitted by a wildcard will show up using the `presents` method, except when the argument is tainted. For example, if we used `@article_presenter.permit! :**`, and in our view, had:
|
247
311
|
|
248
312
|
```erb
|
249
|
-
|
313
|
+
<% @article_presenter.presents :title, "subtitle".taint, [:author, :name].taint, ["author".taint, :email], :text do |value| %>
|
314
|
+
<%= value %><br>
|
315
|
+
<% end %>
|
250
316
|
```
|
251
317
|
|
252
|
-
|
318
|
+
then `:title` and `:text` attributes will be displayed, but the other tainted attributes will not be displayed. For example, attributes constructed using the `params` hash will be tainted. This is a security measure to prevent a bug similar to the mass-assignment vulnerability, where an arbitrary presenter method can be called.
|
319
|
+
|
320
|
+
#### Association Permissions
|
253
321
|
|
254
|
-
|
322
|
+
Association presenters will automatically inherit the permissions of its parent, and any new permissions will be propagated to the association presenter. For example:
|
255
323
|
|
256
|
-
|
324
|
+
```ruby
|
325
|
+
@article_presenter.permit! [:author, :name]
|
326
|
+
@author_presenter = @article_presenter.author
|
327
|
+
@author_presenter.present(:name) # this is permitted
|
328
|
+
@article_presenter.permit! [:author, :email]
|
329
|
+
@author_presenter.present(:email) # now this is also permitted
|
330
|
+
```
|
257
331
|
|
258
|
-
|
332
|
+
Attributes permitted by an item in a collection will not be permitted on the siblings:
|
259
333
|
|
260
|
-
|
334
|
+
```ruby
|
335
|
+
@articles_presenter[0].permit! :author
|
336
|
+
@articles_presenter[1].present(:author) # not permitted
|
337
|
+
```
|
261
338
|
|
262
339
|
### Testing
|
263
340
|
|
@@ -273,7 +350,7 @@ In tests, a view context is built to access helper methods. By default, it will
|
|
273
350
|
StrongPresenter::ViewContext.test_strategy :fast
|
274
351
|
```
|
275
352
|
|
276
|
-
In doing so, your presenters will no longer have access to your application
|
353
|
+
In doing so, your presenters will no longer have access to your application's helpers. If you need to selectively include such helpers, you can pass a block:
|
277
354
|
|
278
355
|
```ruby
|
279
356
|
StrongPresenter::ViewContext.test_strategy :fast do
|
@@ -3,6 +3,7 @@ module StrongPresenter
|
|
3
3
|
# Methods for defining presenter associations
|
4
4
|
module Associable
|
5
5
|
extend ActiveSupport::Concern
|
6
|
+
include StrongPresenter::Permissible
|
6
7
|
|
7
8
|
module ClassMethods
|
8
9
|
# Automatically wraps multiple associations.
|
@@ -19,13 +20,14 @@ module StrongPresenter
|
|
19
20
|
# @return [void]
|
20
21
|
def presents_association(association, options = {})
|
21
22
|
options.assert_valid_keys(:with, :scope)
|
23
|
+
association = association.to_sym
|
22
24
|
options[:with] = Associable.object_association_class(object_class, association) unless options.has_key? :with
|
23
25
|
presenter_associations[association] ||= StrongPresenter::PresenterAssociation.new(association, options) do |presenter|
|
24
26
|
presenter.link_permissions self, association
|
25
27
|
yield presenter if block_given?
|
26
28
|
end
|
27
29
|
define_method(association) do
|
28
|
-
self.class.send(:presenter_associations)[association].wrap(self)
|
30
|
+
presenter_associations[association] ||= self.class.send(:presenter_associations)[association].wrap(self)
|
29
31
|
end
|
30
32
|
end
|
31
33
|
|
@@ -54,18 +56,50 @@ module StrongPresenter
|
|
54
56
|
end
|
55
57
|
end
|
56
58
|
|
59
|
+
# Permits given attributes, with propagation to associations.
|
60
|
+
# @param (see StrongPresenter::Permissible#permit!)
|
61
|
+
def permit! *attribute_paths
|
62
|
+
super
|
63
|
+
attribute_paths.each do |path| # propagate permit to associations
|
64
|
+
path = Array(path)
|
65
|
+
if path == [:**]
|
66
|
+
presenter_associations.each_value { |presenter| presenter.permit! [:**]}
|
67
|
+
elsif path.size>1
|
68
|
+
association = path[0].to_sym
|
69
|
+
presenter_associations[association].permit! path.drop(1) if presenter_associations.has_key?(association)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
self
|
73
|
+
end
|
74
|
+
|
75
|
+
# Resets association presenters - clears the cache
|
76
|
+
def reload!
|
77
|
+
@presenter_associations.clear
|
78
|
+
self
|
79
|
+
end
|
80
|
+
|
81
|
+
private
|
82
|
+
def presenter_associations
|
83
|
+
@presenter_associations ||= {}
|
84
|
+
end
|
85
|
+
|
57
86
|
protected
|
58
87
|
|
59
88
|
# infer association class if possible
|
60
89
|
def self.object_association_class(object_class, association)
|
61
|
-
|
62
|
-
|
90
|
+
klass, collection = get_orm_association_info(object_class, association)
|
91
|
+
return nil if klass.nil?
|
92
|
+
"#{klass}Presenter".constantize
|
93
|
+
rescue NameError
|
94
|
+
get_collection_presenter(klass) if collection # else nil
|
95
|
+
end
|
96
|
+
|
97
|
+
def self.get_orm_association_info(object_class, association)
|
98
|
+
if descendant_of(object_class, "ActiveRecord::Reflection")
|
99
|
+
object_class.reflect_on_association(association).instance_eval { [klass, collection?] }
|
63
100
|
else
|
64
|
-
|
101
|
+
nil
|
65
102
|
end
|
66
|
-
"#{association_class}Presenter".constantize
|
67
|
-
rescue NameError
|
68
|
-
nil
|
69
103
|
end
|
70
104
|
|
71
105
|
def self.descendant_of(object_class, klass)
|
@@ -73,6 +107,12 @@ module StrongPresenter
|
|
73
107
|
rescue NameError
|
74
108
|
false
|
75
109
|
end
|
110
|
+
|
111
|
+
def self.get_collection_presenter(klass)
|
112
|
+
"#{"#{klass}".pluralize}Presenter".constantize
|
113
|
+
rescue NameError
|
114
|
+
StrongPresenter::CollectionPresenter
|
115
|
+
end
|
76
116
|
end
|
77
117
|
end
|
78
118
|
|
@@ -14,12 +14,28 @@ module StrongPresenter
|
|
14
14
|
options.assert_valid_keys(:with)
|
15
15
|
@object = object
|
16
16
|
@presenter_class = options[:with]
|
17
|
+
|
18
|
+
yield self if block_given?
|
17
19
|
end
|
18
20
|
|
19
21
|
def to_s
|
20
22
|
"#<#{self.class.name} of #{presenter_class || "inferred presenters"} for #{object.inspect}>"
|
21
23
|
end
|
22
24
|
|
25
|
+
# Permits given attributes, with propagation to collection items.
|
26
|
+
# @param (see StrongPresenter::Permissible#permit!)
|
27
|
+
def permit! *attribute_paths
|
28
|
+
super
|
29
|
+
@collection.each { |presenter| presenter.permit! *attribute_paths } unless @collection.nil?
|
30
|
+
self
|
31
|
+
end
|
32
|
+
|
33
|
+
# Resets item presenters - clears the cache
|
34
|
+
def reload!
|
35
|
+
@collection = nil
|
36
|
+
self
|
37
|
+
end
|
38
|
+
|
23
39
|
protected
|
24
40
|
# @return [Class] the presenter class used to present each item, as set by
|
25
41
|
# {#initialize}.
|
@@ -2,13 +2,6 @@ module StrongPresenter
|
|
2
2
|
module Permissible
|
3
3
|
extend ActiveSupport::Concern
|
4
4
|
|
5
|
-
module ClassMethods
|
6
|
-
# Permits all presenter attributes for presents, present & filter methods.
|
7
|
-
def permit!
|
8
|
-
define_method(:permitted_attributes){ @permitted_attributes ||= StrongPresenter::Permissions.new.permit_all! }
|
9
|
-
end
|
10
|
-
end
|
11
|
-
|
12
5
|
# Permits given attributes. May be invoked multiple times.
|
13
6
|
#
|
14
7
|
# @example Each argument represents a single attribute:
|
@@ -20,24 +13,23 @@ module StrongPresenter
|
|
20
13
|
# @param [[Symbols*]*] attribute_paths
|
21
14
|
# the attributes to permit. An array of symbols represents an attribute path.
|
22
15
|
# @return [self]
|
23
|
-
def permit *attribute_paths
|
24
|
-
permitted_attributes.permit
|
16
|
+
def permit! *attribute_paths
|
17
|
+
permitted_attributes.permit *attribute_paths
|
25
18
|
self
|
26
19
|
end
|
27
20
|
|
28
21
|
# Permits all presenter attributes for presents, present & filter methods.
|
29
|
-
def
|
22
|
+
def permit_all!
|
30
23
|
permitted_attributes.permit_all!
|
31
24
|
self
|
32
25
|
end
|
33
26
|
|
34
|
-
# Selects the attributes given which have been permitted - an array of attributes
|
35
|
-
#
|
36
|
-
# @param [Array<Symbol>*] attribute_paths
|
27
|
+
# Selects the attributes given which have been permitted - an array of attributes
|
28
|
+
# @param [Array<Symbols>*] attribute_paths
|
37
29
|
# the attribute paths to check. The attribute paths may also have arguments.
|
38
|
-
# @return [Array<Array<Symbol
|
39
|
-
def
|
40
|
-
select_permitted
|
30
|
+
# @return [Array<Array<Symbol>>] attribute (paths)
|
31
|
+
def select_permitted *attribute_paths
|
32
|
+
permitted_attributes.select_permitted *attribute_paths
|
41
33
|
end
|
42
34
|
|
43
35
|
protected
|
@@ -45,29 +37,14 @@ module StrongPresenter
|
|
45
37
|
@permitted_attributes ||= StrongPresenter::Permissions.new
|
46
38
|
end
|
47
39
|
|
48
|
-
# Selects the attributes given which have been permitted - an array of attributes
|
49
|
-
# Each returned attribute paths will be an array, even if it consists of only 1 symbol
|
50
|
-
# @param [Array<Symbols>*] attribute_paths
|
51
|
-
# the attribute paths to check. The attribute paths may also have arguments.
|
52
|
-
# @return [Array<Array<Symbol>>] attribute (paths)
|
53
|
-
def select_permitted *attribute_paths
|
54
|
-
permitted_attributes.select_permitted permissions_prefix, *attribute_paths
|
55
|
-
end
|
56
|
-
|
57
40
|
# Links presenter to permissions group of given presenter.
|
58
41
|
# @param [Presenter] parent_presenter
|
59
42
|
# @param [Array<Symbol>] relative_path
|
60
43
|
# The prefix prepended before every permission check relative to parent presenter.
|
61
44
|
def link_permissions parent_presenter, relative_path = []
|
62
|
-
|
63
|
-
@permitted_attributes = parent_presenter.send(:permitted_attributes).merge @permitted_attributes, permissions_prefix
|
45
|
+
@permitted_attributes = StrongPresenter::Permissions.new(parent_presenter.permitted_attributes, relative_path)
|
64
46
|
end
|
65
47
|
|
66
|
-
private
|
67
|
-
attr_writer :permissions_prefix
|
68
|
-
def permissions_prefix
|
69
|
-
@permissions_prefix ||= []
|
70
|
-
end
|
71
48
|
end
|
72
49
|
end
|
73
50
|
|