authority 1.0.0.pre3 → 1.0.0.pre4
Sign up to get free protection for your applications and to get access to all the features.
- data/README.markdown +145 -199
- data/TODO.markdown +1 -4
- data/lib/authority.rb +13 -4
- data/lib/authority/abilities.rb +1 -0
- data/lib/authority/controller.rb +1 -1
- data/lib/authority/version.rb +1 -1
- data/lib/generators/authority/authorizers_generator.rb +47 -0
- data/lib/generators/templates/authority_initializer.rb +6 -3
- data/spec/authority/controller_spec.rb +3 -3
- data/spec/authority_spec.rb +4 -4
- metadata +7 -6
data/README.markdown
CHANGED
@@ -6,23 +6,34 @@ It requires that you already have some kind of user object in your application,
|
|
6
6
|
|
7
7
|
[![Build Status](https://secure.travis-ci.org/nathanl/authority.png)](http://travis-ci.org/nathanl/authority)
|
8
8
|
|
9
|
-
##
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
9
|
+
## Contents
|
10
|
+
|
11
|
+
<ul>
|
12
|
+
<li><a href="#overview">Overview</a></li>
|
13
|
+
<li><a href="#flow_of_authority">The flow of Authority</a></li>
|
14
|
+
<li><a href="#installation">Installation</a></li>
|
15
|
+
<li><a href="#defining_your_abilities">Defining Your Abilities</a></li>
|
16
|
+
<li><a href="#wiring_it_together">Wiring It Together</a>
|
17
|
+
<ul>
|
18
|
+
<li><a href="#users">Users</a></li>
|
19
|
+
<li><a href="#models">Models</a></li>
|
20
|
+
<li><a href="#authorizers">Authorizers</a>
|
21
|
+
<ul>
|
22
|
+
<li><a href="#custom_authorizers">Custom Authorizers</a></li>
|
23
|
+
<li><a href="#default_strategies">Default strategies</a></li>
|
24
|
+
<li><a href="#testing_authorizers">Testing Authorizers</a></li>
|
25
|
+
</ul></li>
|
26
|
+
<li><a href="#controllers">Controllers</a></li>
|
27
|
+
<li><a href="#views">Views</a></li>
|
28
|
+
</ul></li>
|
29
|
+
<li><a href="#security_violations_and_logging">Security Violations & Logging</a></li>
|
30
|
+
<li><a href="#credits">Credits</a></li>
|
31
|
+
<li><a href="#contributing">Contributing</a></li>
|
32
|
+
</ul>
|
33
|
+
|
34
|
+
<a name="overview">
|
22
35
|
## Overview
|
23
36
|
|
24
|
-
Still here? Reading is fun! You always knew that. Time for a deeper look at things.
|
25
|
-
|
26
37
|
The goals of Authority are:
|
27
38
|
|
28
39
|
- To allow broad, class-level rules. Examples:
|
@@ -34,274 +45,211 @@ The goals of Authority are:
|
|
34
45
|
- To provide a clear syntax for permissions-based views. Example:
|
35
46
|
- `link_to 'Edit Widget', edit_widget_path(@widget) if current_user.can_update?(@widget)`
|
36
47
|
- To gracefully handle any access violations: display a "you can't do that" screen and log the violation.
|
37
|
-
- To do all
|
48
|
+
- To do all this with minimal effort and mess.
|
38
49
|
|
50
|
+
<a name="flow_of_authority">
|
39
51
|
## The flow of Authority
|
40
52
|
|
41
|
-
|
53
|
+
Authority encapsulates all authorization logic in `Authorizer` classes. Want to do something with a model? Ask its authorizer.
|
42
54
|
|
43
|
-
|
55
|
+
The process generally flows like this:
|
56
|
+
|
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)`.
|
44
58
|
- The user just asks the model the same question: `resource.creatable_by?(self)`.
|
45
59
|
- The model passes that question to its Authorizer, which actually contains the logic to answer the question.
|
46
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.
|
47
62
|
|
63
|
+
<a name="installation">
|
48
64
|
## Installation
|
49
65
|
|
50
|
-
|
51
|
-
|
52
|
-
Now, add this line to your application's Gemfile:
|
53
|
-
|
54
|
-
gem 'authority'
|
66
|
+
Starting from a clean commit status, add `authority` to your Gemfile, `bundle`, then `rails g authority:install`.
|
55
67
|
|
56
|
-
|
68
|
+
<a name="defining_your_abilities">
|
69
|
+
## Defining Your Abilities
|
57
70
|
|
58
|
-
|
71
|
+
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:
|
59
72
|
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
$ rails g authority:install
|
73
|
+
config.abilities = {
|
74
|
+
:create => 'creatable',
|
75
|
+
:read => 'readable',
|
76
|
+
:update => 'updatable',
|
77
|
+
:delete => 'deletable'
|
78
|
+
}
|
67
79
|
|
68
|
-
|
80
|
+
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.
|
69
81
|
|
70
|
-
|
82
|
+
<a name="wiring_it_together">
|
83
|
+
## Wiring It Together
|
71
84
|
|
72
85
|
<a name="users">
|
73
86
|
### Users
|
74
|
-
|
75
|
-
Your user model (whatever you call it) should `include Authority::UserAbilities`. This defines methods like `can_update?(resource)`. These methods do nothing but pass the question on to the resource itself. For example, `resource.updatable_by?(user)`.
|
76
|
-
|
77
|
-
The list of methods that get defined comes from `config.abilities`.
|
87
|
+
In whatever class represents a logged-in user in your application, `include Authority::UserAbilities`.
|
78
88
|
|
79
89
|
<a name="models">
|
80
90
|
### Models
|
81
|
-
|
82
|
-
In
|
83
|
-
|
84
|
-
The list of methods that get defined comes from `config.abilities`.
|
85
|
-
|
86
|
-
You **could** define those methods yourself on the model, but to keep things organized, we want to put all our authorization logic in authorizer classes. Therefore, these methods, too, are pass-through, which delegate to corresponding methods on the model's authorizer. For example, the `Rabbit` model's `editable_by?(user)` would delegate to `RabbitAuthorizer.editable_by?(user)`.
|
87
|
-
|
88
|
-
Which leads us to...
|
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.
|
89
93
|
|
90
94
|
<a name="authorizers">
|
91
95
|
### Authorizers
|
92
96
|
|
93
|
-
|
97
|
+
Add your authorizers under `app/authorizers`, subclassing `Authority::Authorizer` or your own authorizer class. (See `rails g authority::authorizers --help`.)
|
94
98
|
|
95
|
-
|
96
|
-
class LaserCannonAuthorizer < Authority::Authorizer
|
97
|
-
# Nothing defined - just use the default strategy
|
98
|
-
end
|
99
|
+
These are where your actual authorization logic goes. Here's how it works:
|
99
100
|
|
100
|
-
These are where your actual authorization logic goes. Here's how you do it:
|
101
|
-
|
102
|
-
- Class methods answer questions about model classes, like "is it **ever** permissible for this user to update a widget?"
|
103
|
-
- 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).
|
104
101
|
- 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`).
|
105
102
|
- 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
|
+
- 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).
|
106
105
|
|
107
|
-
|
108
|
-
|
109
|
-
- "Do I have a `deletable_by?` method, to tell me whether this particular laser cannon can be deleted by this user? ... No."
|
110
|
-
- "OK, do I have a `self.deletable_by?` method, to tell me whether **any** laser cannon can be deleted by this user? ... No."
|
111
|
-
- "OK, did the user define a default strategy for deciding whether any resource can be deleted by a user? ... No."
|
112
|
-
- "Well, I do have a **default** default strategy, which always returns false. So the answer is 'false' - the user can't delete this laser cannon."
|
113
|
-
|
114
|
-
As you can see, **you can specify different logic for every method on every model, or [supply a single default strategy](#default_strategies) that covers them all, or anything in between**.
|
115
|
-
|
116
|
-
And because the **default** default strategy returns false, we start by assuming that **everything is forbidden**. This whitelisting approach will keep you from accidentally allowing things you didn't intend.
|
106
|
+
For example:
|
117
107
|
|
118
|
-
|
108
|
+
# app/authorizers/schedule_authorizer.rb
|
109
|
+
class ScheduleAuthorizer < Authority::Authorizer
|
119
110
|
|
120
|
-
|
111
|
+
# Class method: can this user at least sometimes create a Schedule?
|
112
|
+
def self.creatable_by?(user)
|
113
|
+
user.manager?
|
114
|
+
end
|
121
115
|
|
122
|
-
|
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
|
123
120
|
|
124
|
-
# app/authorizers/laser_cannon_authorizer.rb
|
125
|
-
class LaserCannonAuthorizer < Authority::Authorizer
|
126
121
|
end
|
127
122
|
|
128
|
-
|
123
|
+
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.
|
129
124
|
|
130
|
-
|
131
|
-
# `default_strategy` is used. It returns false.
|
132
|
-
current_user.can_create?(@laser_cannon) # false; instance-level permissions check class-level ones by default,
|
133
|
-
# so this is the same as the previous example.
|
125
|
+
#### Custom Authorizers
|
134
126
|
|
135
|
-
If you
|
136
|
-
|
137
|
-
# app/authorizers/laser_cannon_authorizer.rb
|
138
|
-
class LaserCannonAuthorizer < Authority::Authorizer
|
139
|
-
|
140
|
-
# Class-level permissions
|
141
|
-
#
|
142
|
-
def self.creatable_by?(user)
|
143
|
-
true # blanket true means that **any** user can create a laser cannon
|
144
|
-
end
|
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.
|
145
128
|
|
146
|
-
|
147
|
-
|
148
|
-
|
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
|
149
136
|
|
150
|
-
# Instance-level permissions
|
151
|
-
#
|
152
|
-
def updatable_by?(user)
|
153
|
-
resource.color == 'blue' && user.first_name == 'Larry' && Date.today.friday?
|
154
137
|
end
|
155
|
-
|
138
|
+
end
|
139
|
+
|
140
|
+
#app/authorizers/badger_authorizer.rb
|
141
|
+
class BadgerAuthorizer < MyApp::Authorizer
|
142
|
+
# contents
|
156
143
|
end
|
157
144
|
|
158
|
-
|
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`.
|
159
146
|
|
160
|
-
current_user.can_create?(LaserCannon) # true, per class method above
|
161
|
-
current_user.can_create?(@laser_cannon) # true; inherited instance method calls class method
|
162
|
-
current_user.can_delete?(@laser_cannon) # false
|
163
|
-
current_user.can_update?(@laser_cannon) # Only Larry, only blue laser cannons, and only on
|
164
|
-
# Fridays (weapons maintenance day)
|
165
147
|
<a name="default_strategies">
|
166
148
|
#### Default Strategies
|
167
149
|
|
168
|
-
|
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.
|
151
|
+
|
152
|
+
You can configure a different default strategy. For example, you might want one that looks up permissions in your database:
|
169
153
|
|
170
154
|
# In config/initializers/authority.rb
|
171
155
|
config.default_strategy = Proc.new { |able, authorizer, user|
|
172
|
-
|
173
|
-
|
156
|
+
# Does the user have any of the roles which give this permission?
|
157
|
+
(roles_which_grant(able, authorizer) & user.roles).any?
|
174
158
|
}
|
175
159
|
|
176
|
-
|
177
|
-
|
178
|
-
<a name="controllers">
|
179
|
-
### Controllers
|
180
|
-
|
181
|
-
#### Basic Usage
|
182
|
-
|
183
|
-
In your controllers, add this method call:
|
160
|
+
<a name="testing_authorizers">
|
161
|
+
#### Testing Authorizers
|
184
162
|
|
185
|
-
|
163
|
+
One nice thing about putting your authorization logic in authorizers is the ease of testing. Here's a brief example.
|
186
164
|
|
187
|
-
|
165
|
+
# An authorizer shared by several admin-only models
|
166
|
+
describe AdminAuthorizer do
|
188
167
|
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
#### Usage within a controller action
|
168
|
+
before :each do
|
169
|
+
@user = Factory.build(:user)
|
170
|
+
@admin = Factory.build(:admin)
|
171
|
+
end
|
194
172
|
|
195
|
-
|
173
|
+
describe "class" do
|
174
|
+
it "should let admins update in bulk" do
|
175
|
+
AdminAuthorizer.should be_bulk_updatable_by(@admin)
|
176
|
+
end
|
196
177
|
|
197
|
-
|
178
|
+
it "should not let users update in bulk" do
|
179
|
+
AdminAuthorizer.should_not be_bulk_updatable_by(@user)
|
180
|
+
end
|
181
|
+
end
|
198
182
|
|
199
|
-
|
200
|
-
:index => 'read', # index action requires that the user can_read?(resource)
|
201
|
-
:show => 'read', # etc
|
202
|
-
:new => 'create',
|
203
|
-
:create => 'create',
|
204
|
-
:edit => 'update',
|
205
|
-
:update => 'update',
|
206
|
-
:destroy => 'delete'
|
207
|
-
}
|
183
|
+
describe "instances" do
|
208
184
|
|
209
|
-
|
185
|
+
before :each do
|
186
|
+
# A mock model that uses AdminAuthorizer
|
187
|
+
@admin_resource_instance = mock_admin_resource
|
188
|
+
end
|
210
189
|
|
211
|
-
|
212
|
-
|
190
|
+
it "should not allow users to delete" do
|
191
|
+
@admin_resource_instance.authorizer.should_not be_deletable_by(@user)
|
192
|
+
end
|
213
193
|
|
214
|
-
def edit
|
215
|
-
@message = Message.find(params[:id])
|
216
|
-
authorize_action_for(@message)
|
217
194
|
end
|
218
|
-
...
|
219
195
|
|
220
196
|
end
|
221
197
|
|
222
|
-
|
198
|
+
<a name="controllers">
|
199
|
+
### Controllers
|
223
200
|
|
224
|
-
|
201
|
+
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:
|
225
202
|
|
226
|
-
|
227
|
-
|
228
|
-
...
|
229
|
-
end
|
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.
|
230
205
|
|
231
|
-
|
206
|
+
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.
|
232
207
|
|
233
|
-
class
|
234
|
-
authorize_actions_for Badger
|
208
|
+
class LlamaController < ApplicationController
|
235
209
|
|
236
|
-
|
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'
|
237
216
|
|
238
217
|
...
|
239
|
-
end
|
240
218
|
|
241
|
-
|
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
|
242
226
|
|
243
|
-
|
227
|
+
end
|
228
|
+
|
229
|
+
<a name="views">
|
230
|
+
### Views
|
244
231
|
|
245
232
|
Assuming your user object is available in your views, you can do all kinds of conditional rendering. For example:
|
246
233
|
|
247
|
-
|
234
|
+
link_to 'Edit Widget', edit_widget_path(@widget) if current_user.can_update?(@widget)
|
248
235
|
|
249
|
-
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
|
236
|
+
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.
|
250
237
|
|
251
|
-
|
238
|
+
<a name="security_violations_and_logging">
|
239
|
+
## Security Violations & Logging
|
252
240
|
|
253
241
|
Anytime a user attempts an unauthorized action, Authority does two things:
|
254
242
|
|
255
243
|
- Renders your `public/403.html`
|
256
244
|
- Logs the violation to whatever logger you configured.
|
257
245
|
|
258
|
-
If you want to
|
259
|
-
|
260
|
-
## Configuration
|
261
|
-
|
262
|
-
Configuration should be done from `config/initializers/authority.rb`, which will be generated for you by `rails g authority:install`. That file includes copious documentation. Copious, do you hear me?!
|
263
|
-
|
264
|
-
Ahem. Note that the configuration block in that file **must** run in your application. Authority metaprograms its methods on boot, but waits until your configuration block has run to do so. If you want the default settings, you don't have to put anything in your configure block, but you must at least run `Authority.configure`.
|
265
|
-
|
266
|
-
Some of the things you can configure which haven't already been mentioned are...
|
267
|
-
|
268
|
-
### Abilities
|
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
|
269
247
|
|
270
|
-
|
248
|
+
"Kenneth Lay is not allowed to delete this resource: 'accounting_tricks.doc'"
|
271
249
|
|
272
|
-
|
273
|
-
|
274
|
-
Authority will log a message any time a user tries to access a resource for which they are not authorized. By default, this is logged to standard error, but you can supply whatever logger you want, as long as it responds to `warn`. Some possible settings are:
|
275
|
-
|
276
|
-
config.logger = Rails.logger
|
277
|
-
config.logger = Logger.new('logs/authority.log') # From Ruby standard library
|
278
|
-
|
279
|
-
## Custom authorizer inheritence
|
280
|
-
|
281
|
-
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.
|
282
|
-
|
283
|
-
# lib/my_app/authorizer.rb
|
284
|
-
module MyApp
|
285
|
-
class Authorizer < Authority::Authorizer
|
286
|
-
|
287
|
-
def self.has_permission(user, permission_name)
|
288
|
-
# look that up somewhere
|
289
|
-
end
|
290
|
-
|
291
|
-
end
|
292
|
-
end
|
293
|
-
|
294
|
-
#app/authorizers/badger_authorizer.rb
|
295
|
-
class BadgerAuthorizer < MyApp::Authorizer
|
296
|
-
# contents
|
297
|
-
end
|
298
|
-
|
299
|
-
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`.
|
300
|
-
|
301
|
-
## Integration Notes
|
302
|
-
|
303
|
-
- If you want to have nice log messages for security violations, you should ensure that your user object has a `to_s` method; this will control how it shows up in log messages saying things like "**Kenneth Lay** is not allowed to delete this resource:..."
|
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.
|
304
251
|
|
252
|
+
<a name="credits">
|
305
253
|
## Credits, AKA 'Shout-Outs'
|
306
254
|
|
307
255
|
- [adamhunter](https://github.com/adamhunter) for pairing with me on this gem. The only thing faster than his typing is his brain.
|
@@ -309,12 +257,10 @@ If you decide to place your custom class in `lib` as shown above (as opposed to
|
|
309
257
|
- [jnunemaker](https://github.com/jnunemaker) for later creating [Canable](http://github.com/jnunemaker/canable), another inspiration for Authority.
|
310
258
|
- [TMA](http://www.tma1.com) for employing me and letting me open source some of our code.
|
311
259
|
|
260
|
+
<a name="contributing">
|
312
261
|
## Contributing
|
313
262
|
|
314
|
-
What should you contribute?
|
315
|
-
|
316
|
-
- Documentation improvements will always be welcome (though of course, whether something is an improvement will be up to me to decide).
|
317
|
-
- Look in the separate TODO file or grep the project for 'TODO' for other ideas.
|
263
|
+
What should you contribute? Try the TODO file for ideas, or grep the project for 'TODO' comments.
|
318
264
|
|
319
265
|
How can you contribute?
|
320
266
|
|
data/TODO.markdown
CHANGED
@@ -6,12 +6,9 @@
|
|
6
6
|
|
7
7
|
## Chores
|
8
8
|
|
9
|
-
- Add separate generator to make an empty authorizer for each file in `app/models` (prompt for each one)
|
10
9
|
- Test generators
|
10
|
+
- Configurable proc for logging method
|
11
11
|
|
12
12
|
## Documentation
|
13
13
|
|
14
|
-
- Make README more concise, or at least more navigable.
|
15
|
-
- How to bypass creating an authorizer for each model - by setting authorizer name directly and having them share.
|
16
|
-
- For instance-level checks, ensuring that you don't call `update` first; use `attributes=` before calling `authorize_action_on`.
|
17
14
|
- 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?
|
data/lib/authority.rb
CHANGED
@@ -26,13 +26,12 @@ module Authority
|
|
26
26
|
# @param [Symbol] action
|
27
27
|
# @param [Model] resource instance
|
28
28
|
# @param [User] user instance
|
29
|
-
# @raise [
|
29
|
+
# @raise [SecurityViolation] if user is not allowed to perform action on resource
|
30
30
|
# @return [Model] resource instance
|
31
31
|
def self.enforce(action, resource, user)
|
32
32
|
action_authorized = user.send("can_#{action}?", resource)
|
33
33
|
unless action_authorized
|
34
|
-
|
35
|
-
raise SecurityTransgression.new(message)
|
34
|
+
raise SecurityViolation.new(user, action, resource)
|
36
35
|
end
|
37
36
|
resource
|
38
37
|
end
|
@@ -57,7 +56,17 @@ module Authority
|
|
57
56
|
require 'authority/user_abilities'
|
58
57
|
end
|
59
58
|
|
60
|
-
class
|
59
|
+
class SecurityViolation < StandardError
|
60
|
+
def initialize(user, action, resource)
|
61
|
+
@user = user
|
62
|
+
@action = action
|
63
|
+
@resource = resource
|
64
|
+
end
|
65
|
+
|
66
|
+
def message
|
67
|
+
"#{@user} is not authorized to #{@action} this resource: #{@resource}"
|
68
|
+
end
|
69
|
+
end
|
61
70
|
|
62
71
|
end
|
63
72
|
|
data/lib/authority/abilities.rb
CHANGED
data/lib/authority/controller.rb
CHANGED
@@ -6,7 +6,7 @@ module Authority
|
|
6
6
|
extend ActiveSupport::Concern
|
7
7
|
|
8
8
|
included do
|
9
|
-
rescue_from Authority::
|
9
|
+
rescue_from Authority::SecurityViolation, :with => :authority_forbidden
|
10
10
|
class_attribute :authority_resource
|
11
11
|
end
|
12
12
|
|
data/lib/authority/version.rb
CHANGED
@@ -0,0 +1,47 @@
|
|
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
|
@@ -42,6 +42,9 @@ Authority.configure do |config|
|
|
42
42
|
# For a given controller method, what verb must a user be able to do?
|
43
43
|
# For example, a user can access 'show' if they 'can_read' the resource.
|
44
44
|
#
|
45
|
+
# These can be modified on a per-controller basis; see README. This option
|
46
|
+
# applies to all controllers.
|
47
|
+
#
|
45
48
|
# Defaults are as follows:
|
46
49
|
#
|
47
50
|
# config.controller_action_map = {
|
@@ -77,9 +80,9 @@ Authority.configure do |config|
|
|
77
80
|
# config.logger = Logger.new(STDERR)
|
78
81
|
#
|
79
82
|
# Some possible settings:
|
80
|
-
# config.logger = Rails.logger
|
81
|
-
# config.logger = Logger.new('logs/authority.log')
|
82
|
-
# config.logger = Logger.new('/dev/null')
|
83
|
+
# config.logger = Rails.logger # Log with all your app's other messages
|
84
|
+
# config.logger = Logger.new('logs/authority.log') # Use this file
|
85
|
+
# config.logger = Logger.new('/dev/null') # Don't log at all (on a Unix system)
|
83
86
|
|
84
87
|
end
|
85
88
|
|
@@ -8,7 +8,7 @@ describe Authority::Controller do
|
|
8
8
|
|
9
9
|
describe "when including" do
|
10
10
|
it "should specify rescuing security transgressions" do
|
11
|
-
SampleController.should_receive(:rescue_from).with(Authority::
|
11
|
+
SampleController.should_receive(:rescue_from).with(Authority::SecurityViolation, :with => :authority_forbidden)
|
12
12
|
SampleController.send(:include, Authority::Controller)
|
13
13
|
end
|
14
14
|
end
|
@@ -68,8 +68,8 @@ describe Authority::Controller do
|
|
68
68
|
@controller.send(:run_authorization_check)
|
69
69
|
end
|
70
70
|
|
71
|
-
it "should raise a
|
72
|
-
expect { @controller.send(:run_authorization_check) }.to raise_error(Authority::
|
71
|
+
it "should raise a SecurityViolation if authorization fails" do
|
72
|
+
expect { @controller.send(:run_authorization_check) }.to raise_error(Authority::SecurityViolation)
|
73
73
|
end
|
74
74
|
|
75
75
|
it "should raise a MissingAction if there is no corresponding action for the controller" do
|
data/spec/authority_spec.rb
CHANGED
@@ -44,12 +44,12 @@ describe Authority do
|
|
44
44
|
@user = User.new
|
45
45
|
end
|
46
46
|
|
47
|
-
it "should raise a
|
48
|
-
expect { Authority.enforce(:update, AbilityModel, @user) }.to raise_error(Authority::
|
47
|
+
it "should raise a SecurityViolation if the action is unauthorized" do
|
48
|
+
expect { Authority.enforce(:update, AbilityModel, @user) }.to raise_error(Authority::SecurityViolation)
|
49
49
|
end
|
50
50
|
|
51
|
-
it "should not raise a
|
52
|
-
expect { Authority.enforce(:read, AbilityModel, @user) }.not_to raise_error(Authority::
|
51
|
+
it "should not raise a SecurityViolation if the action is authorized" do
|
52
|
+
expect { Authority.enforce(:read, AbilityModel, @user) }.not_to raise_error(Authority::SecurityViolation)
|
53
53
|
end
|
54
54
|
|
55
55
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: authority
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0.0.
|
4
|
+
version: 1.0.0.pre4
|
5
5
|
prerelease: 6
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -10,11 +10,11 @@ authors:
|
|
10
10
|
autorequire:
|
11
11
|
bindir: bin
|
12
12
|
cert_chain: []
|
13
|
-
date: 2012-03-
|
13
|
+
date: 2012-03-26 00:00:00.000000000 Z
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
16
16
|
name: rails
|
17
|
-
requirement: &
|
17
|
+
requirement: &2152325920 !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: *2152325920
|
26
26
|
- !ruby/object:Gem::Dependency
|
27
27
|
name: bundler
|
28
|
-
requirement: &
|
28
|
+
requirement: &2152325380 !ruby/object:Gem::Requirement
|
29
29
|
none: false
|
30
30
|
requirements:
|
31
31
|
- - ! '>='
|
@@ -33,7 +33,7 @@ dependencies:
|
|
33
33
|
version: 1.0.0
|
34
34
|
type: :development
|
35
35
|
prerelease: false
|
36
|
-
version_requirements: *
|
36
|
+
version_requirements: *2152325380
|
37
37
|
description: Gem for managing authorization on model actions in Rails.
|
38
38
|
email:
|
39
39
|
- nathanmlong@gmail.com
|
@@ -61,6 +61,7 @@ files:
|
|
61
61
|
- lib/authority/railtie.rb
|
62
62
|
- lib/authority/user_abilities.rb
|
63
63
|
- lib/authority/version.rb
|
64
|
+
- lib/generators/authority/authorizers_generator.rb
|
64
65
|
- lib/generators/authority/install_generator.rb
|
65
66
|
- lib/generators/templates/403.html
|
66
67
|
- lib/generators/templates/authority_initializer.rb
|