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.
Files changed (3) hide show
  1. data/README.md +139 -0
  2. data/lib/load_and_authorize_resource.rb +248 -0
  3. 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