load_and_authorize_resource 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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: []