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 ADDED
@@ -0,0 +1,10 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.8.7
4
+ - 1.9.2
5
+ - 1.9.3
6
+ - jruby-18mode # JRuby in 1.8 mode
7
+ - jruby-19mode # JRuby in 1.9 mode
8
+ - rbx-18mode
9
+ - rbx-19mode
10
+ script: rspec spec
@@ -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
@@ -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
@@ -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 self.#{adjective}_by?(user)
13
- Authority.configuration.default_strategy.call(:#{adjective}, self, user)
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 #{adjective}_by?(user)
21
- self.class.#{adjective}_by?(user)
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
- attr_accessor :default_strategy, :abilities, :authority_actions, :user_method, :logger
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
- @authority_actions = {
20
+ @controller_action_map = {
19
21
  :index => 'read',
20
22
  :show => 'read',
21
23
  :new => 'create',
@@ -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 :authority_actions
11
+ class_attribute :controller_action_map
9
12
  end
10
13
 
11
14
  module ClassMethods
12
- def check_authorization_on(model_class, options = {})
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.authority_actions = Authority.configuration.authority_actions.merge(options[:actions] || {}).symbolize_keys
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.authority_actions.merge!(action_map).symbolize_keys
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
- check_authorization_for self.class.authority_resource, send(Authority.configuration.user_method)
46
+ authorize_action_on self.class.authority_resource, send(Authority.configuration.user_method)
32
47
  end
33
48
 
34
- def check_authorization_for(authority_resource, user)
35
- authority_action = self.class.authority_actions[action_name.to_sym]
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
@@ -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)
@@ -1,3 +1,3 @@
1
1
  module Authority
2
- VERSION = "0.9.0"
2
+ VERSION = "1.0.0.pre2"
3
3
  end
@@ -17,7 +17,8 @@ module Authority
17
17
  end
18
18
 
19
19
  def create_authorizers_directory
20
- empty_directory "app/authorizers" # creates empty directory if none; doesn't empty the directory
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
- # (Permissions.find_by_name_and_authorizer(able, authorizer).roles & user.roles).any?
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
- # AUTHORITY_ACTIONS
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.authority_actions = {
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
- # Suggested setting for a Rails app is:
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.authority_actions.should be_a(Hash)
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.check_authorization_on AbilityModel
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.check_authorization_on AbilityModel, @options
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.check_authorization_on AbilityModel
36
- ExampleController.authority_actions.should be_a(Hash)
37
- ExampleController.authority_actions.should_not be(Authority.configuration.authority_actions)
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 `check_authorization_on` declaration" do
41
- ExampleController.check_authorization_on AbilityModel, :actions => {:eat => 'delete'}
42
- ExampleController.authority_actions[:eat].should eq('delete')
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.authority_actions[:smite].should eq('delete')
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(:check_authorization_for).with(AbilityModel, @user)
60
+ @controller.should_receive(:authorize_action_on).with(AbilityModel, @user)
61
61
  @controller.send(:run_authorization_check)
62
62
  end
63
63
 
@@ -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(RuntimeError, "can't modify frozen Hash")
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([:create, :delete, :read, :update])
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.9.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 00:00:00.000000000 Z
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: &2164617440 !ruby/object:Gem::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: *2164617440
25
+ version_requirements: *82931510
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: bundler
28
- requirement: &2164616920 !ruby/object:Gem::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: *2164616920
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.md
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: '0'
95
+ version: 1.3.1
93
96
  requirements: []
94
97
  rubyforge_project:
95
- rubygems_version: 1.8.16
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