load_and_authorize_resource 0.1.0

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