authority 0.9.0 → 1.0.0.pre2
Sign up to get free protection for your applications and to get access to all the features.
- data/.travis.yml +10 -0
- data/CHANGELOG.markdown +18 -0
- data/README.markdown +328 -0
- data/TODO.markdown +14 -0
- data/authority.gemspec +2 -2
- data/lib/authority.rb +8 -0
- data/lib/authority/abilities.rb +11 -5
- data/lib/authority/authorizer.rb +13 -4
- data/lib/authority/configuration.rb +4 -2
- data/lib/authority/controller.rb +27 -7
- data/lib/authority/railtie.rb +2 -0
- data/lib/authority/user_abilities.rb +7 -0
- data/lib/authority/version.rb +1 -1
- data/lib/generators/authority/install_generator.rb +2 -1
- data/lib/generators/templates/authority_initializer.rb +8 -6
- data/spec/authority/configuration_spec.rb +1 -1
- data/spec/authority/controller_spec.rb +10 -10
- data/spec/authority_spec.rb +4 -2
- metadata +17 -26
- data/README.md +0 -252
data/.travis.yml
ADDED
data/CHANGELOG.markdown
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
# Changelog
|
2
|
+
|
3
|
+
This is mainly to document major new features and backwards-incompatible changes.
|
4
|
+
|
5
|
+
## v1.0.0.pre2
|
6
|
+
|
7
|
+
Rename controller methods:
|
8
|
+
|
9
|
+
- `check_authorization_on` => `authorize_actions_on`
|
10
|
+
- `check_authorization_for` => `authorize_action_on`
|
11
|
+
|
12
|
+
## v1.0.0.pre1
|
13
|
+
|
14
|
+
- Renamed `config.authority_actions` to `config.controller_action_map`.
|
15
|
+
|
16
|
+
## v0.9.0
|
17
|
+
|
18
|
+
Initial release (basically)
|
data/README.markdown
ADDED
@@ -0,0 +1,328 @@
|
|
1
|
+
# Authority
|
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.
|
4
|
+
|
5
|
+
It requires that you already have some kind of user object in your application, accessible from all controllers (like `current_user`).
|
6
|
+
|
7
|
+
[![Build Status](https://secure.travis-ci.org/nathanl/authority.png)](http://travis-ci.org/nathanl/authority)
|
8
|
+
|
9
|
+
## TL;DR
|
10
|
+
|
11
|
+
No time for reading! Reading is for chumps! Here's the skinny:
|
12
|
+
|
13
|
+
- Install in your Rails project: add to Gemfile, `bundle`, then `rails g authority:install`
|
14
|
+
- For each model you have, create a corresponding [authorizer](#authorizers). For example, for `app/models/lolcat.rb`, create `app/authorizers/lolcat_authorizer.rb` with an empty `class LolcatAuthorizer < Authority::Authorizer`.
|
15
|
+
- Add class methods to that authorizer to set rules that can be enforced just by looking at the resource class, like "this user cannot create Lolcats, period."
|
16
|
+
- Add instance methods to that authorizer to set rules that need to look at a resource instance, like "a user can only edit a Lolcat if it belongs to that user and has not been marked as 'classic'".
|
17
|
+
- Wire up your user, models and controllers to work with your authorizers:
|
18
|
+
- In your [user class](#users), `include Authority::UserAbilities`.
|
19
|
+
- Put this in your [controllers](#controllers): `authorize_actions_on YourModelNameHere` (the model that controller works with)
|
20
|
+
- Put this in your [models](#models): `include Authority::Abilities`
|
21
|
+
|
22
|
+
## Overview
|
23
|
+
|
24
|
+
Still here? Reading is fun! You always knew that. Time for a deeper look at things.
|
25
|
+
|
26
|
+
The goals of Authority are:
|
27
|
+
|
28
|
+
- To allow broad, class-level rules. Examples:
|
29
|
+
- "Basic users cannot delete any Widget."
|
30
|
+
- "Only admin users can create Offices."
|
31
|
+
- To allow fine-grained, instance-level rules. Examples:
|
32
|
+
- "Management users can only edit schedules with date ranges in the future."
|
33
|
+
- "Users can't create playlists more than 20 songs long unless they've paid."
|
34
|
+
- To provide a clear syntax for permissions-based views. Example:
|
35
|
+
- `link_to 'Edit Widget', edit_widget_path(@widget) if current_user.can_update?(@widget)`
|
36
|
+
- To gracefully handle any access violations: display a "you can't do that" screen and log the violation.
|
37
|
+
- To do all of this **without cluttering** either your controllers or your models. This is done by letting Authorizer classes do most of the work. More on that below.
|
38
|
+
|
39
|
+
## The flow of Authority
|
40
|
+
|
41
|
+
In broad terms, the authorization process flows like this:
|
42
|
+
|
43
|
+
- 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
|
+
- The user just asks the model the same question: `resource.creatable_by?(self)`.
|
45
|
+
- The model passes that question to its Authorizer, which actually contains the logic to answer the question.
|
46
|
+
- The Authorizer returns an answer back up the call chain to the original caller.
|
47
|
+
|
48
|
+
## Installation
|
49
|
+
|
50
|
+
First, check in whatever changes you've made to your app already. You want to see what we're doing to your app, don't you?
|
51
|
+
|
52
|
+
Now, add this line to your application's Gemfile:
|
53
|
+
|
54
|
+
gem 'authority'
|
55
|
+
|
56
|
+
And then execute:
|
57
|
+
|
58
|
+
$ bundle
|
59
|
+
|
60
|
+
Or install it yourself as:
|
61
|
+
|
62
|
+
$ gem install authority
|
63
|
+
|
64
|
+
Then run the generator:
|
65
|
+
|
66
|
+
$ rails g authority:install
|
67
|
+
|
68
|
+
Hooray! New files! Go look at them. Look look look.
|
69
|
+
|
70
|
+
## Usage
|
71
|
+
|
72
|
+
<a name="users">
|
73
|
+
### 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`.
|
78
|
+
|
79
|
+
<a name="models">
|
80
|
+
### Models
|
81
|
+
|
82
|
+
In your models, `include Authority::Abilities`. This sets up both class-level and instance-level methods like `creatable_by?(user)`, etc.
|
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...
|
89
|
+
|
90
|
+
<a name="authorizers">
|
91
|
+
### Authorizers
|
92
|
+
|
93
|
+
Authorizers should be added under `app/authorizers`, one for each of your models. So if you have a `LaserCannon` model, you should have, at minimum:
|
94
|
+
|
95
|
+
# app/authorizers/laser_cannon_authorizer.rb
|
96
|
+
class LaserCannonAuthorizer < Authority::Authorizer
|
97
|
+
# Nothing defined - just use the default strategy
|
98
|
+
end
|
99
|
+
|
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
|
+
- 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
|
+
- 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.
|
106
|
+
|
107
|
+
So, suppose you've got the empty `LaserCannonAuthorizer` above and haven't supplied a default strategy. Then you ask the authorizer "is `@laser_cannon_x.deletable_by?(@user_y)`?" It will ask itself:
|
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.
|
117
|
+
|
118
|
+
#### An Authorizer Tutorial
|
119
|
+
|
120
|
+
Let's work our way up from the simplest possible authorizer to see how you can customize your rules.
|
121
|
+
|
122
|
+
If your authorizer looks like this:
|
123
|
+
|
124
|
+
# app/authorizers/laser_cannon_authorizer.rb
|
125
|
+
class LaserCannonAuthorizer < Authority::Authorizer
|
126
|
+
end
|
127
|
+
|
128
|
+
... you will find that everything is forbidden:
|
129
|
+
|
130
|
+
current_user.can_create?(LaserCannon) # false; you haven't defined a class-level `can_create?`, so the
|
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.
|
134
|
+
|
135
|
+
If you update your authorizer as follows:
|
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
|
145
|
+
|
146
|
+
def self.deletable_by?(user)
|
147
|
+
false # blanket false means that **no** user can delete a laser cannon
|
148
|
+
end
|
149
|
+
|
150
|
+
# Instance-level permissions
|
151
|
+
#
|
152
|
+
def updatable_by?(user)
|
153
|
+
resource.color == 'blue' && user.first_name == 'Larry' && Date.today.friday?
|
154
|
+
end
|
155
|
+
|
156
|
+
end
|
157
|
+
|
158
|
+
... you can now do the following:
|
159
|
+
|
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
|
+
<a name="default_strategies">
|
166
|
+
#### Default Strategies
|
167
|
+
|
168
|
+
To take a different approach, if you wanted to answer all these questions in a uniform way - perhaps by looking up permissions in a database table - you could just supply a default strategy that does that.
|
169
|
+
|
170
|
+
# In config/initializers/authority.rb
|
171
|
+
config.default_strategy = Proc.new { |able, authorizer, user|
|
172
|
+
# Does the user have any of the roles which give this permission?
|
173
|
+
(roles_which_grant(able, authorizer) & user.roles).any?
|
174
|
+
}
|
175
|
+
|
176
|
+
That's it! All your authorizer classes could be left empty.
|
177
|
+
|
178
|
+
<a name="controllers">
|
179
|
+
### Controllers
|
180
|
+
|
181
|
+
#### Basic Usage
|
182
|
+
|
183
|
+
In your controllers, add this method call:
|
184
|
+
|
185
|
+
authorize_actions_on ModelName
|
186
|
+
|
187
|
+
That sets up a `before_filter` that **calls your class-level methods before each action**. For instance, before running the `update` action, it will check whether the current user (determined using the configurable `user_method`) `can_update?(ModelName)` at a class level. A return value of false means "this user can never update models of this class."
|
188
|
+
|
189
|
+
By the way, any options you pass in will be used on the `before_filter` that gets created, so you can do things like this:
|
190
|
+
|
191
|
+
authorize_actions_on InvisibleSwordsman, :only => :show
|
192
|
+
|
193
|
+
#### Usage within a controller action
|
194
|
+
|
195
|
+
If you need to check some attributes of a model instance to decide if an action is permissible, you can use the **singular** `authorize_action_on(@resource_instance, @user)`. This method will determine which controller action it was called from, look at the controller action map, determine which method should be checked on the model, and check it.
|
196
|
+
|
197
|
+
The default controller action map is as follows:
|
198
|
+
|
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
|
+
}
|
208
|
+
|
209
|
+
So, for example, if you did this:
|
210
|
+
|
211
|
+
class MessageController < ApplicationController
|
212
|
+
...
|
213
|
+
|
214
|
+
def edit
|
215
|
+
@message = Message.find(params[:id])
|
216
|
+
authorize_action_on(@message, current_user)
|
217
|
+
end
|
218
|
+
...
|
219
|
+
|
220
|
+
end
|
221
|
+
|
222
|
+
... Authority would determine that it was called from within `edit`, that the `edit` controller action requires permission to `update`, and check whether the user `can_update?(@message)`.
|
223
|
+
|
224
|
+
Each controller gets its own copy of the controller action map. If you want to edit a **single** controller's action map, you can either pass a hash into `authorize_actions_on`, which will get merged into the existing actions hash...
|
225
|
+
|
226
|
+
class BadgerController < ApplicationController
|
227
|
+
authorize_actions_on Badger, :actions => {:neuter => 'update'}
|
228
|
+
...
|
229
|
+
end
|
230
|
+
|
231
|
+
...or you can use a separate method call:
|
232
|
+
|
233
|
+
class BadgerController < ApplicationController
|
234
|
+
authorize_actions_on Badger
|
235
|
+
|
236
|
+
authority_action :neuter => 'update'
|
237
|
+
|
238
|
+
...
|
239
|
+
end
|
240
|
+
|
241
|
+
Finally, if you want to update this hash for **all** your controllers, you can do that with `config.controller_action_map` in the initializer.
|
242
|
+
|
243
|
+
## Views
|
244
|
+
|
245
|
+
Assuming your user object is available in your views, you can do all kinds of conditional rendering. For example:
|
246
|
+
|
247
|
+
`link_to 'Edit Widget', edit_widget_path(@widget) if current_user.can_update?(@widget)`
|
248
|
+
|
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... **SECURITY VIOLATION** (cue cheesy, dramatic music).
|
250
|
+
|
251
|
+
## Security Violations
|
252
|
+
|
253
|
+
Anytime a user attempts an unauthorized action, Authority does two things:
|
254
|
+
|
255
|
+
- Renders your `public/403.html`
|
256
|
+
- Logs the violation to whatever logger you configured.
|
257
|
+
|
258
|
+
If you want to set 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.
|
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
|
269
|
+
|
270
|
+
If you want to be able to say `user.can_eat?` and have Authority ask the model and authorizer if the resource is `edible_by?` the user, edit your `config.abilities` to include `{:eat => 'edible'}`.
|
271
|
+
|
272
|
+
### Logging
|
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:..."
|
304
|
+
|
305
|
+
## Credits, AKA 'Shout-Outs'
|
306
|
+
|
307
|
+
- [adamhunter](https://github.com/adamhunter) for pairing with me on this gem. The only thing faster than his typing is his brain.
|
308
|
+
- [nkallen](https://github.com/nkallen) for writing [a lovely blog post on access control](http://pivotallabs.com/users/nick/blog/articles/272-access-control-permissions-in-rails) when he worked at Pivotal Labs. I cried sweet tears of joy when I read that a couple of years ago. I was like, "Zee access code, she is so BEEUTY-FUL!"
|
309
|
+
- [jnunemaker](https://github.com/jnunemaker) for later creating [Canable](http://github.com/jnunemaker/canable), another inspiration for Authority.
|
310
|
+
- [TMA](http://www.tma1.com) for employing me and letting me open source some of our code.
|
311
|
+
|
312
|
+
## Contributing
|
313
|
+
|
314
|
+
What should you contribute? Some ideas:
|
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.
|
318
|
+
|
319
|
+
How can you contribute?
|
320
|
+
|
321
|
+
1. Fork this project
|
322
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
323
|
+
3. `bundle install` to get all dependencies
|
324
|
+
4. `rspec spec` to run all tests.
|
325
|
+
5. Update/add tests for your changes and code until they pass.
|
326
|
+
6. Commit your changes (`git commit -am 'Added some feature'`)
|
327
|
+
7. Push to the branch (`git push origin my-new-feature`)
|
328
|
+
8. Create a new Pull Request
|
data/TODO.markdown
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
# TODO
|
2
|
+
|
3
|
+
## Design
|
4
|
+
|
5
|
+
- Carefully think through names of all public methods & see if they could be clearer or more intuitive
|
6
|
+
- Consider making empty authorizers unnecessary: if one isn't defined, automatically define it as empty. This would reduce setup but slightly increase obfuscation of the workings.
|
7
|
+
- Decide whether there's any reason why `authorizer_action_on` needs a user argument, when we already know the method to call to get the current user.
|
8
|
+
|
9
|
+
## Chores
|
10
|
+
|
11
|
+
- Add separate generator to make an empty authorizer for each file in `app/models`
|
12
|
+
- Test generators
|
13
|
+
- Test view helpers
|
14
|
+
- Document how you can bypass creating an authorizer for each model - by setting authorizer name directly and having them share.
|
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.}
|
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.}
|
9
9
|
gem.homepage = "https://github.com/nathanl/authority"
|
10
10
|
|
11
11
|
gem.add_dependency "rails", ">= 3.0.0"
|
data/lib/authority.rb
CHANGED
@@ -8,18 +8,26 @@ module Authority
|
|
8
8
|
|
9
9
|
# NOTE: once this method is called, the library has started meta programming
|
10
10
|
# and abilities should no longer be modified
|
11
|
+
# @return [Hash] list of abilities, mapping verbs and adjectives, like :create => 'creatable'
|
11
12
|
def self.abilities
|
12
13
|
configuration.abilities.freeze
|
13
14
|
end
|
14
15
|
|
16
|
+
# @return [Array] keys from adjectives method
|
15
17
|
def self.verbs
|
16
18
|
abilities.keys
|
17
19
|
end
|
18
20
|
|
21
|
+
# @return [Array] values from adjectives method
|
19
22
|
def self.adjectives
|
20
23
|
abilities.values
|
21
24
|
end
|
22
25
|
|
26
|
+
# @param [Symbol] action
|
27
|
+
# @param [Model] resource instance
|
28
|
+
# @param [User] user instance
|
29
|
+
# @raise [SecurityTransgression] if user is not allowed to perform action on resource
|
30
|
+
# @return [Model] resource instance
|
23
31
|
def self.enforce(action, resource, user)
|
24
32
|
action_authorized = user.send("can_#{action}?", resource)
|
25
33
|
unless action_authorized
|
data/lib/authority/abilities.rb
CHANGED
@@ -1,10 +1,18 @@
|
|
1
1
|
module Authority
|
2
|
+
|
3
|
+
# Should be included into all models in a Rails app. Provides the model
|
4
|
+
# with both class and instance methods like `updatable_by?(user)`
|
5
|
+
# Exactly which methods get defined is determined from `config.abilities`;
|
6
|
+
# the module is evaluated after any user-supplied config block is run
|
7
|
+
# in order to make that possible.
|
8
|
+
# All delegate to the methods of the same name on the model's authorizer.
|
9
|
+
|
2
10
|
module Abilities
|
3
11
|
extend ActiveSupport::Concern
|
4
12
|
|
13
|
+
# Let the Foo model know that its authorizer is called 'FooAuthorizer'
|
5
14
|
included do
|
6
15
|
class_attribute :authorizer_name
|
7
|
-
|
8
16
|
self.authorizer_name = "#{name}Authorizer"
|
9
17
|
end
|
10
18
|
|
@@ -12,7 +20,6 @@ module Authority
|
|
12
20
|
|
13
21
|
Authority.adjectives.each do |adjective|
|
14
22
|
|
15
|
-
# Metaprogram needed methods, allowing for nice backtraces
|
16
23
|
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
17
24
|
def #{adjective}_by?(user)
|
18
25
|
authorizer.#{adjective}_by?(user)
|
@@ -21,7 +28,7 @@ module Authority
|
|
21
28
|
end
|
22
29
|
|
23
30
|
def authorizer
|
24
|
-
@authorizer ||= authorizer_name.constantize
|
31
|
+
@authorizer ||= authorizer_name.constantize # Get an actual reference to the authorizer class
|
25
32
|
rescue NameError => e
|
26
33
|
raise Authority::NoAuthorizerError.new("#{authorizer_name} does not exist in your application")
|
27
34
|
end
|
@@ -29,14 +36,13 @@ module Authority
|
|
29
36
|
|
30
37
|
Authority.adjectives.each do |adjective|
|
31
38
|
|
32
|
-
# Metaprogram needed methods, allowing for nice backtraces
|
33
39
|
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
34
40
|
def #{adjective}_by?(user)
|
35
41
|
authorizer.#{adjective}_by?(user)
|
36
42
|
end
|
37
43
|
|
38
44
|
def authorizer
|
39
|
-
self.class.authorizer.new(self)
|
45
|
+
self.class.authorizer.new(self) # instantiate on every check, in case model has changed
|
40
46
|
end
|
41
47
|
RUBY
|
42
48
|
end
|
data/lib/authority/authorizer.rb
CHANGED
@@ -1,24 +1,33 @@
|
|
1
1
|
module Authority
|
2
2
|
class Authorizer
|
3
3
|
|
4
|
+
# The base Authorizer class, from which all the authorizers in an app will
|
5
|
+
# descend. Provides the authorizer with both class and instance methods
|
6
|
+
# like `updatable_by?(user)`.
|
7
|
+
# Exactly which methods get defined is determined from `config.abilities`;
|
8
|
+
# the class is evaluated after any user-supplied config block is run
|
9
|
+
# in order to make that possible.
|
10
|
+
|
4
11
|
attr_reader :resource
|
5
12
|
|
6
13
|
def initialize(resource)
|
7
14
|
@resource = resource
|
8
15
|
end
|
9
16
|
|
17
|
+
# Each instance method simply calls the corresponding class method
|
10
18
|
Authority.adjectives.each do |adjective|
|
11
19
|
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
12
|
-
def
|
13
|
-
|
20
|
+
def #{adjective}_by?(user)
|
21
|
+
self.class.#{adjective}_by?(user)
|
14
22
|
end
|
15
23
|
RUBY
|
16
24
|
end
|
17
25
|
|
26
|
+
# Each class method simply calls the user-definable default strategy
|
18
27
|
Authority.adjectives.each do |adjective|
|
19
28
|
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
20
|
-
def
|
21
|
-
|
29
|
+
def self.#{adjective}_by?(user)
|
30
|
+
Authority.configuration.default_strategy.call(:#{adjective}, self, user)
|
22
31
|
end
|
23
32
|
RUBY
|
24
33
|
end
|
@@ -1,7 +1,9 @@
|
|
1
1
|
module Authority
|
2
2
|
class Configuration
|
3
3
|
|
4
|
-
|
4
|
+
# Has default settings, overrideable in the initializer.
|
5
|
+
|
6
|
+
attr_accessor :default_strategy, :abilities, :controller_action_map, :user_method, :logger
|
5
7
|
|
6
8
|
def initialize
|
7
9
|
@default_strategy = Proc.new { |able, authorizer, user|
|
@@ -15,7 +17,7 @@ module Authority
|
|
15
17
|
:delete => 'deletable'
|
16
18
|
}
|
17
19
|
|
18
|
-
@
|
20
|
+
@controller_action_map = {
|
19
21
|
:index => 'read',
|
20
22
|
:show => 'read',
|
21
23
|
:new => 'create',
|
data/lib/authority/controller.rb
CHANGED
@@ -1,38 +1,58 @@
|
|
1
1
|
module Authority
|
2
2
|
module Controller
|
3
|
+
|
4
|
+
# Gets included into the app's controllers automatically by the railtie
|
5
|
+
|
3
6
|
extend ActiveSupport::Concern
|
4
7
|
|
5
8
|
included do
|
6
9
|
rescue_from Authority::SecurityTransgression, :with => :authority_forbidden
|
7
10
|
class_attribute :authority_resource
|
8
|
-
class_attribute :
|
11
|
+
class_attribute :controller_action_map
|
9
12
|
end
|
10
13
|
|
11
14
|
module ClassMethods
|
12
|
-
|
15
|
+
|
16
|
+
# Sets up before_filter to ensure user is allowed to perform a given controller action
|
17
|
+
#
|
18
|
+
# @param [Class] model_class - class whose authorizer should be consulted
|
19
|
+
# @param [Hash] options - can contain :actions to be merged with existing
|
20
|
+
# ones and any other options applicable to a before_filter
|
21
|
+
def authorize_actions_on(model_class, options = {})
|
13
22
|
self.authority_resource = model_class
|
14
|
-
self.
|
23
|
+
self.controller_action_map = Authority.configuration.controller_action_map.merge(options[:actions] || {}).symbolize_keys
|
15
24
|
before_filter :run_authorization_check, options
|
16
25
|
end
|
17
26
|
|
27
|
+
# Allows defining and overriding a controller's map of its actions to the model's authorizer methods
|
28
|
+
#
|
29
|
+
# @param [Hash] action_map - controller actions and methods, to be merged with existing action_map
|
18
30
|
def authority_action(action_map)
|
19
|
-
self.
|
31
|
+
self.controller_action_map.merge!(action_map).symbolize_keys
|
20
32
|
end
|
21
33
|
end
|
22
34
|
|
23
35
|
protected
|
24
36
|
|
37
|
+
# Renders a static file to minimize the chances of further errors.
|
38
|
+
#
|
39
|
+
# @param [Exception] error, an error that indicates the user tried to perform a forbidden action.
|
25
40
|
def authority_forbidden(error)
|
26
41
|
Authority.configuration.logger.warn(error.message)
|
27
42
|
render :file => Rails.root.join('public', '403.html'), :status => 403, :layout => false
|
28
43
|
end
|
29
44
|
|
30
45
|
def run_authorization_check
|
31
|
-
|
46
|
+
authorize_action_on self.class.authority_resource, send(Authority.configuration.user_method)
|
32
47
|
end
|
33
48
|
|
34
|
-
|
35
|
-
|
49
|
+
# To be run in a before_filter; ensure this controller action is allowed for the user
|
50
|
+
#
|
51
|
+
# @param authority_resource [Class], the model class associated with this controller
|
52
|
+
# @param user, object representing the current user of the application
|
53
|
+
# @raise [MissingAction] if controller action isn't a key in `config.controller_action_map`
|
54
|
+
def authorize_action_on(authority_resource, user)
|
55
|
+
authority_action = self.class.controller_action_map[action_name.to_sym]
|
36
56
|
if authority_action.nil?
|
37
57
|
raise MissingAction.new("No authority action defined for #{action_name}")
|
38
58
|
end
|
data/lib/authority/railtie.rb
CHANGED
@@ -4,6 +4,8 @@ module Authority
|
|
4
4
|
class Railtie < ::Rails::Railtie
|
5
5
|
|
6
6
|
initializer "authority.controller" do
|
7
|
+
# Include here instead of ApplicationController to avoid being lost when
|
8
|
+
# classes are reloaded in Rails' development mode
|
7
9
|
ActionController::Base.send(:include, Authority::Controller)
|
8
10
|
end
|
9
11
|
|
@@ -1,6 +1,13 @@
|
|
1
1
|
module Authority
|
2
2
|
module UserAbilities
|
3
3
|
|
4
|
+
# Should be included into whatever class represents users in an app.
|
5
|
+
# Provides methods like `can_update?(resource)`
|
6
|
+
# Exactly which methods get defined is determined from `config.abilities`;
|
7
|
+
# the module is evaluated after any user-supplied config block is run
|
8
|
+
# in order to make that possible.
|
9
|
+
# All delegate to corresponding methods on the resource.
|
10
|
+
|
4
11
|
Authority.verbs.each do |verb|
|
5
12
|
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
6
13
|
def can_#{verb}?(resource)
|
data/lib/authority/version.rb
CHANGED
@@ -17,7 +17,8 @@ module Authority
|
|
17
17
|
end
|
18
18
|
|
19
19
|
def create_authorizers_directory
|
20
|
-
|
20
|
+
# creates empty directory if none; doesn't empty the directory
|
21
|
+
empty_directory "app/authorizers"
|
21
22
|
end
|
22
23
|
|
23
24
|
end
|
@@ -24,8 +24,8 @@ Authority.configure do |config|
|
|
24
24
|
# For example:
|
25
25
|
#
|
26
26
|
# config.default_strategy = Proc.new { |able, authorizer, user|
|
27
|
-
# # Does the user have any roles which give this permission?
|
28
|
-
# (
|
27
|
+
# # Does the user have any of the roles which give this permission?
|
28
|
+
# (roles_which_grant(able, authorizer) & user.roles).any?
|
29
29
|
# }
|
30
30
|
#
|
31
31
|
# OR
|
@@ -38,13 +38,13 @@ Authority.configure do |config|
|
|
38
38
|
#
|
39
39
|
# config.default_strategy = Proc.new { |able, authorizer, user| false }
|
40
40
|
|
41
|
-
#
|
41
|
+
# CONTROLLER_ACTION_MAP
|
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
45
|
# Defaults are as follows:
|
46
46
|
#
|
47
|
-
# config.
|
47
|
+
# config.controller_action_map = {
|
48
48
|
# :index => 'read',
|
49
49
|
# :show => 'read',
|
50
50
|
# :new => 'create',
|
@@ -76,8 +76,10 @@ Authority.configure do |config|
|
|
76
76
|
#
|
77
77
|
# config.logger = Logger.new(STDERR)
|
78
78
|
#
|
79
|
-
#
|
80
|
-
config.logger = Rails.logger
|
79
|
+
# Some possible settings:
|
80
|
+
# config.logger = Rails.logger
|
81
|
+
# config.logger = Logger.new('logs/authority.log')
|
82
|
+
# config.logger = Logger.new('/dev/null')
|
81
83
|
|
82
84
|
end
|
83
85
|
|
@@ -12,7 +12,7 @@ describe Authority::Configuration do
|
|
12
12
|
end
|
13
13
|
|
14
14
|
it "should have a default authority controller actions map" do
|
15
|
-
Authority.configuration.
|
15
|
+
Authority.configuration.controller_action_map.should be_a(Hash)
|
16
16
|
end
|
17
17
|
|
18
18
|
it "should have a default controller method for accessing the user object" do
|
@@ -21,30 +21,30 @@ describe Authority::Controller do
|
|
21
21
|
|
22
22
|
describe "DSL (class) methods" do
|
23
23
|
it "should allow specifying the model to protect" do
|
24
|
-
ExampleController.
|
24
|
+
ExampleController.authorize_actions_on AbilityModel
|
25
25
|
ExampleController.authority_resource.should eq(AbilityModel)
|
26
26
|
end
|
27
27
|
|
28
28
|
it "should pass the options provided to the before filter that is set up" do
|
29
29
|
@options = {:only => [:show, :edit, :update]}
|
30
30
|
ExampleController.should_receive(:before_filter).with(:run_authorization_check, @options)
|
31
|
-
ExampleController.
|
31
|
+
ExampleController.authorize_actions_on AbilityModel, @options
|
32
32
|
end
|
33
33
|
|
34
34
|
it "should give the controller its own copy of the authority actions map" do
|
35
|
-
ExampleController.
|
36
|
-
ExampleController.
|
37
|
-
ExampleController.
|
35
|
+
ExampleController.authorize_actions_on AbilityModel
|
36
|
+
ExampleController.controller_action_map.should be_a(Hash)
|
37
|
+
ExampleController.controller_action_map.should_not be(Authority.configuration.controller_action_map)
|
38
38
|
end
|
39
39
|
|
40
|
-
it "should allow specifying the authority action map in the `
|
41
|
-
ExampleController.
|
42
|
-
ExampleController.
|
40
|
+
it "should allow specifying the authority action map in the `authorize_actions_on` declaration" do
|
41
|
+
ExampleController.authorize_actions_on AbilityModel, :actions => {:eat => 'delete'}
|
42
|
+
ExampleController.controller_action_map[:eat].should eq('delete')
|
43
43
|
end
|
44
44
|
|
45
45
|
it "should have a write into the authority actions map usuable in a DSL format" do
|
46
46
|
ExampleController.authority_action :smite => 'delete'
|
47
|
-
ExampleController.
|
47
|
+
ExampleController.controller_action_map[:smite].should eq('delete')
|
48
48
|
end
|
49
49
|
end
|
50
50
|
|
@@ -57,7 +57,7 @@ describe Authority::Controller do
|
|
57
57
|
end
|
58
58
|
|
59
59
|
it "should check authorization on the model specified" do
|
60
|
-
@controller.should_receive(:
|
60
|
+
@controller.should_receive(:authorize_action_on).with(AbilityModel, @user)
|
61
61
|
@controller.send(:run_authorization_check)
|
62
62
|
end
|
63
63
|
|
data/spec/authority_spec.rb
CHANGED
@@ -9,11 +9,13 @@ describe Authority do
|
|
9
9
|
end
|
10
10
|
|
11
11
|
it "should not allow modification of the Authority.abilities hash directly" do
|
12
|
-
expect { Authority.abilities[:exchange] = 'fungible' }.to raise_error(
|
12
|
+
expect { Authority.abilities[:exchange] = 'fungible' }.to raise_error(
|
13
|
+
StandardError, /modify frozen/
|
14
|
+
) # can't modify frozen hash - exact error type and message depends on Ruby version
|
13
15
|
end
|
14
16
|
|
15
17
|
it "should have a convenience accessor for the ability verbs" do
|
16
|
-
Authority.verbs.sort.should eq([
|
18
|
+
Authority.verbs.map(&:to_s).sort.should eq(['create', 'delete', 'read', 'update'])
|
17
19
|
end
|
18
20
|
|
19
21
|
it "should have a convenience accessor for the ability adjectives" 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: 0.
|
5
|
-
prerelease:
|
4
|
+
version: 1.0.0.pre2
|
5
|
+
prerelease: 6
|
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-
|
13
|
+
date: 2012-03-20 00:00:00.000000000 Z
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
16
16
|
name: rails
|
17
|
-
requirement: &
|
17
|
+
requirement: &82931510 !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: *82931510
|
26
26
|
- !ruby/object:Gem::Dependency
|
27
27
|
name: bundler
|
28
|
-
requirement: &
|
28
|
+
requirement: &82931200 !ruby/object:Gem::Requirement
|
29
29
|
none: false
|
30
30
|
requirements:
|
31
31
|
- - ! '>='
|
@@ -33,8 +33,8 @@ dependencies:
|
|
33
33
|
version: 1.0.0
|
34
34
|
type: :development
|
35
35
|
prerelease: false
|
36
|
-
version_requirements: *
|
37
|
-
description: Gem for managing authorization on model actions in Rails
|
36
|
+
version_requirements: *82931200
|
37
|
+
description: Gem for managing authorization on model actions in Rails.
|
38
38
|
email:
|
39
39
|
- nathanmlong@gmail.com
|
40
40
|
- adamhunter@me.com
|
@@ -44,10 +44,13 @@ extra_rdoc_files: []
|
|
44
44
|
files:
|
45
45
|
- .gitignore
|
46
46
|
- .rvmrc
|
47
|
+
- .travis.yml
|
48
|
+
- CHANGELOG.markdown
|
47
49
|
- Gemfile
|
48
50
|
- LICENSE
|
49
|
-
- README.
|
51
|
+
- README.markdown
|
50
52
|
- Rakefile
|
53
|
+
- TODO.markdown
|
51
54
|
- authority.gemspec
|
52
55
|
- lib/authority.rb
|
53
56
|
- lib/authority/abilities.rb
|
@@ -87,26 +90,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
87
90
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
88
91
|
none: false
|
89
92
|
requirements:
|
90
|
-
- - ! '
|
93
|
+
- - ! '>'
|
91
94
|
- !ruby/object:Gem::Version
|
92
|
-
version:
|
95
|
+
version: 1.3.1
|
93
96
|
requirements: []
|
94
97
|
rubyforge_project:
|
95
|
-
rubygems_version: 1.8.
|
98
|
+
rubygems_version: 1.8.10
|
96
99
|
signing_key:
|
97
100
|
specification_version: 3
|
98
101
|
summary: Authority gives you a clean and easy way to say, in your Rails app, **who**
|
99
|
-
is allowed to do **what** with your models.
|
100
|
-
test_files:
|
101
|
-
- spec/authority/abilities_spec.rb
|
102
|
-
- spec/authority/authorizer_spec.rb
|
103
|
-
- spec/authority/configuration_spec.rb
|
104
|
-
- spec/authority/controller_spec.rb
|
105
|
-
- spec/authority/user_abilities_spec.rb
|
106
|
-
- spec/authority_spec.rb
|
107
|
-
- spec/spec_helper.rb
|
108
|
-
- spec/support/ability_model.rb
|
109
|
-
- spec/support/example_controller.rb
|
110
|
-
- spec/support/mock_rails.rb
|
111
|
-
- spec/support/no_authorizer_model.rb
|
112
|
-
- spec/support/user.rb
|
102
|
+
is allowed to do **what** with your models, with minimal clutter.
|
103
|
+
test_files: []
|
data/README.md
DELETED
@@ -1,252 +0,0 @@
|
|
1
|
-
# Authority
|
2
|
-
|
3
|
-
## TL;DR
|
4
|
-
|
5
|
-
No time for reading! Reading is for chumps! Here's the skinny:
|
6
|
-
|
7
|
-
- Install in your Rails project: add to Gemfile, `bundle`, then `rails g authority:install`
|
8
|
-
- Put this in your controllers: `check_authorization_on YourModelNameHere` (the model that controller works with)
|
9
|
-
- Put this in your models: `include Authority::Abilities`
|
10
|
-
- For each model you have, create a corresponding `YourModelNameHereAuthorizer`. For example, for `app/models/lolcat.rb`, create `app/authorizers/lolcat_authorizer.rb` with an empty class inheriting from `Authority::Authorizer`.
|
11
|
-
- Add class methods to that authorizer to set rules that can be enforced just by looking at the resource class, like "this user cannot create Lolcats, period."
|
12
|
-
- Add instance methods to that authorizer to set rules that need to look at a resource instance, like "a user can only edit a Lolcat if it belongs to that user and has not been marked as 'classic'".
|
13
|
-
|
14
|
-
## Overview
|
15
|
-
|
16
|
-
Still here? Reading is fun! You always knew that. Time for a deeper look at things.
|
17
|
-
|
18
|
-
Authority gives you a clean and easy way to say, in your Rails app, **who** is allowed to do **what** with your models.
|
19
|
-
|
20
|
-
It requires that you already have some kind of user object in your application, accessible from all controllers (like `current_user`).
|
21
|
-
|
22
|
-
The goals of Authority are:
|
23
|
-
|
24
|
-
- To allow broad, class-level rules. Examples:
|
25
|
-
- "Basic users cannot delete any Widget."
|
26
|
-
- "Only admin users can create Offices."
|
27
|
-
- To allow fine-grained, instance-level rules. Examples:
|
28
|
-
- "Management users can only edit schedules with date ranges in the future."
|
29
|
-
- "Users can't create playlists more than 20 songs long unless they've paid."
|
30
|
-
- To provide a clear syntax for permissions-based views. Example:
|
31
|
-
- `link_to 'Edit Widget', edit_widget_path(@widget) if current_user.can_update?(@widget)`
|
32
|
-
- To gracefully handle any access violations: display a "you can't do that" screen and log the violation.
|
33
|
-
- To do all of this **without cluttering** either your controllers or your models. This is done by letting Authorizer classes do most of the work. More on that below.
|
34
|
-
|
35
|
-
## The flow of Authority
|
36
|
-
|
37
|
-
In broad terms, the authorization process flows like this:
|
38
|
-
|
39
|
-
- A request comes to a model, either the class or an instance, saying "can this user do this action to you?"
|
40
|
-
- The model passes that question to its Authorizer
|
41
|
-
- The Authorizer checks whatever user properties and business rules are relevant to answer that question.
|
42
|
-
- The answer is passed back up to the model, then back to the original caller
|
43
|
-
|
44
|
-
## Installation
|
45
|
-
|
46
|
-
First, check in whatever changes you've made to your app already. You want to see what we're doing to your app, don't you?
|
47
|
-
|
48
|
-
Now, add this line to your application's Gemfile:
|
49
|
-
|
50
|
-
gem 'authority'
|
51
|
-
|
52
|
-
And then execute:
|
53
|
-
|
54
|
-
$ bundle
|
55
|
-
|
56
|
-
Or install it yourself as:
|
57
|
-
|
58
|
-
$ gem install authority
|
59
|
-
|
60
|
-
Then run the generator:
|
61
|
-
|
62
|
-
$ rails g authority:install
|
63
|
-
|
64
|
-
Hooray! New files! Go look at them.
|
65
|
-
|
66
|
-
## Usage
|
67
|
-
|
68
|
-
### Users
|
69
|
-
|
70
|
-
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)`.
|
71
|
-
|
72
|
-
### Models
|
73
|
-
|
74
|
-
In your models, `include Authority::Abilities`. This sets up both class-level and instance-level methods like `creatable_by?(user)`, etc.
|
75
|
-
|
76
|
-
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 would delegate to `RabbitAuthorizer`.
|
77
|
-
|
78
|
-
Which leads us to...
|
79
|
-
|
80
|
-
### Authorizers
|
81
|
-
|
82
|
-
Authorizers should be added under `app/authorizers`, one for each of your models. So if you have a `LaserCannon` model, you should have, at minimum:
|
83
|
-
|
84
|
-
# app/authorizers/laser_cannon_authorizer.rb
|
85
|
-
class LaserCannonAuthorizer < Authority::Authorizer
|
86
|
-
# Nothing defined - just use the default strategy
|
87
|
-
end
|
88
|
-
|
89
|
-
These are where your actual authorization logic goes. You do have to specify your own business rules, but Authority comes with the following baked in:
|
90
|
-
|
91
|
-
- All instance-level methods defined on `Authority::Authorizer` call their corresponding class-level method by default. 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.
|
92
|
-
- All class-level methods defined on `Authority::Authorizer` will use the `default_strategy` you define in your configuration (see the notes in the generated file).
|
93
|
-
- The **default** default strategy simply returns false, so unless you redefine it or write methods in your Authorizer classes, **everything is forbidden**. This whitelisting approach will keep you from accidentally allowing things you didn't intend.
|
94
|
-
|
95
|
-
Let's work our way up from the simplest possible authorizer to see how you can customize your rules.
|
96
|
-
|
97
|
-
If your authorizer looks like this:
|
98
|
-
|
99
|
-
# app/authorizers/laser_cannon_authorizer.rb
|
100
|
-
class LaserCannonAuthorizer < Authority::Authorizer
|
101
|
-
end
|
102
|
-
|
103
|
-
... you will find that everything is forbidden:
|
104
|
-
|
105
|
-
current_user.can_create?(LaserCannon) # false; you haven't defined a class-level `can_create?`, so the
|
106
|
-
# `default_strategy` is used. It returns false.
|
107
|
-
current_user.can_create?(@laser_cannon) # false; instance-level permissions check class-level ones by default,
|
108
|
-
# so this is the same as the previous example.
|
109
|
-
|
110
|
-
If you update your authorizer as follows:
|
111
|
-
|
112
|
-
# app/authorizers/laser_cannon_authorizer.rb
|
113
|
-
class LaserCannonAuthorizer < Authority::Authorizer
|
114
|
-
|
115
|
-
# Class-level permissions
|
116
|
-
#
|
117
|
-
def self.creatable_by?(user)
|
118
|
-
true # blanket true means that **any** user can create a laser cannon
|
119
|
-
end
|
120
|
-
|
121
|
-
def self.deletable_by?(user)
|
122
|
-
false # blanket false means that **no** user can delete a laser cannon
|
123
|
-
end
|
124
|
-
|
125
|
-
# Instance-level permissions
|
126
|
-
#
|
127
|
-
def updatable_by?(user)
|
128
|
-
user.first_name == 'Larry' && Date.today.friday?
|
129
|
-
end
|
130
|
-
|
131
|
-
end
|
132
|
-
|
133
|
-
... you can now do the following:
|
134
|
-
|
135
|
-
current_user.can_create?(LaserCannon) # true, per class method above
|
136
|
-
current_user.can_create?(@laser_cannon) # true; inherited instance method calls class method
|
137
|
-
current_user.can_delete?(@laser_cannon) # false
|
138
|
-
current_user.can_update?(@laser_cannon) # Only Larry, and only on Fridays (weapons maintenance day)
|
139
|
-
|
140
|
-
### Controllers
|
141
|
-
|
142
|
-
#### Basic Usage
|
143
|
-
|
144
|
-
In your controllers, add this method call:
|
145
|
-
|
146
|
-
`check_authorization_on ModelName`
|
147
|
-
|
148
|
-
That sets up a `before_filter` that calls your class-level methods before each action. For instance, before running the `update` action, it will check whether `ModelName` is `updatable_by?` the current user at a class level. A return value of false means "this user can never update models of this class."
|
149
|
-
|
150
|
-
If that's all you need, one line does it.
|
151
|
-
|
152
|
-
#### In-action usage
|
153
|
-
|
154
|
-
If you need to check some attributes of a model instance to decide if an action is permissible, you can use `check_authorization_for(@resource_instance, @user)`. This will check the proper instance method on the authorizer, based on which controller action you're currently in.
|
155
|
-
|
156
|
-
The default map from controller actions to authorizations is as follows:
|
157
|
-
|
158
|
-
{
|
159
|
-
:index => 'read',
|
160
|
-
:show => 'read',
|
161
|
-
:new => 'create',
|
162
|
-
:create => 'create',
|
163
|
-
:edit => 'update',
|
164
|
-
:update => 'update',
|
165
|
-
:destroy => 'delete'
|
166
|
-
}
|
167
|
-
|
168
|
-
Each controller gets its own copy of this hash.
|
169
|
-
|
170
|
-
If you want to edit a **single** controller's action map, you can either pass a hash into `check_authorization_on`, which will get merged into the existing actions hash...
|
171
|
-
|
172
|
-
class BadgerController < ApplicationController
|
173
|
-
check_authorization_on Badger, :actions => {:neuter => 'update'}
|
174
|
-
...
|
175
|
-
end
|
176
|
-
|
177
|
-
...or you can use a separate method call:
|
178
|
-
|
179
|
-
class BadgerController < ApplicationController
|
180
|
-
check_authorization_on Badger
|
181
|
-
|
182
|
-
authority_action :neuter => 'update'
|
183
|
-
|
184
|
-
...
|
185
|
-
end
|
186
|
-
|
187
|
-
Finally, if you want to update this hash for **all** your controllers, you can do that in `config.authority_actions` in the initializer.
|
188
|
-
|
189
|
-
## Configuration
|
190
|
-
|
191
|
-
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?!
|
192
|
-
|
193
|
-
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`.
|
194
|
-
|
195
|
-
Some of the things you can configure which haven't already been mentioned are...
|
196
|
-
|
197
|
-
### Abilities
|
198
|
-
|
199
|
-
If you want to be able to say `user.can_eat?` and have Authority ask the model and authorizer if the resource is `edible_by?` the user, edit your `config.abilities` to include `{:eat => 'edible'}`.
|
200
|
-
|
201
|
-
### Logging
|
202
|
-
|
203
|
-
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:
|
204
|
-
|
205
|
-
config.logger = Rails.logger
|
206
|
-
config.logger = Logger.new('logs/authority.log') # From Ruby standard library
|
207
|
-
|
208
|
-
## Further customization of authorizers
|
209
|
-
|
210
|
-
If you want to customize your authorizers even further - for example, maybe you want them to have a method like `has_permission?(user, permission_name)` - just add an extra class into their hierarchy.
|
211
|
-
|
212
|
-
# lib/my_app/authorizer
|
213
|
-
module MyApp
|
214
|
-
class Authorizer < Authority::Authorizer
|
215
|
-
|
216
|
-
def self.has_permission(user, permission_name)
|
217
|
-
# look that up somewhere
|
218
|
-
end
|
219
|
-
|
220
|
-
end
|
221
|
-
end
|
222
|
-
|
223
|
-
Require that file in an `after_initialize` block, and have all your other authorizers subclass `MyApp::Authorizer` instead of `Authority::Authorizer`.
|
224
|
-
|
225
|
-
## Integration Notes
|
226
|
-
|
227
|
-
- 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 "**Regina Johnson** is not allowed to delete this resource:..."
|
228
|
-
|
229
|
-
## Credits, AKA 'Shout-Outs'
|
230
|
-
|
231
|
-
- @adamhunter for pairing with me on this gem.
|
232
|
-
- @nkallen for [this lovely blog post on access control](http://pivotallabs.com/users/nick/blog/articles/272-access-control-permissions-in-rails) when he worked at Pivotal Labs.
|
233
|
-
- @jnunemaker for creating Canable, another inspiration for Authority.
|
234
|
-
|
235
|
-
## Contributing
|
236
|
-
|
237
|
-
1. Fork it
|
238
|
-
2. Create your feature branch (`git checkout -b my-new-feature`)
|
239
|
-
3. `bundle install` to get all dependencies
|
240
|
-
4. `rspec spec` to run all tests.
|
241
|
-
5. Make your changes and update/add tests as necessary.
|
242
|
-
6. Commit your changes (`git commit -am 'Added some feature'`)
|
243
|
-
7. Push to the branch (`git push origin my-new-feature`)
|
244
|
-
8. Create new Pull Request
|
245
|
-
|
246
|
-
## TODO
|
247
|
-
|
248
|
-
- Integrate Travis CI
|
249
|
-
- Add YARD docs everywhere
|
250
|
-
- Test generators
|
251
|
-
- Test view helpers
|
252
|
-
- Make TL;DR examples link to examples further down in README
|