authority 0.3.0 → 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +133 -50
- data/lib/authority/abilities.rb +14 -20
- data/lib/authority/controller.rb +1 -1
- data/lib/authority/version.rb +1 -1
- data/lib/generators/authority/install_generator.rb +6 -2
- data/lib/generators/templates/{authority.rb → authority_initializer.rb} +4 -5
- data/spec/authority/controller_spec.rb +1 -1
- metadata +6 -6
data/README.md
CHANGED
@@ -1,12 +1,10 @@
|
|
1
1
|
# Authority
|
2
2
|
|
3
|
-
## SUPER BETA VERSION. Stabler release coming soon.
|
4
|
-
|
5
3
|
## TL;DR
|
6
4
|
|
7
5
|
No time for reading! Reading is for chumps! Here's the skinny:
|
8
6
|
|
9
|
-
- Install in your Rails project
|
7
|
+
- Install in your Rails project: add to Gemfile, `bundle`, then `rails g authority:install`
|
10
8
|
- Put this in your controllers: `check_authorization_on YourModelNameHere` (the model that controller works with)
|
11
9
|
- Put this in your models: `include Authority::Abilities`
|
12
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`.
|
@@ -19,23 +17,19 @@ Still here? Reading is fun! You always knew that. Time for a deeper look at thin
|
|
19
17
|
|
20
18
|
Authority gives you a clean and easy way to say, in your Rails app, **who** is allowed to do **what** with your models.
|
21
19
|
|
22
|
-
It
|
20
|
+
It requires that you already have some kind of user object in your application, accessible from all controllers (like `current_user`).
|
23
21
|
|
24
22
|
The goals of Authority are:
|
25
23
|
|
26
24
|
- To allow broad, class-level rules. Examples:
|
27
|
-
- "Basic users cannot delete
|
25
|
+
- "Basic users cannot delete any Widget."
|
28
26
|
- "Only admin users can create Offices."
|
29
|
-
|
30
27
|
- To allow fine-grained, instance-level rules. Examples:
|
31
|
-
- "Management users can only edit schedules in
|
28
|
+
- "Management users can only edit schedules with date ranges in the future."
|
32
29
|
- "Users can't create playlists more than 20 songs long unless they've paid."
|
33
|
-
|
34
30
|
- To provide a clear syntax for permissions-based views. Example:
|
35
|
-
- `link_to 'Edit Widget', edit_widget_path(@widget) if current_user.
|
36
|
-
|
31
|
+
- `link_to 'Edit Widget', edit_widget_path(@widget) if current_user.can_update?(@widget)`
|
37
32
|
- To gracefully handle any access violations: display a "you can't do that" screen and log the violation.
|
38
|
-
|
39
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.
|
40
34
|
|
41
35
|
## The flow of Authority
|
@@ -73,89 +67,170 @@ Hooray! New files! Go look at them.
|
|
73
67
|
|
74
68
|
### Users
|
75
69
|
|
76
|
-
Your user model (whatever you call it) should `include Authority::UserAbilities`. This defines methods like `
|
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)`.
|
77
71
|
|
78
72
|
### Models
|
79
73
|
|
80
|
-
In your models,
|
81
|
-
|
82
|
-
### Controllers
|
83
|
-
|
84
|
-
#### Basic Usage
|
74
|
+
In your models, `include Authority::Abilities`. This sets up both class-level and instance-level methods like `creatable_by?(user)`, etc.
|
85
75
|
|
86
|
-
|
87
|
-
|
88
|
-
`check_authorization_on ModelName`
|
89
|
-
|
90
|
-
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."
|
91
|
-
|
92
|
-
If that's all you need, one line does it.
|
93
|
-
|
94
|
-
#### In-action usage
|
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`.
|
95
77
|
|
96
|
-
|
78
|
+
Which leads us to...
|
97
79
|
|
98
80
|
### Authorizers
|
99
81
|
|
100
|
-
Authorizers should be added under `app/authorizers`, one for each of your models.
|
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:
|
101
83
|
|
102
84
|
# app/authorizers/laser_cannon_authorizer.rb
|
103
85
|
class LaserCannonAuthorizer < Authority::Authorizer
|
86
|
+
# Nothing defined - just use the default strategy
|
104
87
|
end
|
105
88
|
|
106
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:
|
107
90
|
|
108
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.
|
109
|
-
- All class-level methods defined on `Authority::Authorizer` will use the `default_strategy` you define in your configuration.
|
110
|
-
- The **default** default strategy simply returns false
|
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.
|
111
94
|
|
112
|
-
|
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:
|
113
98
|
|
114
99
|
# app/authorizers/laser_cannon_authorizer.rb
|
115
100
|
class LaserCannonAuthorizer < Authority::Authorizer
|
116
101
|
end
|
117
102
|
|
118
|
-
... you
|
103
|
+
... you will find that everything is forbidden:
|
119
104
|
|
120
|
-
current_user.can_create?(LaserCannon) # false;
|
121
|
-
|
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.
|
122
109
|
|
123
110
|
If you update your authorizer as follows:
|
124
111
|
|
125
112
|
# app/authorizers/laser_cannon_authorizer.rb
|
126
113
|
class LaserCannonAuthorizer < Authority::Authorizer
|
127
114
|
|
128
|
-
|
129
|
-
|
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
|
130
123
|
end
|
131
124
|
|
132
|
-
|
125
|
+
# Instance-level permissions
|
126
|
+
#
|
127
|
+
def updatable_by?(user)
|
133
128
|
user.first_name == 'Larry' && Date.today.friday?
|
134
129
|
end
|
135
130
|
|
136
131
|
end
|
137
132
|
|
138
|
-
... you can now do
|
133
|
+
... you can now do the following:
|
139
134
|
|
140
135
|
current_user.can_create?(LaserCannon) # true, per class method above
|
141
136
|
current_user.can_create?(@laser_cannon) # true; inherited instance method calls class method
|
142
|
-
current_user.can_delete?(@laser_cannon) #
|
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`.
|
143
224
|
|
144
225
|
## Integration Notes
|
145
226
|
|
146
|
-
- 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 "
|
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:..."
|
147
228
|
|
148
|
-
##
|
229
|
+
## Credits, AKA 'Shout-Outs'
|
149
230
|
|
150
|
-
-
|
151
|
-
-
|
152
|
-
-
|
153
|
-
- Add generators or hook into existing rails generators
|
154
|
-
- Add generator to installation instructions
|
155
|
-
- Generate well-commented default configuration file like Devise does (shout out!)
|
156
|
-
- Generate 403.html, with option to skip if exists
|
157
|
-
- Note that you MUST call configure; internals aren't included until you do.
|
158
|
-
- Write about configuration file and options in Configuration section.
|
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.
|
159
234
|
|
160
235
|
## Contributing
|
161
236
|
|
@@ -167,3 +242,11 @@ If you update your authorizer as follows:
|
|
167
242
|
6. Commit your changes (`git commit -am 'Added some feature'`)
|
168
243
|
7. Push to the branch (`git push origin my-new-feature`)
|
169
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
|
data/lib/authority/abilities.rb
CHANGED
@@ -21,31 +21,25 @@ module Authority
|
|
21
21
|
end
|
22
22
|
|
23
23
|
def authorizer
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
if e.is_a?(NameError)
|
28
|
-
raise Authority::NoAuthorizerError.new("#{authorizer_name} does not exist in your application")
|
29
|
-
else
|
30
|
-
raise e
|
31
|
-
end
|
32
|
-
end
|
24
|
+
@authorizer ||= authorizer_name.constantize
|
25
|
+
rescue NameError => e
|
26
|
+
raise Authority::NoAuthorizerError.new("#{authorizer_name} does not exist in your application")
|
33
27
|
end
|
34
28
|
end
|
35
29
|
|
36
|
-
|
30
|
+
Authority.adjectives.each do |adjective|
|
37
31
|
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
32
|
+
# Metaprogram needed methods, allowing for nice backtraces
|
33
|
+
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
34
|
+
def #{adjective}_by?(user)
|
35
|
+
authorizer.#{adjective}_by?(user)
|
36
|
+
end
|
43
37
|
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
38
|
+
def authorizer
|
39
|
+
self.class.authorizer.new(self)
|
40
|
+
end
|
41
|
+
RUBY
|
42
|
+
end
|
49
43
|
|
50
44
|
end
|
51
45
|
end
|
data/lib/authority/controller.rb
CHANGED
@@ -24,7 +24,7 @@ module Authority
|
|
24
24
|
|
25
25
|
def authority_forbidden(error)
|
26
26
|
Authority.configuration.logger.warn(error.message)
|
27
|
-
render :file => Rails.root.join('public', '403.html'), :status => 403
|
27
|
+
render :file => Rails.root.join('public', '403.html'), :status => 403, :layout => false
|
28
28
|
end
|
29
29
|
|
30
30
|
def run_authorization_check
|
data/lib/authority/version.rb
CHANGED
@@ -9,13 +9,17 @@ module Authority
|
|
9
9
|
desc "Creates an Authority initializer for your application."
|
10
10
|
|
11
11
|
def copy_initializer
|
12
|
-
template "
|
12
|
+
template "authority_initializer.rb", "config/initializers/authority.rb"
|
13
13
|
end
|
14
14
|
|
15
15
|
def copy_forbidden
|
16
16
|
template "403.html", "public/403.html"
|
17
17
|
end
|
18
|
-
|
18
|
+
|
19
|
+
def create_authorizers_directory
|
20
|
+
empty_directory "app/authorizers" # creates empty directory if none; doesn't empty the directory
|
21
|
+
end
|
22
|
+
|
19
23
|
end
|
20
24
|
end
|
21
25
|
end
|
@@ -17,9 +17,8 @@ Authority.configure do |config|
|
|
17
17
|
#
|
18
18
|
# The arguments passed to this proc will be:
|
19
19
|
#
|
20
|
-
# able - symbol name of
|
21
|
-
#
|
22
|
-
# authorizer - constant name of authorizer. Ex: `WidgetAuthorizer` or `UserAuthorizer`
|
20
|
+
# able - symbol name of 'able', like `:creatable`, or `:deletable`
|
21
|
+
# authorizer - authorizer constant. Ex: `WidgetAuthorizer` or `UserAuthorizer`
|
23
22
|
# user - user object (whatever that is in your application; found using config.user_method)
|
24
23
|
#
|
25
24
|
# For example:
|
@@ -32,10 +31,10 @@ Authority.configure do |config|
|
|
32
31
|
# OR
|
33
32
|
#
|
34
33
|
# config.default_strategy = Proc.new { |able, authorizer, user|
|
35
|
-
# able !=
|
34
|
+
# able != :implodable && user.has_hairstyle?('pompadour')
|
36
35
|
# }
|
37
36
|
#
|
38
|
-
# Default strategy simply returns false, as follows:
|
37
|
+
# Default default strategy simply returns false, as follows:
|
39
38
|
#
|
40
39
|
# config.default_strategy = Proc.new { |able, authorizer, user| false }
|
41
40
|
|
@@ -84,7 +84,7 @@ describe Authority::Controller do
|
|
84
84
|
|
85
85
|
it "should render the public/403.html file" do
|
86
86
|
forbidden_page = Rails.root.join('public/403.html')
|
87
|
-
@controller.should_receive(:render).with(:file => forbidden_page, :status => 403)
|
87
|
+
@controller.should_receive(:render).with(:file => forbidden_page, :status => 403, :layout => false)
|
88
88
|
@controller.send(:authority_forbidden, @mock_error)
|
89
89
|
end
|
90
90
|
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: 0.
|
4
|
+
version: 0.9.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -14,7 +14,7 @@ date: 2012-03-13 00:00:00.000000000 Z
|
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
16
16
|
name: rails
|
17
|
-
requirement: &
|
17
|
+
requirement: &2164617440 !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: *2164617440
|
26
26
|
- !ruby/object:Gem::Dependency
|
27
27
|
name: bundler
|
28
|
-
requirement: &
|
28
|
+
requirement: &2164616920 !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: *2164616920
|
37
37
|
description: Gem for managing authorization on model actions in Rails
|
38
38
|
email:
|
39
39
|
- nathanmlong@gmail.com
|
@@ -59,7 +59,7 @@ files:
|
|
59
59
|
- lib/authority/version.rb
|
60
60
|
- lib/generators/authority/install_generator.rb
|
61
61
|
- lib/generators/templates/403.html
|
62
|
-
- lib/generators/templates/
|
62
|
+
- lib/generators/templates/authority_initializer.rb
|
63
63
|
- spec/authority/abilities_spec.rb
|
64
64
|
- spec/authority/authorizer_spec.rb
|
65
65
|
- spec/authority/configuration_spec.rb
|