load_and_authorize_resource 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/README.md CHANGED
@@ -4,6 +4,14 @@ Auto-loads and authorizes resources in Rails 3 and up.
4
4
 
5
5
  This was inspired heavily by functionality in the [CanCan](https://github.com/ryanb/cancan) gem, but built to work mostly independent of any authorization library.
6
6
 
7
+ [Documentation](http://rubydoc.info/github/seven1m/load_and_authorize_resource/master/frames)
8
+
9
+ ## Mascot
10
+
11
+ This is LAAR. He's a horse my daughter drew.
12
+
13
+ ![LAAR](https://raw.github.com/seven1m/load_and_authorize_resource/master/mascot.png)
14
+
7
15
  ## Assumptions
8
16
 
9
17
  This library assumes your app follows some (fairly common) conventions:
@@ -13,6 +21,24 @@ This library assumes your app follows some (fairly common) conventions:
13
21
  3. Your User model has methods like `can_read?`, `can_update?`, `can_delete?`, etc. (This works great with [Authority](https://github.com/nathanl/authority) gem, but naturally can work with any authorization library, given you/it defines those methods.)
14
22
  4. You have a method on your controller that returns the resource parameters, e.g. `note_params`. You're probably already doing this if you're using [StrongParameters](https://github.com/rails/strong_parameters) or Rails 4.
15
23
 
24
+ ## Installing
25
+
26
+ Add to your Gemfile:
27
+
28
+ ```
29
+ gem 'load_and_authorize_resource'
30
+ ```
31
+
32
+ ...and run `bundle install`.
33
+
34
+ Then add the following to your ApplicationController:
35
+
36
+ ```ruby
37
+ class ApplicationController < ActionController::Base
38
+ include LoadAndAuthorizeResource
39
+ end
40
+ ```
41
+
16
42
  ## Loading and Authorizing the Resource
17
43
 
18
44
  ```ruby
@@ -38,6 +64,8 @@ For each controller action, `current_user.can_<action>?(@note)` is consulted. If
38
64
 
39
65
  This works very nicely along with the [Authority](https://github.com/nathanl/authority) gem.
40
66
 
67
+ If you don't wish to authorize, or if you wish to do the loading yourself, you can just call `load_resource` and/or `authorize_resource`. Also, each macro accepts the normal before_filter options such as `:only` and `:except` if you wish to only apply the filters to certain actions.
68
+
41
69
  ## Loading and Authorizing the Parent Resource
42
70
 
43
71
  Also loads and authorizes the parent resource(s)... given the following routes:
@@ -70,7 +98,11 @@ class NotesController < ApplicationController
70
98
  end
71
99
  ```
72
100
 
73
- Further, a private method is defined with the name of the resource that returns an ActiveRecord::Relation scoped to the `@parent` (if present). It basically looks like this:
101
+ If you don't wish to authorize, or if you wish to do the loading yourself, you can just call `load_parent` and/or `authorize_parent`. Also, each macro accepts the normal before_filter options such as `:only` and `:except` if you wish to only apply the filters to certain actions.
102
+
103
+ ### Accessing Children
104
+
105
+ When you setup to load a parent resoure, a private method is defined with the name of the child resource that returns an ActiveRecord::Relation scoped to the `@parent` (if present). It basically looks like this:
74
106
 
75
107
  ```ruby
76
108
  class NotesController < ApplicationController
@@ -78,9 +110,9 @@ class NotesController < ApplicationController
78
110
  private
79
111
 
80
112
  def notes
81
- if @parent
82
- @parent.notes.scoped
83
- else
113
+ if @person
114
+ @person.notes.scoped
115
+ elsif !required(:parent)
84
116
  Note.scoped
85
117
  end
86
118
  end
@@ -104,13 +136,13 @@ For parent resources, `current_user.can_read?(@parent)` is consulted. If false,
104
136
 
105
137
  If none of the parent IDs are present, e.g. `person_id` and `group_id` are both absent in `params`, then a `LoadAndAuthorizeResource::ParameterMissing` exception is raised.
106
138
 
107
- ### Shallow Routes
139
+ ### Shallow (Optional) Routes
108
140
 
109
- You can make the parent loading and authorization optional by setting the `shallow` option:
141
+ You can make the parent loading and authorization optional by making it `optional`:
110
142
 
111
143
  ```ruby
112
144
  class NotesController < ApplicationController
113
- load_and_authorize_parent :person, :group, shallow: true
145
+ load_and_authorize_parent :person, :group, optional: true
114
146
  end
115
147
  ```
116
148
 
@@ -6,6 +6,12 @@ module LoadAndAuthorizeResource
6
6
  class ParameterMissing < KeyError; end
7
7
  class AccessDenied < StandardError; end
8
8
 
9
+ # Controller method names to action verb mapping
10
+ #
11
+ # Other controller methods will use the name of the action, e.g.
12
+ # if your controller action is `rotate`, then it will be assumed
13
+ # to be your verb too: `current_user.can_rotate?(resource)`
14
+ #
9
15
  METHOD_TO_ACTION_NAMES = {
10
16
  'show' => 'read',
11
17
  'new' => 'create',
@@ -44,7 +50,7 @@ module LoadAndAuthorizeResource
44
50
  #
45
51
  # 1. look for `params[:person_id]`
46
52
  # 2. if present, call `Person.find(params[:person_id])`
47
- # 3. set @person and @parent
53
+ # 3. set @person
48
54
  #
49
55
  # If we've exhausted our list of potential parent resources without
50
56
  # seeing the needed parameter (:person_id or :group_id), then a
@@ -62,62 +68,90 @@ module LoadAndAuthorizeResource
62
68
  # load_parent :person, :group, shallow: true
63
69
  # end
64
70
  #
71
+ # The `:shallow` option is aliased to `:optional` in cases where it
72
+ # sense to think about parent resources that way. Further, you can
73
+ # call the macro more than once should you want to make some
74
+ # optional and some not:
75
+ #
76
+ # class NotesController < ApplicationController
77
+ # load_parent :person, group, optional: true
78
+ # load_parent :book
79
+ # end
80
+ #
65
81
  # Additionally, a private method is defined with the same name as
66
- # the resource. The method looks basically like this:
82
+ # the resource. The method looks basically like this (if you were
83
+ # to write it yourself):
67
84
  #
68
85
  # class NotesController < ApplicationController
69
86
  #
70
87
  # private
71
88
  #
72
89
  # def notes
73
- # if @parent
74
- # @parent.notes.scoped
75
- # else
90
+ # if @person
91
+ # @person.notes.scoped
92
+ # elsif not required(:person)
76
93
  # Note.scoped
77
94
  # end
78
95
  # end
79
96
  # end
80
97
  #
98
+ # You can change the name of this accessor if it is not the same
99
+ # as the resource this controller represents:
100
+ #
101
+ # class NotesController < ApplicationController
102
+ # load_parent :group, children: :people
103
+ # end
104
+ #
105
+ # This will create a private method called "people" that either returns
106
+ # `@group.people.scoped` or Person.scoped (only if @group is optional).
107
+ #
108
+ # @param names [Array<String, Symbol>] one or more names of resources in lower case
109
+ # @option options [Boolean] :optional set to true to allow non-nested routes, e.g. `/notes` in addition to `/people/1/notes`
110
+ # @option options [Boolean] :shallow (alias for :optional)
111
+ # @option options [Boolean] :except controller actions to ignore when applying this filter
112
+ # @option options [Boolean] :only controller actions to apply this filter
113
+ # @option options [String, Symbol] :children name of child accessor (inferred from controller name, e.g. "notes" for the NotesController)
114
+ #
81
115
  def load_parent(*names)
82
116
  options = names.extract_options!.dup
83
- self.nested_resource_options ||= {}
84
- self.nested_resource_options[:load] = {
85
- options: {shallow: options.delete(:shallow)},
86
- resources: names
87
- }
117
+ required = !(options.delete(:shallow) || options.delete(:optional))
118
+ save_nested_resource_options(:load, names, required)
119
+ define_scope_method(names, options.delete(:children))
88
120
  before_filter :load_parent, options
89
- define_scope_method
90
121
  end
91
122
 
92
123
  # Macro sets a before filter to authorize the parent resource.
93
- # Assumes there is a `@parent` variable.
124
+ # Assumes ther resource is already set (in a before filter).
94
125
  #
95
126
  # class NotesController < ApplicationController
96
- # authorize_parent
127
+ # authorize_parent :group
97
128
  # end
98
129
  #
99
- # If `@parent` is not found, or calling `current_user.can_read?(@parent)` fails,
130
+ # If `@group` is not found, or calling `current_user.can_read?(@group)` fails,
100
131
  # an exception will be raised.
101
132
  #
102
133
  # If the parent resource is optional, and you only want to check authorization
103
134
  # if it is set, you can set the `:shallow` option to `true`:
104
135
  #
105
136
  # class NotesController < ApplicationController
106
- # authorize_parent shallow: true
137
+ # authorize_parent :group, shallow: true
107
138
  # end
108
139
  #
109
- def authorize_parent(options={})
110
- self.nested_resource_options ||= {}
111
- self.nested_resource_options[:auth] = {
112
- options: {shallow: options.delete(:shallow)}
113
- }
140
+ # @option options [Boolean] :shallow set to true to allow non-nested routes, e.g. `/notes` in addition to `/people/1/notes`
141
+ # @option options [Boolean] :except controller actions to ignore when applying this filter
142
+ # @option options [Boolean] :only controller actions to apply this filter
143
+ #
144
+ def authorize_parent(*names)
145
+ options = names.extract_options!.dup
146
+ required = !(options.delete(:shallow) || options.delete(:optional))
147
+ save_nested_resource_options(:auth, names, required)
114
148
  before_filter :authorize_parent, options
115
149
  end
116
150
 
117
151
  # A convenience method for calling both `load_parent` and `authorize_parent`
118
152
  def load_and_authorize_parent(*names)
119
153
  load_parent(*names)
120
- authorize_parent(names.extract_options!)
154
+ authorize_parent(*names)
121
155
  end
122
156
 
123
157
  # Load the resource and set to an instance variable.
@@ -135,22 +169,28 @@ module LoadAndAuthorizeResource
135
169
  # new resource. For `create`, instantiates and
136
170
  # sets attributes to `<resource>_params`.
137
171
  #
172
+ # @option options [Boolean] :except controller actions to ignore when applying this filter
173
+ # @option options [Boolean] :only controller actions to apply this filter (default is show, new, create, edit, update, and destroy)
174
+ # @option options [String, Symbol] :children name of child accessor (inferred from controller name, e.g. "notes" for the NotesController)
175
+ #
138
176
  def load_resource(options={})
177
+ options = options.dup
139
178
  unless options[:only] or options[:except]
140
179
  options.reverse_merge!(only: [:show, :new, :create, :edit, :update, :destroy])
141
180
  end
181
+ define_scope_method([], options.delete(:children))
142
182
  before_filter :load_resource, options
143
- define_scope_method
144
183
  end
145
184
 
146
- # Checks authorization on resource by calling one of:
185
+ # Checks authorization on the already-loaded resource.
186
+ #
187
+ # This method calls `current_user.can_<action>?(@resource)` and raises an exception if the answer is 'no'.
147
188
  #
148
- # * `current_user.can_read?(@note)`
149
- # * `current_user.can_create?(@note)`
150
- # * `current_user.can_update?(@note)`
151
- # * `current_user.can_delete?(@note)`
189
+ # @option options [Boolean] :except controller actions to ignore when applying this filter
190
+ # @option options [Boolean] :only controller actions to apply this filter
152
191
  #
153
192
  def authorize_resource(options={})
193
+ options = options.dup
154
194
  unless options[:only] or options[:except]
155
195
  options.reverse_merge!(only: [:show, :new, :create, :edit, :update, :destroy])
156
196
  end
@@ -163,19 +203,50 @@ module LoadAndAuthorizeResource
163
203
  authorize_resource(options)
164
204
  end
165
205
 
206
+ # Returns the name of the resource, in singular form, e.g. "note"
207
+ #
208
+ # By default, this is simply `controller_name.singularize`.
209
+ #
210
+ def resource_name
211
+ controller_name.singularize
212
+ end
213
+
214
+ # Returns the name of the resource, in plural form, e.g. "notes"
215
+ #
216
+ # By default, this is simply the `controller_name`.
217
+ #
218
+ def resource_accessor_name
219
+ controller_name
220
+ end
221
+
166
222
  protected
167
223
 
168
224
  # Defines a method with the same name as the resource (`notes` for the NotesController)
169
225
  # that returns a scoped relation, either @parent.notes, or Note itself.
170
- def define_scope_method
171
- define_method(controller_name) do
172
- if @parent
173
- @parent.send(controller_name).scoped
174
- else
175
- controller_name.classify.constantize.scoped
226
+ def define_scope_method(parents, name=nil)
227
+ name ||= resource_accessor_name
228
+ self.nested_resource_options ||= {}
229
+ self.nested_resource_options[:accessors] ||= []
230
+ unless self.nested_resource_options[:accessors].include?(name)
231
+ self.nested_resource_options[:accessors] << name
232
+ define_method(name) do
233
+ parents.each do |parent|
234
+ if resource = instance_variable_get("@#{parent}")
235
+ return resource.send(name).scoped
236
+ end
237
+ end
238
+ name.to_s.classify.constantize.scoped
176
239
  end
240
+ private(name)
177
241
  end
178
- private(controller_name)
242
+ end
243
+
244
+ # Stores groups of names and options (required) on a class attribute on the controller
245
+ def save_nested_resource_options(key, names, required)
246
+ self.nested_resource_options ||= {}
247
+ self.nested_resource_options[key] ||= []
248
+ group = {resources: names, required: required}
249
+ self.nested_resource_options[key] << group
179
250
  end
180
251
  end
181
252
 
@@ -184,23 +255,24 @@ module LoadAndAuthorizeResource
184
255
  # Loop over each parent resource, and try to find a matching parameter.
185
256
  # Then lookup the resource using the supplied id.
186
257
  def load_parent
187
- keys = self.class.nested_resource_options[:load][:resources]
188
- parent = keys.detect do |key|
189
- if id = params["#{key}_id".to_sym]
190
- @parent = key.to_s.classify.constantize.find(id)
191
- instance_variable_set "@#{key}", @parent
258
+ self.class.nested_resource_options[:load].each do |group|
259
+ parent = group[:resources].detect do |key|
260
+ if id = params["#{key}_id".to_sym]
261
+ parent = key.to_s.classify.constantize.find(id)
262
+ instance_variable_set "@#{key}", parent
263
+ end
192
264
  end
265
+ verify_shallow_route!(group) unless parent
193
266
  end
194
- verify_shallow_route! unless @parent
195
267
  end
196
268
 
197
269
  # Loads/instantiates the resource object.
198
270
  def load_resource
199
- scope = send(controller_name)
271
+ scope = send(resource_accessor_name)
200
272
  if ['new', 'create'].include?(params[:action].to_s)
201
273
  resource = scope.new
202
274
  if 'create' == params[:action].to_s
203
- resource.attributes = send("#{controller_name.singularize}_params")
275
+ resource.attributes = send("#{resource_name}_params")
204
276
  end
205
277
  elsif params[:id]
206
278
  resource = scope.find(params[:id])
@@ -212,21 +284,26 @@ module LoadAndAuthorizeResource
212
284
 
213
285
  # Verify the current user is authorized to view the parent resource.
214
286
  # Assumes that `load_parent` has already been run and that `@parent` is set.
215
- # If `@parent` is empty and the `shallow` option is enabled, don't
216
- # perform any authorization check.
287
+ # If `@parent` is empty and the parent is optional, don't perform any
288
+ # authorization check.
217
289
  def authorize_parent
218
- if not @parent and not self.class.nested_resource_options[:auth][:options][:shallow]
219
- raise ParameterMissing.new('parent resource not found')
220
- end
221
- if @parent
222
- authorize_resource(@parent, :read)
290
+ self.class.nested_resource_options[:auth].each do |group|
291
+ group[:resources].each do |name|
292
+ parent = instance_variable_get("@#{name}")
293
+ if not parent and group[:required]
294
+ raise ParameterMissing.new('parent resource not found')
295
+ end
296
+ if parent
297
+ authorize_resource(parent, :read)
298
+ end
299
+ end
223
300
  end
224
301
  end
225
302
 
226
303
  # Asks the current_user if he/she is authorized to perform the given action.
227
304
  def authorize_resource(resource=nil, action=nil)
228
- resource ||= instance_variable_get("@#{controller_name.singularize}")
229
- action ||= METHOD_TO_ACTION_NAMES[params[:action].to_s]
305
+ resource ||= instance_variable_get("@#{resource_name}")
306
+ action ||= METHOD_TO_ACTION_NAMES[params[:action].to_s] || params[:action].presence
230
307
  raise ArgumentError unless resource and action
231
308
  unless current_user.send("can_#{action}?", resource)
232
309
  raise AccessDenied.new("#{current_user} cannot #{action} #{resource}")
@@ -234,15 +311,19 @@ module LoadAndAuthorizeResource
234
311
  end
235
312
 
236
313
  # Verify this shallow route is allowed, otherwise raise an exception.
237
- def verify_shallow_route!
238
- return if self.class.nested_resource_options[:load][:options][:shallow]
239
- expected = self.class.nested_resource_options[:load][:resources].map { |n| ":#{n}_id" }
314
+ def verify_shallow_route!(group)
315
+ return unless group[:required]
316
+ expected = group[:resources].map { |n| ":#{n}_id" }
240
317
  raise ParameterMissing.new(
241
318
  "must supply one of #{expected.join(', ')}"
242
319
  )
243
320
  end
244
321
 
245
322
  def resource_name
246
- controller_name.singularize
323
+ self.class.resource_name
324
+ end
325
+
326
+ def resource_accessor_name
327
+ self.class.resource_accessor_name
247
328
  end
248
329
  end
@@ -0,0 +1,37 @@
1
+ --- lib/load_and_authorize_resource.rb 2013-07-11 21:52:43.091465423 -0500
2
+ +++ /tmp/vu0jgwl/146 2013-07-12 14:27:46.872763565 -0500
3
+ @@ -178,6 +178,7 @@
4
+ unless options[:only] or options[:except]
5
+ options.reverse_merge!(only: [:show, :new, :create, :edit, :update, :destroy])
6
+ end
7
+ + define_scope_method([], options.delete(:children))
8
+ before_filter :load_resource, options
9
+ end
10
+
11
+ @@ -224,15 +225,19 @@
12
+ # that returns a scoped relation, either @parent.notes, or Note itself.
13
+ def define_scope_method(parents, name=nil)
14
+ name ||= resource_accessor_name
15
+ - define_method(name) do
16
+ - parents.each do |parent|
17
+ - if resource = instance_variable_get("@#{parent}")
18
+ - return resource.send(name).scoped
19
+ + nested_resource_options[:accessors] ||= []
20
+ + unless nested_resource_options[:accessors].include?(name)
21
+ + nested_resource_options[:accessors] << name
22
+ + define_method(name) do
23
+ + parents.each do |parent|
24
+ + if resource = instance_variable_get("@#{parent}")
25
+ + return resource.send(name).scoped
26
+ + end
27
+ end
28
+ + name.to_s.classify.constantize.scoped
29
+ end
30
+ - name.to_s.classify.constantize.scoped
31
+ + private(name)
32
+ end
33
+ - private(name)
34
+ end
35
+
36
+ # Stores groups of names and options (required) on a class attribute on the controller
37
+
metadata CHANGED
@@ -2,14 +2,14 @@
2
2
  name: load_and_authorize_resource
3
3
  version: !ruby/object:Gem::Version
4
4
  prerelease:
5
- version: 0.1.0
5
+ version: 0.2.0
6
6
  platform: ruby
7
7
  authors:
8
8
  - Tim Morgan
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-07-09 00:00:00.000000000 Z
12
+ date: 2013-07-12 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  prerelease: false
@@ -82,6 +82,7 @@ extensions: []
82
82
  extra_rdoc_files: []
83
83
  files:
84
84
  - README.md
85
+ - lib/load_and_authorize_resource.rb.20130712142746.patch
85
86
  - lib/load_and_authorize_resource.rb
86
87
  homepage: https://github.com/seven1m/load_and_authorize_resource
87
88
  licenses: []