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 +39 -7
- data/lib/load_and_authorize_resource.rb +136 -55
- data/lib/load_and_authorize_resource.rb.20130712142746.patch +37 -0
- metadata +3 -2
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
|
+

|
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
|
-
|
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 @
|
82
|
-
@
|
83
|
-
|
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
|
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,
|
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
|
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 @
|
74
|
-
# @
|
75
|
-
#
|
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
|
-
|
84
|
-
|
85
|
-
|
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
|
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 `@
|
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
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
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
|
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
|
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
|
-
#
|
149
|
-
#
|
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
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
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
|
-
|
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
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
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(
|
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("#{
|
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
|
216
|
-
#
|
287
|
+
# If `@parent` is empty and the parent is optional, don't perform any
|
288
|
+
# authorization check.
|
217
289
|
def authorize_parent
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
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("@#{
|
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
|
239
|
-
expected =
|
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
|
-
|
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.
|
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-
|
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: []
|