load_and_authorize_resource 0.1.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 +139 -0
- data/lib/load_and_authorize_resource.rb +248 -0
- metadata +111 -0
data/README.md
ADDED
@@ -0,0 +1,139 @@
|
|
1
|
+
# Load And Authorize Resource
|
2
|
+
|
3
|
+
Auto-loads and authorizes resources in Rails 3 and up.
|
4
|
+
|
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
|
+
|
7
|
+
## Assumptions
|
8
|
+
|
9
|
+
This library assumes your app follows some (fairly common) conventions:
|
10
|
+
|
11
|
+
1. Your controller name matches your model name, e.g. "NotesController" for the "Note" model.
|
12
|
+
2. You have a method on your (Application)Controller called `current_user` that returns your User model.
|
13
|
+
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
|
+
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
|
+
|
16
|
+
## Loading and Authorizing the Resource
|
17
|
+
|
18
|
+
```ruby
|
19
|
+
class NotesController < ApplicationController
|
20
|
+
load_and_authorize_resource
|
21
|
+
|
22
|
+
def show
|
23
|
+
# @note is already loaded
|
24
|
+
end
|
25
|
+
|
26
|
+
def new
|
27
|
+
# @note is a new Note instance
|
28
|
+
end
|
29
|
+
|
30
|
+
def create
|
31
|
+
# @note is a new Note instance
|
32
|
+
# with attributes set from note_params
|
33
|
+
end
|
34
|
+
end
|
35
|
+
```
|
36
|
+
|
37
|
+
For each controller action, `current_user.can_<action>?(@note)` is consulted. If false, then an `LoadAndAuthorizeResource::AccessDenied` error is raised.
|
38
|
+
|
39
|
+
This works very nicely along with the [Authority](https://github.com/nathanl/authority) gem.
|
40
|
+
|
41
|
+
## Loading and Authorizing the Parent Resource
|
42
|
+
|
43
|
+
Also loads and authorizes the parent resource(s)... given the following routes:
|
44
|
+
|
45
|
+
```ruby
|
46
|
+
My::Application.routes.draw do
|
47
|
+
resources :people do
|
48
|
+
resources :notes
|
49
|
+
end
|
50
|
+
|
51
|
+
resources :groups do
|
52
|
+
resources :notes
|
53
|
+
end
|
54
|
+
end
|
55
|
+
```
|
56
|
+
|
57
|
+
... you can do this in your controller:
|
58
|
+
|
59
|
+
```ruby
|
60
|
+
class NotesController < ApplicationController
|
61
|
+
load_and_authorize_parent :person, :group
|
62
|
+
load_and_authorize_resource
|
63
|
+
|
64
|
+
def show
|
65
|
+
# for /people/1/notes/2
|
66
|
+
# @parent = @person = Person.find(1)
|
67
|
+
# for /groups/1/notes/2
|
68
|
+
# @parent = @group = Group.find(1)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
```
|
72
|
+
|
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:
|
74
|
+
|
75
|
+
```ruby
|
76
|
+
class NotesController < ApplicationController
|
77
|
+
|
78
|
+
private
|
79
|
+
|
80
|
+
def notes
|
81
|
+
if @parent
|
82
|
+
@parent.notes.scoped
|
83
|
+
else
|
84
|
+
Note.scoped
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
```
|
89
|
+
|
90
|
+
This allows you to easily access the set of notes that make sense given the URL, e.g.:
|
91
|
+
|
92
|
+
|
93
|
+
```ruby
|
94
|
+
class NotesController < ApplicationController
|
95
|
+
def index
|
96
|
+
# notes is basically equivalent to @group.notes, @person.notes, or just Note,
|
97
|
+
# for the urls /groups/1/notes, /people/1/notes, or /notes (respectively).
|
98
|
+
@notes = notes.order(:created_at).page(params[:page])
|
99
|
+
end
|
100
|
+
end
|
101
|
+
```
|
102
|
+
|
103
|
+
For parent resources, `current_user.can_read?(@parent)` is consulted. If false, then an `LoadAndAuthorizeResource::AccessDenied` error is raised.
|
104
|
+
|
105
|
+
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
|
+
|
107
|
+
### Shallow Routes
|
108
|
+
|
109
|
+
You can make the parent loading and authorization optional by setting the `shallow` option:
|
110
|
+
|
111
|
+
```ruby
|
112
|
+
class NotesController < ApplicationController
|
113
|
+
load_and_authorize_parent :person, :group, shallow: true
|
114
|
+
end
|
115
|
+
```
|
116
|
+
|
117
|
+
...this will allow all of the following URLs to work:
|
118
|
+
|
119
|
+
* `/people/1/notes/2` - `@person` will be set and authorized for reading
|
120
|
+
* `/groups/1/notes/2` - `@group` will be set and authorized for reading
|
121
|
+
* `/notes/2` - no parent will be set
|
122
|
+
|
123
|
+
## Rescuing Exceptions
|
124
|
+
|
125
|
+
You are encouraged to rescue the two possible exceptions in your ApplicationController, like so:
|
126
|
+
|
127
|
+
```ruby
|
128
|
+
class ApplicationController < ActionController::Base
|
129
|
+
rescue_from 'LoadAndAuthorizeResource::AccessDenied', 'LoadAndAuthorizeResource::ParameterMissing' do |exception|
|
130
|
+
render text: 'not authorized', status: :forbidden
|
131
|
+
end
|
132
|
+
end
|
133
|
+
```
|
134
|
+
|
135
|
+
## Author
|
136
|
+
|
137
|
+
Made with ❤ by [Tim Morgan](http://timmorgan.org).
|
138
|
+
|
139
|
+
Licensed under MIT license. Please use it, fork it, make it more awesome.
|
@@ -0,0 +1,248 @@
|
|
1
|
+
require 'active_support/concern'
|
2
|
+
|
3
|
+
module LoadAndAuthorizeResource
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
class ParameterMissing < KeyError; end
|
7
|
+
class AccessDenied < StandardError; end
|
8
|
+
|
9
|
+
METHOD_TO_ACTION_NAMES = {
|
10
|
+
'show' => 'read',
|
11
|
+
'new' => 'create',
|
12
|
+
'create' => 'create',
|
13
|
+
'edit' => 'update',
|
14
|
+
'update' => 'update',
|
15
|
+
'destroy' => 'delete'
|
16
|
+
}
|
17
|
+
|
18
|
+
included do
|
19
|
+
class_attribute :nested_resource_options
|
20
|
+
end
|
21
|
+
|
22
|
+
module ClassMethods
|
23
|
+
|
24
|
+
# Macro sets a before filter to load the parent resource.
|
25
|
+
# Pass in one symbol for each potential parent you're nested under.
|
26
|
+
#
|
27
|
+
# For example, if you have routes:
|
28
|
+
#
|
29
|
+
# resources :people do
|
30
|
+
# resources :notes
|
31
|
+
# end
|
32
|
+
#
|
33
|
+
# resources :groups do
|
34
|
+
# resources :notes
|
35
|
+
# end
|
36
|
+
#
|
37
|
+
# ...you can call load_parent like so in your controller:
|
38
|
+
#
|
39
|
+
# class NotesController < ApplicationController
|
40
|
+
# load_parent :person, :group
|
41
|
+
# end
|
42
|
+
#
|
43
|
+
# This will attempt to do the following for each resource, in order:
|
44
|
+
#
|
45
|
+
# 1. look for `params[:person_id]`
|
46
|
+
# 2. if present, call `Person.find(params[:person_id])`
|
47
|
+
# 3. set @person and @parent
|
48
|
+
#
|
49
|
+
# If we've exhausted our list of potential parent resources without
|
50
|
+
# seeing the needed parameter (:person_id or :group_id), then a
|
51
|
+
# LoadAndAuthorizeResource::ParameterMissing error is raised.
|
52
|
+
#
|
53
|
+
# Note: load_parent assumes you've only nested your route a single
|
54
|
+
# layer deep, e.g. /parents/1/children/2
|
55
|
+
# You're on your own if you want to load multiple nested
|
56
|
+
# parents, e.g. /grandfathers/1/parents/2/children/3
|
57
|
+
#
|
58
|
+
# If you wish to also allow shallow routes (no parent), you can
|
59
|
+
# set the `:shallow` option to `true`:
|
60
|
+
#
|
61
|
+
# class NotesController < ApplicationController
|
62
|
+
# load_parent :person, :group, shallow: true
|
63
|
+
# end
|
64
|
+
#
|
65
|
+
# Additionally, a private method is defined with the same name as
|
66
|
+
# the resource. The method looks basically like this:
|
67
|
+
#
|
68
|
+
# class NotesController < ApplicationController
|
69
|
+
#
|
70
|
+
# private
|
71
|
+
#
|
72
|
+
# def notes
|
73
|
+
# if @parent
|
74
|
+
# @parent.notes.scoped
|
75
|
+
# else
|
76
|
+
# Note.scoped
|
77
|
+
# end
|
78
|
+
# end
|
79
|
+
# end
|
80
|
+
#
|
81
|
+
def load_parent(*names)
|
82
|
+
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
|
+
}
|
88
|
+
before_filter :load_parent, options
|
89
|
+
define_scope_method
|
90
|
+
end
|
91
|
+
|
92
|
+
# Macro sets a before filter to authorize the parent resource.
|
93
|
+
# Assumes there is a `@parent` variable.
|
94
|
+
#
|
95
|
+
# class NotesController < ApplicationController
|
96
|
+
# authorize_parent
|
97
|
+
# end
|
98
|
+
#
|
99
|
+
# If `@parent` is not found, or calling `current_user.can_read?(@parent)` fails,
|
100
|
+
# an exception will be raised.
|
101
|
+
#
|
102
|
+
# If the parent resource is optional, and you only want to check authorization
|
103
|
+
# if it is set, you can set the `:shallow` option to `true`:
|
104
|
+
#
|
105
|
+
# class NotesController < ApplicationController
|
106
|
+
# authorize_parent shallow: true
|
107
|
+
# end
|
108
|
+
#
|
109
|
+
def authorize_parent(options={})
|
110
|
+
self.nested_resource_options ||= {}
|
111
|
+
self.nested_resource_options[:auth] = {
|
112
|
+
options: {shallow: options.delete(:shallow)}
|
113
|
+
}
|
114
|
+
before_filter :authorize_parent, options
|
115
|
+
end
|
116
|
+
|
117
|
+
# A convenience method for calling both `load_parent` and `authorize_parent`
|
118
|
+
def load_and_authorize_parent(*names)
|
119
|
+
load_parent(*names)
|
120
|
+
authorize_parent(names.extract_options!)
|
121
|
+
end
|
122
|
+
|
123
|
+
# Load the resource and set to an instance variable.
|
124
|
+
#
|
125
|
+
# For example:
|
126
|
+
#
|
127
|
+
# class NotesController < ApplicationController
|
128
|
+
# load_resource
|
129
|
+
# end
|
130
|
+
#
|
131
|
+
# ...automatically finds the note for actions
|
132
|
+
# `show`, `edit`, `update`, and `destroy`.
|
133
|
+
#
|
134
|
+
# For the `new` action, simply instantiates a
|
135
|
+
# new resource. For `create`, instantiates and
|
136
|
+
# sets attributes to `<resource>_params`.
|
137
|
+
#
|
138
|
+
def load_resource(options={})
|
139
|
+
unless options[:only] or options[:except]
|
140
|
+
options.reverse_merge!(only: [:show, :new, :create, :edit, :update, :destroy])
|
141
|
+
end
|
142
|
+
before_filter :load_resource, options
|
143
|
+
define_scope_method
|
144
|
+
end
|
145
|
+
|
146
|
+
# Checks authorization on resource by calling one of:
|
147
|
+
#
|
148
|
+
# * `current_user.can_read?(@note)`
|
149
|
+
# * `current_user.can_create?(@note)`
|
150
|
+
# * `current_user.can_update?(@note)`
|
151
|
+
# * `current_user.can_delete?(@note)`
|
152
|
+
#
|
153
|
+
def authorize_resource(options={})
|
154
|
+
unless options[:only] or options[:except]
|
155
|
+
options.reverse_merge!(only: [:show, :new, :create, :edit, :update, :destroy])
|
156
|
+
end
|
157
|
+
before_filter :authorize_resource, options
|
158
|
+
end
|
159
|
+
|
160
|
+
# A convenience method for calling both `load_resource` and `authorize_resource`
|
161
|
+
def load_and_authorize_resource(options={})
|
162
|
+
load_resource(options)
|
163
|
+
authorize_resource(options)
|
164
|
+
end
|
165
|
+
|
166
|
+
protected
|
167
|
+
|
168
|
+
# Defines a method with the same name as the resource (`notes` for the NotesController)
|
169
|
+
# 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
|
176
|
+
end
|
177
|
+
end
|
178
|
+
private(controller_name)
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
protected
|
183
|
+
|
184
|
+
# Loop over each parent resource, and try to find a matching parameter.
|
185
|
+
# Then lookup the resource using the supplied id.
|
186
|
+
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
|
192
|
+
end
|
193
|
+
end
|
194
|
+
verify_shallow_route! unless @parent
|
195
|
+
end
|
196
|
+
|
197
|
+
# Loads/instantiates the resource object.
|
198
|
+
def load_resource
|
199
|
+
scope = send(controller_name)
|
200
|
+
if ['new', 'create'].include?(params[:action].to_s)
|
201
|
+
resource = scope.new
|
202
|
+
if 'create' == params[:action].to_s
|
203
|
+
resource.attributes = send("#{controller_name.singularize}_params")
|
204
|
+
end
|
205
|
+
elsif params[:id]
|
206
|
+
resource = scope.find(params[:id])
|
207
|
+
else
|
208
|
+
resource = nil
|
209
|
+
end
|
210
|
+
instance_variable_set("@#{resource_name}", resource)
|
211
|
+
end
|
212
|
+
|
213
|
+
# Verify the current user is authorized to view the parent resource.
|
214
|
+
# 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.
|
217
|
+
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)
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
226
|
+
# Asks the current_user if he/she is authorized to perform the given action.
|
227
|
+
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]
|
230
|
+
raise ArgumentError unless resource and action
|
231
|
+
unless current_user.send("can_#{action}?", resource)
|
232
|
+
raise AccessDenied.new("#{current_user} cannot #{action} #{resource}")
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
236
|
+
# 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" }
|
240
|
+
raise ParameterMissing.new(
|
241
|
+
"must supply one of #{expected.join(', ')}"
|
242
|
+
)
|
243
|
+
end
|
244
|
+
|
245
|
+
def resource_name
|
246
|
+
controller_name.singularize
|
247
|
+
end
|
248
|
+
end
|
metadata
ADDED
@@ -0,0 +1,111 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: load_and_authorize_resource
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease:
|
5
|
+
version: 0.1.0
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Tim Morgan
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-07-09 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
prerelease: false
|
16
|
+
type: :runtime
|
17
|
+
name: rails
|
18
|
+
version_requirements: !ruby/object:Gem::Requirement
|
19
|
+
none: false
|
20
|
+
requirements:
|
21
|
+
- - ! '>='
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: '3.0'
|
24
|
+
requirement: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '3.0'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
prerelease: false
|
32
|
+
type: :development
|
33
|
+
name: rspec-rails
|
34
|
+
version_requirements: !ruby/object:Gem::Requirement
|
35
|
+
none: false
|
36
|
+
requirements:
|
37
|
+
- - ! '>='
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '0'
|
40
|
+
requirement: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
prerelease: false
|
48
|
+
type: :development
|
49
|
+
name: yard
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
none: false
|
52
|
+
requirements:
|
53
|
+
- - ! '>='
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: '0'
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
prerelease: false
|
64
|
+
type: :development
|
65
|
+
name: redcarpet
|
66
|
+
version_requirements: !ruby/object:Gem::Requirement
|
67
|
+
none: false
|
68
|
+
requirements:
|
69
|
+
- - ! '>='
|
70
|
+
- !ruby/object:Gem::Version
|
71
|
+
version: '0'
|
72
|
+
requirement: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ! '>='
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '0'
|
78
|
+
description:
|
79
|
+
email: tim@timmorgan.org
|
80
|
+
executables: []
|
81
|
+
extensions: []
|
82
|
+
extra_rdoc_files: []
|
83
|
+
files:
|
84
|
+
- README.md
|
85
|
+
- lib/load_and_authorize_resource.rb
|
86
|
+
homepage: https://github.com/seven1m/load_and_authorize_resource
|
87
|
+
licenses: []
|
88
|
+
post_install_message:
|
89
|
+
rdoc_options: []
|
90
|
+
require_paths:
|
91
|
+
- lib
|
92
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
93
|
+
none: false
|
94
|
+
requirements:
|
95
|
+
- - ! '>='
|
96
|
+
- !ruby/object:Gem::Version
|
97
|
+
version: '0'
|
98
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
99
|
+
none: false
|
100
|
+
requirements:
|
101
|
+
- - ! '>='
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
requirements: []
|
105
|
+
rubyforge_project:
|
106
|
+
rubygems_version: 1.8.25
|
107
|
+
signing_key:
|
108
|
+
specification_version: 3
|
109
|
+
summary: Auto-loads and authorizes resources in your controllers in Rails 3 and up.
|
110
|
+
test_files: []
|
111
|
+
has_rdoc: yard
|