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 CHANGED
@@ -1,14 +1,22 @@
1
1
  # StrongPresenter Changelog
2
2
 
3
- ## 0.2.0 In development
3
+ ## 0.2.0
4
4
 
5
- Changes to be made:
5
+ This release focuses on tightening up security of the permit interface, along with a number of bug fixes.
6
6
 
7
- - Remove permissions grouping - convert to copy on write
8
- - Remove permit_all! class method
9
- - Require explicit wildcard endings to attribute paths to permit everything `[:association, :nested_association, :*]` to permit all methods in the association
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 - Stable
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''s Gemfile:
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 especially powerful because it controls the display of not just the attribute value, but everything that is passed in the block.
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
- <% user.presents *fields.keys do |key, value| # user = @user_presenter because of the call to `presents` in the controller class %>
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` on the presenter with the attribute symbols in the controller.
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
- To remove authorization checks, simply call `permit!` on an instance of a presenter.
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.filter( *fields.keys ) do |key| %>
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 `permit` checks, there is no danger that private information will be revealed.
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&#39;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
- #### Permissions Paths
280
+ #### Wildcards
239
281
 
240
- Association methods can be permitted by passing an array.
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
- ```rb
243
- @article_presenter.permit :body, [:author, :name]
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
- Then, presenting it:
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
- <%= @article_presenter.present [:author, :name] %>
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
- is equivalent to `@article_presenter.author.name`, except it includes the permission check.
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
- #### Permissions Groups
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
- Currently, each group of presenters shares a single permissions object. Therefore, each element in a collection references the same permissions object. The presenter for each association also shares the same permissions object. This means that permitting a method will permit it for all presenters in the group, and it is not possible for two presenters in the same collection to have different methods permitted.
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
- Everytime you get a presenter through a collection or association, it will be added to the permissions group. To start a new group, you will need to initialize it yourself.
332
+ Attributes permitted by an item in a collection will not be permitted on the siblings:
259
333
 
260
- It is the intention that in version 0.2.0, permissions groups (for efficiency in a simple implementation), will be removed, and each new presenter will implement copy on write with the permissions object. This will retain efficiency where `permit` is called early before forking new presenters, but allow different permissions for each presenter. This will change the behaviour of what is considered permitted, but if `permit` is called before using the presenter, the behaviour will not change.
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''s helpers. If you need to selectively include such helpers, you can pass a block:
353
+ In doing so, your presenters will no longer have access to your application&#39;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
- if self.descendant_of(object_class, "ActiveRecord::Reflection")
62
- association_class = object_class.reflect_on_association(association).klass
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
- return nil
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 permissions_prefix, *attribute_paths
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 permit!
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. Attributes are
35
- # symbols, and attribute paths are arrays of symbols.
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>, Symbol>] attribute (paths)
39
- def filter *attribute_paths
40
- select_permitted(*attribute_paths).map{ |attribute| attribute.first if attribute.size == 1 } # un-pack symbol if array with single symbol
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
- self.permissions_prefix = parent_presenter.send(:permissions_prefix) + Array(relative_path)
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