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 +9 -0
- data/README.markdown +200 -120
- data/TODO.markdown +5 -6
- data/authority.gemspec +2 -2
- data/lib/authority/configuration.rb +6 -3
- data/lib/authority/controller.rb +10 -1
- data/lib/authority/version.rb +1 -1
- data/lib/generators/authority/install_generator.rb +24 -1
- data/lib/generators/templates/authority_initializer.rb +9 -1
- data/spec/authority/controller_spec.rb +29 -2
- metadata +16 -27
- data/lib/generators/authority/authorizers_generator.rb +0 -47
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
|
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
|
-
|
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
|
[](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:
|
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
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
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
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
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
|
-
|
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
|
-
|
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
|
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
|
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
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
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
|
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
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
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
|
-
|
166
|
-
|
198
|
+
```ruby
|
199
|
+
# An authorizer shared by several admin-only models
|
200
|
+
describe AdminAuthorizer do
|
167
201
|
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
202
|
+
before :each do
|
203
|
+
@user = FactoryGirl.build(:user)
|
204
|
+
@admin = FactoryGirl.build(:admin)
|
205
|
+
end
|
172
206
|
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
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
|
-
|
179
|
-
|
180
|
-
|
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
|
-
|
228
|
+
end
|
184
229
|
|
185
|
-
|
186
|
-
|
187
|
-
@admin_resource_instance = mock_admin_resource
|
188
|
-
end
|
230
|
+
end
|
231
|
+
```
|
189
232
|
|
190
|
-
|
191
|
-
|
192
|
-
end
|
233
|
+
<a name="custom_authorizers">
|
234
|
+
#### Custom Authorizers
|
193
235
|
|
194
|
-
|
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
|
-
|
268
|
+
```ruby
|
269
|
+
class LlamaController < ApplicationController
|
209
270
|
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
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
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
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
|
-
|
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
|
-
|
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
|
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
|
307
|
+
- Renders `public/403.html`
|
244
308
|
- Logs the violation to whatever logger you configured.
|
245
309
|
|
246
|
-
|
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
|
-
|
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
|
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.
|
268
|
-
2.
|
269
|
-
3.
|
270
|
-
4. `
|
271
|
-
5.
|
272
|
-
6.
|
273
|
-
7.
|
274
|
-
8.
|
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
|
-
-
|
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.
|
8
|
-
gem.
|
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
|
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
|
|
data/lib/authority/controller.rb
CHANGED
@@ -6,10 +6,18 @@ module Authority
|
|
6
6
|
extend ActiveSupport::Concern
|
7
7
|
|
8
8
|
included do
|
9
|
-
rescue_from
|
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
|
data/lib/authority/version.rb
CHANGED
@@ -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
|
-
|
11
|
-
|
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
|
5
|
-
prerelease:
|
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-
|
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: &
|
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: *
|
25
|
+
version_requirements: *82923350
|
26
26
|
- !ruby/object:Gem::Dependency
|
27
27
|
name: bundler
|
28
|
-
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: *
|
37
|
-
description:
|
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:
|
98
|
+
version: '0'
|
98
99
|
requirements: []
|
99
100
|
rubyforge_project:
|
100
|
-
rubygems_version: 1.8.
|
101
|
+
rubygems_version: 1.8.10
|
101
102
|
signing_key:
|
102
103
|
specification_version: 3
|
103
|
-
summary: Authority
|
104
|
-
|
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
|