authority 1.0.0.pre4 → 1.0.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/CHANGELOG.markdown CHANGED
@@ -2,6 +2,15 @@
2
2
 
3
3
  This is mainly to document major new features and backwards-incompatible changes.
4
4
 
5
+ ## v1.0.0
6
+
7
+ - Added `config.security_violation_handler` so users can specify which controller method to use when rescuing `SecurityViolation`s
8
+ - Removed generator to make blank authorizers. On further consideration, one authorizer per model is counterproductive for most use cases, and I'd rather not encourage misuse.
9
+
10
+ ## v1.0.0.pre4
11
+
12
+ Added generator to make blank authorizers. See `rails g authority:authorizers --help`.
13
+
5
14
  ## v1.0.0.pre3
6
15
 
7
16
  - Rename controller methods (again):
data/README.markdown CHANGED
@@ -1,8 +1,10 @@
1
1
  # Authority
2
2
 
3
- Authority gives you a clean and easy way to say, in your Rails app, **who** is allowed to do **what** with your models. Unauthorized actions get a warning and an entry in a log file.
3
+ Authority helps you authorize actions in your Rails app. It's **ORM-neutral** and has very little fancy syntax; just group your models under one or more Authorizer classes and write plain Ruby methods on them.
4
4
 
5
- It requires that you already have some kind of user object in your application, accessible from all controllers (like `current_user`).
5
+ Authority will work fine with a standalone app or a single sign-on system. You can check roles in a database or permissions in a YAML file. It doesn't care! What it **does** do is give you an easy way to organize your logic, define a default strategy, and handle unauthorized actions.
6
+
7
+ It requires that you already have some kind of user object in your application, accessible from all controllers and views via a method like `current_user` (configurable).
6
8
 
7
9
  [![Build Status](https://secure.travis-ci.org/nathanl/authority.png)](http://travis-ci.org/nathanl/authority)
8
10
 
@@ -19,9 +21,9 @@ It requires that you already have some kind of user object in your application,
19
21
  <li><a href="#models">Models</a></li>
20
22
  <li><a href="#authorizers">Authorizers</a>
21
23
  <ul>
22
- <li><a href="#custom_authorizers">Custom Authorizers</a></li>
23
24
  <li><a href="#default_strategies">Default strategies</a></li>
24
25
  <li><a href="#testing_authorizers">Testing Authorizers</a></li>
26
+ <li><a href="#custom_authorizers">Custom Authorizers</a></li>
25
27
  </ul></li>
26
28
  <li><a href="#controllers">Controllers</a></li>
27
29
  <li><a href="#views">Views</a></li>
@@ -36,29 +38,59 @@ It requires that you already have some kind of user object in your application,
36
38
 
37
39
  The goals of Authority are:
38
40
 
39
- - To allow broad, class-level rules. Examples:
41
+ - To allow broad, **class-level** rules. Examples:
40
42
  - "Basic users cannot delete any Widget."
41
43
  - "Only admin users can create Offices."
42
- - To allow fine-grained, instance-level rules. Examples:
44
+ - To allow fine-grained, **instance-level** rules. Examples:
43
45
  - "Management users can only edit schedules with date ranges in the future."
44
46
  - "Users can't create playlists more than 20 songs long unless they've paid."
45
47
  - To provide a clear syntax for permissions-based views. Example:
46
48
  - `link_to 'Edit Widget', edit_widget_path(@widget) if current_user.can_update?(@widget)`
47
- - To gracefully handle any access violations: display a "you can't do that" screen and log the violation.
49
+ - To gracefully handle any access violations: by default, it displays a "you can't do that" screen and logs the violation.
48
50
  - To do all this with minimal effort and mess.
49
51
 
50
52
  <a name="flow_of_authority">
51
53
  ## The flow of Authority
52
54
 
53
- Authority encapsulates all authorization logic in `Authorizer` classes. Want to do something with a model? Ask its authorizer.
55
+ Authority encapsulates all authorization logic in `Authorizer` classes. Want to do something with a model? **Ask its authorizer**.
56
+
57
+ You can group models under authorizers in any way you wish. For example:
58
+
59
+
60
+ +-------------+ +--------------+ +-------------+
61
+ |Simplest case| |Logical groups| |Most granular|
62
+ +-------------+ +--------------+ +-------------+
63
+
64
+ default_strategy default_strategy default_strategy
65
+ + + +
66
+ | +--------+-------+ +-------------------+-------------------+
67
+ + + + + + +
68
+ EverythingAuthorizer BasicAuthorizer AdminAuthorizer CommentAuthorizer ArticleAuthorizer EditionAuthorizer
69
+ + + + + + +
70
+ +-------+-------+ +-+ +------+ | | |
71
+ + + + + + + + + +
72
+ Comment Article Edition Comment Article Edition Comment Article Edition
73
+
54
74
 
55
75
  The process generally flows like this:
56
76
 
57
- - In a controller or view, a user object is asked whether it can do some action to a resource class or instance, like `current_user.can_create?(Widget)` or `current_user.can_update?(@widget)`.
58
- - The user just asks the model the same question: `resource.creatable_by?(self)`.
59
- - The model passes that question to its Authorizer, which actually contains the logic to answer the question.
60
- - The Authorizer returns an answer back up the call chain to the original caller.
61
- - If the answer is "no" and the original caller was a controller, this is treated as a `SecurityViolation`. If it was a view, maybe you just don't show a link.
77
+ current_user.can_create?(Moose) # You ask this question, and the user
78
+ + # automatically asks the model...
79
+ |
80
+ v
81
+ Moose.creatable_by?(current_user) # The model automatically asks
82
+ + # its authorizer...
83
+ |
84
+ v
85
+ MooseAuthorizer.creatable_by?(current_user) # *You define this method.*
86
+ + # If it's missing, the default
87
+ | # strategy is used...
88
+ v
89
+ config.default_strategy.call(:creatable, MooseAuthorizer, user) # *You define this strategy.*
90
+
91
+ If the answer is `false` and the original caller was a controller, this is treated as a `SecurityViolation`. If it was a view, maybe you just don't show a link.
92
+
93
+ (Diagrams made with [AsciiFlow](http://asciiflow.com))
62
94
 
63
95
  <a name="installation">
64
96
  ## Installation
@@ -70,12 +102,14 @@ Starting from a clean commit status, add `authority` to your Gemfile, `bundle`,
70
102
 
71
103
  Edit `config/initializers/authority.rb`. That file documents all your options, but one of particular interest is `config.abilities`, which defines the verbs and corresponding adjectives in your app. The defaults are:
72
104
 
73
- config.abilities = {
74
- :create => 'creatable',
75
- :read => 'readable',
76
- :update => 'updatable',
77
- :delete => 'deletable'
78
- }
105
+ ```ruby
106
+ config.abilities = {
107
+ :create => 'creatable',
108
+ :read => 'readable',
109
+ :update => 'updatable',
110
+ :delete => 'deletable'
111
+ }
112
+ ```
79
113
 
80
114
  This option determines what methods are added to your users, models and authorizers. If you need to ask `user.can_deactivate?(Satellite)` and `@satellite.deactivatable_by?(user)`, add those to the hash.
81
115
 
@@ -84,170 +118,215 @@ This option determines what methods are added to your users, models and authoriz
84
118
 
85
119
  <a name="users">
86
120
  ### Users
87
- In whatever class represents a logged-in user in your application, `include Authority::UserAbilities`.
121
+
122
+ ```ruby
123
+ # Whatever class represents a logged-in user in your app
124
+ class User
125
+ # Adds `can_create?(resource)`, etc
126
+ include Authority::Abilities
127
+ ...
128
+ end
129
+ ```
88
130
 
89
131
  <a name="models">
90
132
  ### Models
91
-
92
- In every model, `include Authority::Abilities`. Give every model an [Authorizer](#authorizers). By default, the `Llama` model will look for a `LlamaAuthorizer`. To specify a different one, call `authorizer_name UngulateAuthorizer`; this way, the `UngulateAuthorizer` could also protect the `Zebra` and `Antelope` models, or the `AdminAuthorizer` could protect business-critical models.
133
+
134
+ ```ruby
135
+ class Article
136
+ # Adds `creatable_by?(user)`, etc
137
+ include Authority::Abilities
138
+
139
+ # Without this, 'ArticleAuthorizer' is assumed
140
+ self.authorizer_name = 'AdminAuthorizer'
141
+ ...
142
+ end
143
+ ```
93
144
 
94
145
  <a name="authorizers">
95
146
  ### Authorizers
96
147
 
97
- Add your authorizers under `app/authorizers`, subclassing `Authority::Authorizer` or your own authorizer class. (See `rails g authority::authorizers --help`.)
148
+ Add your authorizers under `app/authorizers`, subclassing `Authority::Authorizer`.
98
149
 
99
150
  These are where your actual authorization logic goes. Here's how it works:
100
151
 
101
152
  - Instance methods answer questions about model instances, like "can this user update this **particular** widget?" (Within an instance method, you can get the model instance with `resource`).
102
153
  - Any instance method you don't define (for example, if you didn't make a `def deletable_by?(user)`) will fall back to the corresponding class method. In other words, if you haven't said whether a user can update **this particular** widget, we'll decide by checking whether they can update **any** widget.
103
154
  - Class methods answer questions about model classes, like "is it **ever** permissible for this user to update a Widget?"
104
- - Any class method you don't define (for example, if you didn't make a `def self.updatable_by?(user)`) will fall back to your [configurable default strategy](#default_strategies).
155
+ - Any class method you don't define (for example, if you didn't make a `def self.updatable_by?(user)`) will fall back to your configurable [default strategy](#default_strategies).
105
156
 
106
157
  For example:
107
158
 
108
- # app/authorizers/schedule_authorizer.rb
109
- class ScheduleAuthorizer < Authority::Authorizer
110
-
111
- # Class method: can this user at least sometimes create a Schedule?
112
- def self.creatable_by?(user)
113
- user.manager?
114
- end
115
-
116
- # Instance method: can this user delete this particular schedule?
117
- def deletable_by?(user)
118
- resource.in_future? && user.manager? && resource.department == user.department
119
- end
120
-
121
- end
159
+ ```ruby
160
+ # app/authorizers/schedule_authorizer.rb
161
+ class ScheduleAuthorizer < Authority::Authorizer
162
+ # Class method: can this user at least sometimes create a Schedule?
163
+ def self.creatable_by?(user)
164
+ user.manager?
165
+ end
166
+
167
+ # Instance method: can this user delete this particular schedule?
168
+ def deletable_by?(user)
169
+ resource.in_future? && user.manager? && resource.department == user.department
170
+ end
171
+ end
172
+ ```
122
173
 
123
174
  As you can see, you can specify different logic for every method on every model, if necessary. On the other extreme, you could simply supply a [default strategy](#default_strategies) that covers all your use cases.
124
175
 
125
- #### Custom Authorizers
126
-
127
- If you want to customize your authorizers even further - for example, maybe you want them all to have a method like `has_permission?(user, permission_name)` - you can insert a custom class into the inheritance chain.
128
-
129
- # lib/my_app/authorizer.rb
130
- module MyApp
131
- class Authorizer < Authority::Authorizer
132
-
133
- def self.has_permission(user, permission_name)
134
- # look that up somewhere
135
- end
136
-
137
- end
138
- end
139
-
140
- #app/authorizers/badger_authorizer.rb
141
- class BadgerAuthorizer < MyApp::Authorizer
142
- # contents
143
- end
144
-
145
- If you decide to place your custom class in `lib` as shown above (as opposed to putting it in `app`), you should require it at the bottom of `config/initializers/authority.rb`.
146
-
147
176
  <a name="default_strategies">
148
177
  #### Default Strategies
149
178
 
150
- Any class method you don't define on an authorizer will use your default strategy. The **default** default strategy simply returns false, meaning that everything is forbidden. This whitelisting approach will keep you from accidentally allowing things you didn't intend.
179
+ Any class method you don't define on an authorizer will use your default strategy. The **default** default strategy simply returns `false`, meaning that everything is forbidden. This whitelisting approach will keep you from accidentally allowing things you didn't intend.
151
180
 
152
181
  You can configure a different default strategy. For example, you might want one that looks up permissions in your database:
153
182
 
154
- # In config/initializers/authority.rb
155
- config.default_strategy = Proc.new { |able, authorizer, user|
156
- # Does the user have any of the roles which give this permission?
157
- (roles_which_grant(able, authorizer) & user.roles).any?
158
- }
183
+ ```ruby
184
+ # In config/initializers/authority.rb
185
+ config.default_strategy = Proc.new do |able, authorizer, user|
186
+ # Does the user have any of the roles which give this permission?
187
+ (roles_which_grant(able, authorizer) & user.roles).any?
188
+ end
189
+ ```
190
+
191
+ If your system is uniform enough, **this strategy alone might handle all the logic you need**.
159
192
 
160
193
  <a name="testing_authorizers">
161
194
  #### Testing Authorizers
162
195
 
163
196
  One nice thing about putting your authorization logic in authorizers is the ease of testing. Here's a brief example.
164
197
 
165
- # An authorizer shared by several admin-only models
166
- describe AdminAuthorizer do
198
+ ```ruby
199
+ # An authorizer shared by several admin-only models
200
+ describe AdminAuthorizer do
167
201
 
168
- before :each do
169
- @user = Factory.build(:user)
170
- @admin = Factory.build(:admin)
171
- end
202
+ before :each do
203
+ @user = FactoryGirl.build(:user)
204
+ @admin = FactoryGirl.build(:admin)
205
+ end
172
206
 
173
- describe "class" do
174
- it "should let admins update in bulk" do
175
- AdminAuthorizer.should be_bulk_updatable_by(@admin)
176
- end
207
+ describe "class" do
208
+ it "should let admins update in bulk" do
209
+ AdminAuthorizer.should be_bulk_updatable_by(@admin)
210
+ end
211
+
212
+ it "should not let users update in bulk" do
213
+ AdminAuthorizer.should_not be_bulk_updatable_by(@user)
214
+ end
215
+ end
216
+
217
+ describe "instances" do
218
+
219
+ before :each do
220
+ # A mock model that uses AdminAuthorizer
221
+ @admin_resource_instance = mock_admin_resource
222
+ end
177
223
 
178
- it "should not let users update in bulk" do
179
- AdminAuthorizer.should_not be_bulk_updatable_by(@user)
180
- end
181
- end
224
+ it "should not allow users to delete" do
225
+ @admin_resource_instance.authorizer.should_not be_deletable_by(@user)
226
+ end
182
227
 
183
- describe "instances" do
228
+ end
184
229
 
185
- before :each do
186
- # A mock model that uses AdminAuthorizer
187
- @admin_resource_instance = mock_admin_resource
188
- end
230
+ end
231
+ ```
189
232
 
190
- it "should not allow users to delete" do
191
- @admin_resource_instance.authorizer.should_not be_deletable_by(@user)
192
- end
233
+ <a name="custom_authorizers">
234
+ #### Custom Authorizers
193
235
 
194
- end
236
+ If you want to customize your authorizers even further - for example, maybe you want them all to have a method like `has_permission?(user, permission_name)` - just use normal Ruby inheritance. For example, add your own parent class, like this:
195
237
 
238
+ ```ruby
239
+ # lib/my_app/authorizer.rb
240
+ module MyApp
241
+ class Authorizer < Authority::Authorizer
242
+
243
+ def self.has_permission(user, permission_name)
244
+ # look that up somewhere
196
245
  end
197
246
 
247
+ end
248
+ end
249
+
250
+ #app/authorizers/badger_authorizer.rb
251
+ class BadgerAuthorizer < MyApp::Authorizer
252
+ # contents
253
+ end
254
+ ```
255
+
256
+ If you decide to place your custom class in `lib` as shown above (as opposed to putting it in `app`), you should require it at the bottom of `config/initializers/authority.rb`.
257
+
198
258
  <a name="controllers">
199
259
  ### Controllers
200
260
 
201
261
  Anytime a controller finds a user attempting something they're not authorized to do, a [Security Violation](#security_violations_and_logging) will result. Controllers get two ways to check authorization:
202
262
 
203
- - `authorize_actions_for Transaction` protects multiple controller actions with a `before_filter`, which performs a class-level check. If the current user is never allowed to delete a `Transaction`, they'll never even get to the controller's `destroy` method.
204
- - `authorize_action_for @transaction` can be called inside a single controller action, and performs an instance-level check. If called inside `update`, it will check whether the current user is allowed to update this particular `@transaction` instance.
263
+ - `authorize_actions_for Transaction` protects multiple controller actions with a `before_filter`, which performs a **class-level** check. If the current user is never allowed to delete a `Transaction`, they'll never even get to the controller's `destroy` method.
264
+ - `authorize_action_for @transaction` can be called inside a single controller action, and performs an **instance-level** check. If called inside `update`, it will check whether the current user is allowed to update this particular `@transaction` instance.
205
265
 
206
266
  The relationship between controller actions and abilities - like checking `readable_by?` on the `index` action - is configurable both globally, using `config.controller_action_map`, and per controller, as below.
207
267
 
208
- class LlamaController < ApplicationController
268
+ ```ruby
269
+ class LlamaController < ApplicationController
209
270
 
210
- # Check class-level authorizations before all actions except :create
211
- # Before this controller's 'neuter' action, ask whether current_user.can_update?(Llama)
212
- authorize_actions_for Llama, :actions => {:neuter => :update}, :except => :create
213
-
214
- # Before this controller's 'breed' action, ask whether current_user.can_create?(Llama)
215
- authority_action :breed => 'new'
271
+ # Check class-level authorizations before all actions except :create
272
+ # Before this controller's 'neuter' action, ask whether current_user.can_update?(Llama)
273
+ authorize_actions_for Llama, :actions => {:neuter => :update}, :except => :create
274
+
275
+ # Before this controller's 'breed' action, ask whether current_user.can_create?(Llama)
276
+ authority_action :breed => 'new'
216
277
 
217
- ...
278
+ ...
218
279
 
219
- def edit
220
- @llama = Llama.find(params[:id])
221
- @llama.attributes = params[:llama] # Don't save the attributes before authorizing
222
- authorize_action_for(@llama) # failure == SecurityViolation
223
- if @llama.save?
224
- # etc
225
- end
280
+ def edit
281
+ @llama = Llama.find(params[:id])
282
+ @llama.attributes = params[:llama] # Don't save the attributes before authorizing
283
+ authorize_action_for(@llama) # failure == SecurityViolation
284
+ if @llama.save?
285
+ # etc
286
+ end
226
287
 
227
- end
288
+ end
289
+ ```
228
290
 
229
291
  <a name="views">
230
292
  ### Views
231
293
 
232
294
  Assuming your user object is available in your views, you can do all kinds of conditional rendering. For example:
233
295
 
234
- link_to 'Edit Widget', edit_widget_path(@widget) if current_user.can_update?(@widget)
296
+ ```ruby
297
+ link_to 'Edit Widget', edit_widget_path(@widget) if current_user.can_update?(@widget)
298
+ ```
235
299
 
236
300
  If the user isn't allowed to edit widgets, they won't see the link. If they're nosy and try to hit the URL directly, they'll get a [Security Violation](#security_violations_and_logging) from the controller.
237
301
 
238
302
  <a name="security_violations_and_logging">
239
303
  ## Security Violations & Logging
240
304
 
241
- Anytime a user attempts an unauthorized action, Authority does two things:
305
+ Anytime a user attempts an unauthorized action, Authority calls whatever controller method is specified by your `security_violation_handler` option, handing it the exception. The default handler is `authority_forbidden`, which Authority adds to your `ApplicationController`. It does the following:
242
306
 
243
- - Renders your `public/403.html`
307
+ - Renders `public/403.html`
244
308
  - Logs the violation to whatever logger you configured.
245
309
 
246
- If you want to have nice log messages for security violations, you should ensure that your user object and models have `to_s` methods; this will control how they show up in log messages saying things like
310
+ You can specify a different handler like so:
311
+
312
+ ```ruby
313
+ # config/initializers/authority.rb
314
+ ...
315
+ config.security_violation_handler = :fire_ze_missiles
316
+ ...
317
+
318
+ # app/controllers/application_controller.rb
319
+ class ApplicationController < ActionController::Base
247
320
 
248
- "Kenneth Lay is not allowed to delete this resource: 'accounting_tricks.doc'"
321
+ def fire_ze_missiles(exception)
322
+ # Log? Set a flash message? Dispatch minions to
323
+ # fill their mailbox with goose droppings? It's up to you.
324
+ end
325
+ ...
326
+ end
327
+ ```
249
328
 
250
- If you feel like setting up a `cron` job to watch the log file, look up the user's name and address, and dispatch minions to fill their mailbox with goose droppings, that's really up to you. I got nothing to do with it, man.
329
+ If you want different error handling per controller, define `fire_ze_missiles` on each of them.
251
330
 
252
331
  <a name="credits">
253
332
  ## Credits, AKA 'Shout-Outs'
@@ -264,11 +343,12 @@ What should you contribute? Try the TODO file for ideas, or grep the project for
264
343
 
265
344
  How can you contribute?
266
345
 
267
- 1. Fork this project
268
- 2. Create your feature branch (`git checkout -b my-new-feature`)
269
- 3. `bundle install` to get all dependencies
270
- 4. `rspec spec` to run all tests.
271
- 5. Update/add tests for your changes and code until they pass.
272
- 6. Commit your changes (`git commit -am 'Added some feature'`)
273
- 7. Push to the branch (`git push origin my-new-feature`)
274
- 8. Create a new Pull Request
346
+ 1. Let's talk! Before you do a bunch of work, open an issue so we can be sure we agree.
347
+ 2. Fork this project
348
+ 3. Create your feature branch (`git checkout -b my-new-feature`)
349
+ 4. `bundle install` to get all dependencies
350
+ 5. `rspec spec` to run all tests.
351
+ 6. Update/add tests for your changes and code until they pass.
352
+ 7. Commit your changes (`git commit -am 'Added some feature'`)
353
+ 8. Push to the branch (`git push origin my-new-feature`)
354
+ 9. Create a new Pull Request
data/TODO.markdown CHANGED
@@ -1,14 +1,13 @@
1
1
  # TODO
2
2
 
3
- ## Design
4
-
5
- - Carefully think through names of all public methods & see if they could be clearer or more intuitive
6
-
7
3
  ## Chores
8
4
 
9
- - Test generators
10
- - Configurable proc for logging method
5
+ - Add tests for the generators
11
6
 
12
7
  ## Documentation
13
8
 
14
9
  - Example of checking clean/dirty attributes in instance-level checks. For example, if I'm only allowed to update blue laser cannons, can I make them red? Maybe I need to check whether the old value was blue?
10
+
11
+ ## Features
12
+
13
+ - It would be nice to have an `authorized_link_to` method, which determines from the given path and the user's permissions whether to show the link. Not sure yet how hard this would be.
data/authority.gemspec CHANGED
@@ -4,8 +4,8 @@ require File.expand_path('../lib/authority/version', __FILE__)
4
4
  Gem::Specification.new do |gem|
5
5
  gem.authors = ["Nathan Long", "Adam Hunter"]
6
6
  gem.email = ["nathanmlong@gmail.com", "adamhunter@me.com"]
7
- gem.description = %q{Gem for managing authorization on model actions in Rails.}
8
- gem.summary = %q{Authority gives you a clean and easy way to say, in your Rails app, **who** is allowed to do **what** with your models, with minimal clutter.}
7
+ gem.summary = %q{Authority helps you authorize actions in your Rails app using plain Ruby methods on Authorizer classes.}
8
+ gem.description = %q{Authority helps you authorize actions in your Rails app. It's ORM-neutral and has very little fancy syntax; just group your models under one or more Authorizer classes and write plain Ruby methods on them.}
9
9
  gem.homepage = "https://github.com/nathanl/authority"
10
10
 
11
11
  gem.add_dependency "rails", ">= 3.0.0"
@@ -3,12 +3,13 @@ module Authority
3
3
 
4
4
  # Has default settings, overrideable in the initializer.
5
5
 
6
- attr_accessor :default_strategy, :abilities, :controller_action_map, :user_method, :logger
6
+ attr_accessor :default_strategy, :abilities, :controller_action_map, :user_method, :security_violation_handler, :logger
7
7
 
8
8
  def initialize
9
- @default_strategy = Proc.new { |able, authorizer, user|
9
+ @default_strategy = Proc.new do |able, authorizer, user|
10
10
  false
11
- }
11
+ end
12
+
12
13
 
13
14
  @abilities = {
14
15
  :create => 'creatable',
@@ -29,6 +30,8 @@ module Authority
29
30
 
30
31
  @user_method = :current_user
31
32
 
33
+ @security_violation_handler = :authority_forbidden
34
+
32
35
  @logger = Logger.new(STDERR)
33
36
  end
34
37
 
@@ -6,10 +6,18 @@ module Authority
6
6
  extend ActiveSupport::Concern
7
7
 
8
8
  included do
9
- rescue_from Authority::SecurityViolation, :with => :authority_forbidden
9
+ rescue_from(Authority::SecurityViolation, :with => Authority::Controller.security_violation_callback)
10
10
  class_attribute :authority_resource
11
11
  end
12
12
 
13
+ def self.security_violation_callback
14
+ Proc.new do |exception|
15
+ # Through the magic of ActiveSupport's Proc#bind, `ActionController::Base#rescue_from`
16
+ # can call this proc and make `self` the actual controller instance
17
+ self.send(Authority.configuration.security_violation_handler, exception)
18
+ end
19
+ end
20
+
13
21
  module ClassMethods
14
22
 
15
23
  # Sets up before_filter to ensure user is allowed to perform a given controller action
@@ -37,6 +45,7 @@ module Authority
37
45
  def authority_action_map
38
46
  @authority_action_map ||= Authority.configuration.controller_action_map.dup
39
47
  end
48
+
40
49
  end
41
50
 
42
51
  protected
@@ -1,3 +1,3 @@
1
1
  module Authority
2
- VERSION = "1.0.0.pre4"
2
+ VERSION = "1.0.0"
3
3
  end
@@ -5,9 +5,32 @@ module Authority
5
5
  class InstallGenerator < Rails::Generators::Base
6
6
 
7
7
  source_root File.expand_path("../../templates", __FILE__)
8
-
9
8
  desc "Creates an Authority initializer for your application."
10
9
 
10
+ def do_all
11
+ copy_initializer
12
+ copy_forbidden
13
+ create_authorizers_directory
14
+ message = <<-RUBY
15
+
16
+ Install complete! See the README on Github for instructions on getting your
17
+ app running with Authority.
18
+
19
+ One note: each model needs to know the name of its its authorizer class.
20
+ You can specify that in the model like `authorizer_name FooAuthorizer`.
21
+ If you don't, the `Article` model (for example) will look for `ArticleAuthorizer`.
22
+
23
+ To generate one authorizer like that for each of your models, see
24
+ `rails g authority:authorizers`. If you also want to specify your own
25
+ parent class for them, use `rails g authority:authorizers MyClass`.
26
+
27
+ RUBY
28
+ puts message.strip_heredoc
29
+
30
+ end
31
+
32
+ private
33
+
11
34
  def copy_initializer
12
35
  template "authority_initializer.rb", "config/initializers/authority.rb"
13
36
  end
@@ -70,10 +70,18 @@ Authority.configure do |config|
70
70
  # :update => 'updatable',
71
71
  # :delete => 'deletable'
72
72
  # }
73
+
74
+ # SECURITY_VIOLATION_HANDLER
75
+ # If a SecurityViolation is raised, what controller method should be used to rescue it?
76
+ #
77
+ # Default is:
78
+ #
79
+ # config.security_violation_handler = :authority_forbidden # Defined in controller.rb
73
80
 
74
81
  # LOGGER
75
82
  # If a user tries to perform an unauthorized action, where should we log that fact?
76
- # Provide a logger object which responds to `.warn(message)`
83
+ # Provide a logger object which responds to `.warn(message)`, unless your
84
+ # security_violation_handler calls a different method.
77
85
  #
78
86
  # Default is:
79
87
  #
@@ -3,14 +3,41 @@ require 'support/ability_model'
3
3
  require 'support/example_controllers'
4
4
  require 'support/mock_rails'
5
5
  require 'support/user'
6
+ require 'active_support/core_ext/proc'
6
7
 
7
8
  describe Authority::Controller do
8
9
 
10
+ describe "the security violation callback" do
11
+
12
+ it "should call whatever method on the controller that the configuration specifies" do
13
+ # Here be dragons!
14
+ @fake_exception = Exception.new
15
+ @sample_controller = SampleController.new
16
+ # If a callback is passed to a controller's `rescue_from` method as the value for
17
+ # the `with` option (like `SomeController.rescue_from FooException, :with => some_callback`),
18
+ # Rails will use ActiveSupport's `Proc#bind` to ensure that when the proc refers to
19
+ # `self`, it will be the controller, not the proc itself.
20
+ # I need this callback's `self` to be the controller for the purposes of
21
+ # this test, so I'm stealing that behavior.
22
+ @callback = Authority::Controller.security_violation_callback.bind(@sample_controller)
23
+
24
+ Authority.configuration.security_violation_handler = :fire_ze_missiles
25
+ @sample_controller.should_receive(:fire_ze_missiles).with(@fake_exception)
26
+ @callback.call(@fake_exception)
27
+ end
28
+ end
29
+
9
30
  describe "when including" do
10
- it "should specify rescuing security transgressions" do
11
- SampleController.should_receive(:rescue_from).with(Authority::SecurityViolation, :with => :authority_forbidden)
31
+
32
+ before :each do
33
+ Authority::Controller.stub(:security_violation_callback).and_return(Proc.new {|exception| })
34
+ end
35
+
36
+ it "should specify rescuing security violations with a standard callback" do
37
+ SampleController.should_receive(:rescue_from).with(Authority::SecurityViolation, :with => Authority::Controller.security_violation_callback)
12
38
  SampleController.send(:include, Authority::Controller)
13
39
  end
40
+
14
41
  end
15
42
 
16
43
  describe "after including" do
metadata CHANGED
@@ -1,8 +1,8 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: authority
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0.pre4
5
- prerelease: 6
4
+ version: 1.0.0
5
+ prerelease:
6
6
  platform: ruby
7
7
  authors:
8
8
  - Nathan Long
@@ -10,11 +10,11 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2012-03-26 00:00:00.000000000 Z
13
+ date: 2012-04-17 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: rails
17
- requirement: &2152325920 !ruby/object:Gem::Requirement
17
+ requirement: &82923350 !ruby/object:Gem::Requirement
18
18
  none: false
19
19
  requirements:
20
20
  - - ! '>='
@@ -22,10 +22,10 @@ dependencies:
22
22
  version: 3.0.0
23
23
  type: :runtime
24
24
  prerelease: false
25
- version_requirements: *2152325920
25
+ version_requirements: *82923350
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: bundler
28
- requirement: &2152325380 !ruby/object:Gem::Requirement
28
+ requirement: &82923030 !ruby/object:Gem::Requirement
29
29
  none: false
30
30
  requirements:
31
31
  - - ! '>='
@@ -33,8 +33,10 @@ dependencies:
33
33
  version: 1.0.0
34
34
  type: :development
35
35
  prerelease: false
36
- version_requirements: *2152325380
37
- description: Gem for managing authorization on model actions in Rails.
36
+ version_requirements: *82923030
37
+ description: Authority helps you authorize actions in your Rails app. It's ORM-neutral
38
+ and has very little fancy syntax; just group your models under one or more Authorizer
39
+ classes and write plain Ruby methods on them.
38
40
  email:
39
41
  - nathanmlong@gmail.com
40
42
  - adamhunter@me.com
@@ -61,7 +63,6 @@ files:
61
63
  - lib/authority/railtie.rb
62
64
  - lib/authority/user_abilities.rb
63
65
  - lib/authority/version.rb
64
- - lib/generators/authority/authorizers_generator.rb
65
66
  - lib/generators/authority/install_generator.rb
66
67
  - lib/generators/templates/403.html
67
68
  - lib/generators/templates/authority_initializer.rb
@@ -92,26 +93,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
92
93
  required_rubygems_version: !ruby/object:Gem::Requirement
93
94
  none: false
94
95
  requirements:
95
- - - ! '>'
96
+ - - ! '>='
96
97
  - !ruby/object:Gem::Version
97
- version: 1.3.1
98
+ version: '0'
98
99
  requirements: []
99
100
  rubyforge_project:
100
- rubygems_version: 1.8.16
101
+ rubygems_version: 1.8.10
101
102
  signing_key:
102
103
  specification_version: 3
103
- summary: Authority gives you a clean and easy way to say, in your Rails app, **who**
104
- is allowed to do **what** with your models, with minimal clutter.
105
- test_files:
106
- - spec/authority/abilities_spec.rb
107
- - spec/authority/authorizer_spec.rb
108
- - spec/authority/configuration_spec.rb
109
- - spec/authority/controller_spec.rb
110
- - spec/authority/user_abilities_spec.rb
111
- - spec/authority_spec.rb
112
- - spec/spec_helper.rb
113
- - spec/support/ability_model.rb
114
- - spec/support/example_controllers.rb
115
- - spec/support/mock_rails.rb
116
- - spec/support/no_authorizer_model.rb
117
- - spec/support/user.rb
104
+ summary: Authority helps you authorize actions in your Rails app using plain Ruby
105
+ methods on Authorizer classes.
106
+ test_files: []
@@ -1,47 +0,0 @@
1
- require 'rails/generators/base'
2
-
3
- module Authority
4
- module Generators
5
- class AuthorizersGenerator < Rails::Generators::Base
6
-
7
- argument :parentClass, type: :string, default: 'Authority::Authorizer', banner: 'Parent class'
8
-
9
- def make_authorizer_folder
10
- # creates empty directory if none; doesn't empty the directory
11
- empty_directory authorizer_folder
12
- end
13
-
14
- desc "Generates one authorizer per model, with confirmation. Optionally, give name of parent class."
15
- def make_authorizers
16
- authorizer_names.each do |authorizer_name|
17
- filename = File.join(authorizer_folder, authorizer_name.underscore).concat('.rb')
18
- create_file filename do
19
- contents = <<-RUBY
20
- class #{authorizer_name} < #{parentClass}
21
- # Define class and instance methods
22
- end
23
- RUBY
24
- contents.strip_heredoc
25
- end
26
- end
27
- end
28
-
29
- # Non-public generator methods aren't automatically called
30
- private
31
-
32
- def authorizer_folder
33
- 'app/authorizers'
34
- end
35
-
36
- def authorizer_names
37
- # TODO: Make Dir.glob recursive(**), in case there are model subdirs,
38
- # and create same structure in authorizers
39
- models_dir = File.join(Rails.root, 'app', 'models', '*.rb')
40
- Dir.glob(models_dir).map do |filepath|
41
- filepath.split(/models./).last.gsub(/\.rb\z/, '').camelcase.concat('Authorizer')
42
- end
43
- end
44
-
45
- end
46
- end
47
- end