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 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 assumes that you already have some kind of user object in your application.
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 **any** Widget."
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 their jurisdiction."
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.can_edit?(@widget)`
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 `can_edit?(resource)`, which are just nice shortcuts for `resource.editable_by?(user)`.
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, simply `include Authority::Abilities`. This sets up both class-level and instance-level methods like `creatable_by?(user)`, etc, all of which delegate to the model's corresponding authorizer. For example, the `Rabbit` model would delegate to `RabbitAuthorizer`.
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
- In your controllers, add this method call:
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
- If you need to check some attributes of a model instance to decide if an action is permissible, you can use `check_authorization_for(:action, @model_instance, @user)`
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. Each authorizer should correspond to a single model. So if you have `app/models/laser_cannon.rb`, you should have, at minimum:
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; you must override it in your configuration and/or write methods on your individual `Authorizer` classes to grant permissions. This whitelisting approach will keep you from accidentally allowing things you didn't intend.
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
- This combination means that, with this code:
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 can already do the following:
103
+ ... you will find that everything is forbidden:
119
104
 
120
- current_user.can_create?(LaserCannon) # false; all inherited class-level permissions are false
121
- current_user.can_create?(@laser_cannon) # false; instance-level permissions check class-level ones by default
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
- def self.creatable_by?(user) # class-level permission
129
- true
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
- def deletable_by?(user) # instance_level permission
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 this following:
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) # Only Larry, and only on Fridays
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 "Harvey Johnson is not allowed to delete this resource:..."
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
- ## TODO
229
+ ## Credits, AKA 'Shout-Outs'
149
230
 
150
- - Document syntax for checking rules during a controller action
151
- - Update generator to create an authorizer for every model
152
- - Generator
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
@@ -21,31 +21,25 @@ module Authority
21
21
  end
22
22
 
23
23
  def authorizer
24
- begin
25
- @authorizer ||= authorizer_name.constantize
26
- rescue StandardError => e
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
- Authority.adjectives.each do |adjective|
30
+ Authority.adjectives.each do |adjective|
37
31
 
38
- # Metaprogram needed methods, allowing for nice backtraces
39
- class_eval <<-RUBY, __FILE__, __LINE__ + 1
40
- def #{adjective}_by?(user)
41
- authorizer.#{adjective}_by?(user)
42
- end
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
- def authorizer
45
- self.class.authorizer.new(self)
46
- end
47
- RUBY
48
- end
38
+ def authorizer
39
+ self.class.authorizer.new(self)
40
+ end
41
+ RUBY
42
+ end
49
43
 
50
44
  end
51
45
  end
@@ -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
@@ -1,3 +1,3 @@
1
1
  module Authority
2
- VERSION = "0.3.0"
2
+ VERSION = "0.9.0"
3
3
  end
@@ -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 "authority.rb", "config/initializers/authority.rb"
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 class method being called on the Authorizer.
21
- # Ex: `:deletable_by?` or `:updatable_by?`
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 != 'implodable_by?' && user.has_hairstyle?('pompadour')
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.3.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: &2160392740 !ruby/object:Gem::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: *2160392740
25
+ version_requirements: *2164617440
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: bundler
28
- requirement: &2160392220 !ruby/object:Gem::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: *2160392220
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/authority.rb
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