authority 2.2.0 → 2.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## v2.3.0
4
+
5
+ - Added generic `current_user.can?(:mimic_lemurs)` for cases where there is no resource to work with. This calls a corresponding class method on `ApplicationAuthorizer`, like `ApplicationAuthorizer.can_mimic_lemurs?`.
6
+ - Renamed `authority_action` to `authority_actions` (plural) to reflect the fact that you can set multiple actions at once. Use of the old method will raise a deprecation warning.
7
+ - Lots of test cleanup so that test output is clearer - run rspec with `--format doc --order default` to see it.
8
+
3
9
  ## v2.2.0
4
10
 
5
11
  Allow passing options hash to `authorize_action_for`, like `authorize_action_for(@llama, :sporting => @hat_style)`.
data/Gemfile CHANGED
@@ -3,4 +3,4 @@ source 'https://rubygems.org'
3
3
  # Specify your gem's dependencies in authority.gemspec
4
4
  gemspec
5
5
 
6
- gem 'rspec', '>= 2.8.0'
6
+ gem 'rspec', '>= 2.12.0'
@@ -7,7 +7,8 @@ Authority will work fine with a standalone app or a single sign-on system. You c
7
7
  It requires that you already have some kind of user object in your application, accessible from all controllers and views via a method like `current_user` (configurable).
8
8
 
9
9
  [![Build Status](https://secure.travis-ci.org/nathanl/authority.png?branch=master)](http://travis-ci.org/nathanl/authority)
10
- [![Dependency Status](https://gemnasium.com/nathanl/authority.png)](https://gemnasium.com/nathanl/authority)
10
+ <!-- [![Dependency Status](https://gemnasium.com/nathanl/authority.png)](https://gemnasium.com/nathanl/authority) -->
11
+ [![Code Climate](https://codeclimate.com/badge.png)](https://codeclimate.com/github/nathanl/authority)
11
12
 
12
13
  ## Contents
13
14
 
@@ -29,6 +30,7 @@ It requires that you already have some kind of user object in your application,
29
30
  <li><a href="#controllers">Controllers</a></li>
30
31
  <li><a href="#views">Views</a></li>
31
32
  </ul></li>
33
+ <li><a href="#the_generic_can">The Generic `can?`</a>
32
34
  <li><a href="#security_violations_and_logging">Security Violations &amp; Logging</a></li>
33
35
  <li><a href="#credits">Credits</a></li>
34
36
  <li><a href="#contributing">Contributing</a></li>
@@ -45,8 +47,9 @@ Using Authority, you have:
45
47
  - Fine-grained, **instance-level** rules. Examples:
46
48
  - "Management users can only edit schedules with date ranges in the future."
47
49
  - "Users can't create playlists more than 20 songs long unless they've paid."
48
- - A clear syntax for permissions-based views. Example:
50
+ - A clear syntax for permissions-based views. Examples:
49
51
  - `link_to 'Edit Widget', edit_widget_path(@widget) if current_user.can_update?(@widget)`
52
+ - `link_to 'Keelhaul Scallywag', keelhaul_scallywag_path(@scallywag) if current_user.can_keelhaul?(@scallywag)`
50
53
  - Graceful handling of access violations: by default, it displays a "you can't do that" screen and logs the violation.
51
54
  - Minimal effort and mess.
52
55
 
@@ -265,12 +268,12 @@ describe AdminAuthorizer do
265
268
  end
266
269
 
267
270
  describe "class" do
268
- it "should let admins update in bulk" do
269
- AdminAuthorizer.should be_bulk_updatable_by(@admin)
271
+ it "lets admins update in bulk" do
272
+ expect(AdminAuthorizer).to be_bulk_updatable_by(@admin)
270
273
  end
271
274
 
272
- it "should not let users update in bulk" do
273
- AdminAuthorizer.should_not be_bulk_updatable_by(@user)
275
+ it "doesn't let users update in bulk" do
276
+ expect(AdminAuthorizer).not_to be_bulk_updatable_by(@user)
274
277
  end
275
278
  end
276
279
 
@@ -281,12 +284,12 @@ describe AdminAuthorizer do
281
284
  @admin_resource_instance = mock_admin_resource
282
285
  end
283
286
 
284
- it "should let admins delete" do
285
- @admin_resource_instance.authorizer.should be_deletable_by(@admin)
287
+ it "lets admins delete" do
288
+ expect(@admin_resource_instance.authorizer).to be_deletable_by(@admin)
286
289
  end
287
290
 
288
- it "should not let users delete" do
289
- @admin_resource_instance.authorizer.should_not be_deletable_by(@user)
291
+ it "doesn't let users delete" do
292
+ expect(@admin_resource_instance.authorizer).not_to be_deletable_by(@user)
290
293
  end
291
294
 
292
295
  end
@@ -326,7 +329,8 @@ class LlamasController < ApplicationController
326
329
  authorize_actions_for Llama, :except => :create, :actions => {:neuter => :update},
327
330
 
328
331
  # To authorize this controller's 'breed' action, ask whether `current_user.can_create?(Llama)`
329
- authority_action :breed => 'create'
332
+ # To authorize its 'vaporize' action, ask whether `current_user.can_delete?(Llama)`
333
+ authority_actions :breed => 'create', :vaporize => 'delete'
330
334
 
331
335
  ...
332
336
 
@@ -360,6 +364,17 @@ link_to 'Edit Widget', edit_widget_path(@widget) if current_user.can_update?(@wi
360
364
 
361
365
  If the user isn't allowed to edit widgets, they won't see the link. If they're nosy and try to hit the URL directly, they'll get a [Security Violation](#security_violations_and_logging) from the controller.
362
366
 
367
+ <a name="the_generic_can">
368
+ ## The Generic `can?`
369
+
370
+ Authority is organized around protecting resources. But **occasionally** you **may** need to authorize something that has no particular resource. For that, it provides the generic `can?` method. It works like this:
371
+
372
+ ```ruby
373
+ current_user.can?(:view_stats_dashboard) # calls `ApplicationAuthorizer.can_view_stats_dashboard?`
374
+ ```
375
+
376
+ Use this very sparingly, and consider it a [code smell](http://en.wikipedia.org/wiki/Code_smell). Overuse will turn your `ApplicationAuthorizer` into a junk drawer of methods. Ask yourself, "am I sure I don't have a resource for this? Should I have one?"
377
+
363
378
  <a name="security_violations_and_logging">
364
379
  ## Security Violations & Logging
365
380
 
@@ -407,6 +422,7 @@ Your method will be handed the `SecurityViolation`, which has a `message` method
407
422
  ## Credits, AKA 'Shout-Outs'
408
423
 
409
424
  - [adamhunter](https://github.com/adamhunter) for pairing with me on this gem. The only thing faster than his typing is his brain.
425
+ - [kevmoo](https://github.com/kevmoo), [MP211](https://github.com/MP211), and [scottmartin](https://github.com/scottmartin) for pitching in.
410
426
  - [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!"
411
427
  - [jnunemaker](https://github.com/jnunemaker) for later creating [Canable](http://github.com/jnunemaker/canable), another inspiration for Authority.
412
428
  - [TMA](http://www.tma1.com) for employing me and letting me open source some of our code.
@@ -2,12 +2,11 @@
2
2
 
3
3
  ## Tests
4
4
 
5
- - Add tests for the generators
5
+ - Test with Rails 4 and Ruby 2.0
6
6
  - Test `ActionController` integration
7
+ - Add tests for the generators
7
8
 
8
- ## Features
9
-
10
- ### Use translation files
9
+ ## Structural changes
11
10
 
12
- - Move all user-facing messages into en.yml
13
- - Add other languages
11
+ - Consider the huge change from authorizer objects to modules for permissions. This eliminates the awkwardness of "to check a resource instance, let's go instantiate an authorizer and give it this resource instance..." If we make this change, describe a detailed upgrade path.
12
+ - Ensure that Authority can boot without the `configure` method having been run. Maybe this will mean having setters for `abilities` and `controller_action_map` that undefine and redefine those sets of methods if/when the user runs configuration.
@@ -3,6 +3,7 @@ require 'active_support/core_ext/class/attribute'
3
3
  require 'active_support/core_ext/hash/keys'
4
4
  require 'active_support/core_ext/string/inflections'
5
5
  require 'logger'
6
+ require 'authority/security_violation'
6
7
 
7
8
  module Authority
8
9
 
@@ -30,16 +31,20 @@ module Authority
30
31
  # @raise [SecurityViolation] if user is not allowed to perform action on resource
31
32
  # @return [Model] resource instance
32
33
  def self.enforce(action, resource, user, *options)
33
- action_authorized = if options.empty?
34
- user.send("can_#{action}?", resource)
35
- else
36
- user.send("can_#{action}?", resource, Hash[*options])
37
- end
38
- raise SecurityViolation.new(user, action, resource) unless action_authorized
39
-
34
+ unless action_authorized?(action, resource, user, *options)
35
+ raise SecurityViolation.new(user, action, resource)
36
+ end
40
37
  resource
41
38
  end
42
39
 
40
+ def self.action_authorized?(action, resource, user, *options)
41
+ if options.empty?
42
+ user.send("can_#{action}?", resource)
43
+ else
44
+ user.send("can_#{action}?", resource, Hash[*options])
45
+ end
46
+ end
47
+
43
48
  class << self
44
49
  attr_accessor :configuration
45
50
  end
@@ -60,20 +65,6 @@ module Authority
60
65
  require 'authority/user_abilities'
61
66
  end
62
67
 
63
- class SecurityViolation < StandardError
64
- attr_reader :user, :action, :resource
65
-
66
- def initialize(user, action, resource)
67
- @user = user
68
- @action = action
69
- @resource = resource
70
- end
71
-
72
- def message
73
- "#{@user} is not authorized to #{@action} this resource: #{@resource}"
74
- end
75
- end
76
-
77
68
  end
78
69
 
79
70
  require 'authority/configuration'
@@ -35,7 +35,9 @@ module Authority
35
35
  def authorizer
36
36
  @authorizer ||= authorizer_name.constantize # Get an actual reference to the authorizer class
37
37
  rescue NameError
38
- raise Authority::NoAuthorizerError.new("#{authorizer_name} does not exist in your application")
38
+ raise Authority::NoAuthorizerError.new(
39
+ "#{authorizer_name} is set as the authorizer for #{self}, but the constant is missing"
40
+ )
39
41
  end
40
42
  end
41
43
 
@@ -4,11 +4,6 @@ module Authority
4
4
 
5
5
  extend ActiveSupport::Concern
6
6
 
7
- included do
8
- rescue_from(Authority::SecurityViolation, :with => Authority::Controller.security_violation_callback)
9
- class_attribute :authority_resource
10
- end
11
-
12
7
  def self.security_violation_callback
13
8
  Proc.new do |exception|
14
9
  # Through the magic of ActiveSupport's `Proc#bind`, `ActionController::Base#rescue_from`
@@ -17,6 +12,11 @@ module Authority
17
12
  end
18
13
  end
19
14
 
15
+ included do
16
+ rescue_from(Authority::SecurityViolation, :with => Authority::Controller.security_violation_callback)
17
+ class_attribute :authority_resource
18
+ end
19
+
20
20
  module ClassMethods
21
21
 
22
22
  # Sets up before_filter to ensure user is allowed to perform a given controller action
@@ -26,17 +26,25 @@ module Authority
26
26
  # ones and any other options applicable to a before_filter
27
27
  def authorize_actions_for(model_class, options = {})
28
28
  self.authority_resource = model_class
29
- authority_action(options[:actions] || {})
29
+ authority_actions(options[:actions] || {})
30
30
  before_filter :run_authorization_check, options
31
31
  end
32
32
 
33
33
  # Allows defining and overriding a controller's map of its actions to the model's authorizer methods
34
34
  #
35
35
  # @param [Hash] action_map - controller actions and methods, to be merged with existing action_map
36
- def authority_action(action_map)
36
+ def authority_actions(action_map)
37
37
  authority_action_map.merge!(action_map.symbolize_keys)
38
38
  end
39
39
 
40
+ def authority_action(action_map)
41
+ puts "Authority's `authority_action` method has been renamed \
42
+ to `authority_actions` (plural) to reflect the fact that you can \
43
+ set multiple actions in one shot. Please update your controllers \
44
+ accordingly. (called from #{caller.first})".squeeze(' ')
45
+ authority_actions(action_map)
46
+ end
47
+
40
48
  # The controller action to authority action map used for determining
41
49
  # which Rails actions map to which authority actions (ex: index to read)
42
50
  #
@@ -49,6 +57,22 @@ module Authority
49
57
 
50
58
  protected
51
59
 
60
+ # To be run in a `before_filter`; ensure this controller action is allowed for the user
61
+ # Can be used directly within a controller action as well, given an instance or class with or
62
+ # without options to delegate to the authorizer.
63
+ #
64
+ # @param [Class] authority_resource, the model class associated with this controller
65
+ # @param [Hash] options, arbitrary options hash to forward up the chain to the authorizer
66
+ # @raise [MissingAction] if controller action isn't a key in `config.controller_action_map`
67
+ def authorize_action_for(authority_resource, *options)
68
+ # `action_name` comes from ActionController
69
+ authority_action = self.class.authority_action_map[action_name.to_sym]
70
+ if authority_action.nil?
71
+ raise MissingAction.new("No authority action defined for #{action_name}")
72
+ end
73
+ Authority.enforce(authority_action, authority_resource, authority_user, *options)
74
+ end
75
+
52
76
  # Renders a static file to minimize the chances of further errors.
53
77
  #
54
78
  # @param [Exception] error, an error that indicates the user tried to perform a forbidden action.
@@ -73,21 +97,6 @@ module Authority
73
97
  send(Authority.configuration.user_method)
74
98
  end
75
99
 
76
- # To be run in a `before_filter`; ensure this controller action is allowed for the user
77
- # Can be used directly within a controller action as well, given an instance or class with or
78
- # without options to delegate to the authorizer.
79
- #
80
- # @param [Class] authority_resource, the model class associated with this controller
81
- # @param [Hash] options, arbitrary options hash to forward up the chain to the authorizer
82
- # @raise [MissingAction] if controller action isn't a key in `config.controller_action_map`
83
- def authorize_action_for(authority_resource, *options)
84
- authority_action = self.class.authority_action_map[action_name.to_sym]
85
- if authority_action.nil?
86
- raise MissingAction.new("No authority action defined for #{action_name}")
87
- end
88
- Authority.enforce(authority_action, authority_resource, authority_user, *options)
89
- end
90
-
91
100
  class MissingAction < StandardError ; end
92
101
  end
93
102
  end
@@ -0,0 +1,15 @@
1
+ module Authority
2
+ class SecurityViolation < StandardError
3
+ attr_reader :user, :action, :resource
4
+
5
+ def initialize(user, action, resource)
6
+ @user = user
7
+ @action = action
8
+ @resource = resource
9
+ end
10
+
11
+ def message
12
+ "#{@user} is not authorized to #{@action} this resource: #{@resource}"
13
+ end
14
+ end
15
+ end
@@ -20,5 +20,9 @@ module Authority
20
20
  RUBY
21
21
  end
22
22
 
23
+ def can?(action)
24
+ ApplicationAuthorizer.send("can_#{action}?", self)
25
+ end
26
+
23
27
  end
24
28
  end
@@ -1,3 +1,3 @@
1
1
  module Authority
2
- VERSION = "2.2.0"
2
+ VERSION = "2.3.0"
3
3
  end
@@ -1,44 +1,50 @@
1
1
  require 'spec_helper'
2
- require 'support/example_model'
3
- require 'support/user'
2
+ require 'support/example_classes'
4
3
 
5
4
  describe Authority::Abilities do
6
5
 
7
- before :each do
8
- @user = User.new
9
- end
6
+ let(:user) { ExampleUser.new }
7
+ let(:resource_class) { ExampleResource }
10
8
 
11
- describe "authorizer" do
9
+ describe "instance methods" do
12
10
 
13
- it "should have a class attribute getter for authorizer_name" do
14
- ExampleModel.should respond_to(:authorizer_name)
15
- end
11
+ describe "authorizer_name" do
16
12
 
17
- it "should have a class attribute setter for authorizer_name" do
18
- ExampleModel.should respond_to(:authorizer_name=)
19
- end
13
+ it "has a class attribute getter for authorizer_name" do
14
+ expect(resource_class).to respond_to(:authorizer_name)
15
+ end
20
16
 
21
- it "should have a default authorizer_name of 'ApplicationAuthorizer'" do
22
- ExampleModel.authorizer_name.should eq("ApplicationAuthorizer")
23
- end
17
+ it "has a class attribute setter for authorizer_name" do
18
+ expect(resource_class).to respond_to(:authorizer_name=)
19
+ end
24
20
 
25
- it "should constantize the authorizer name as the authorizer" do
26
- ExampleModel.instance_variable_set(:@authorizer, nil)
27
- ExampleModel.authorizer_name.should_receive(:constantize)
28
- ExampleModel.authorizer
29
- end
21
+ it "has a default authorizer_name of 'ApplicationAuthorizer'" do
22
+ expect(resource_class.authorizer_name).to eq("ApplicationAuthorizer")
23
+ end
30
24
 
31
- it "should memoize the authorizer to avoid reconstantizing" do
32
- ExampleModel.authorizer
33
- ExampleModel.authorizer_name.should_not_receive(:constantize)
34
- ExampleModel.authorizer
35
25
  end
36
26
 
37
- it "should raise a friendly error if the authorizer doesn't exist" do
38
- class NoAuthorizerModel < ExampleModel; end ;
39
- NoAuthorizerModel.instance_variable_set(:@authorizer, nil)
40
- NoAuthorizerModel.authorizer_name = 'NonExistentAuthorizer'
41
- expect { NoAuthorizerModel.authorizer }.to raise_error(Authority::NoAuthorizerError)
27
+ describe "authorizer" do
28
+
29
+ it "constantizes the authorizer name as the authorizer" do
30
+ resource_class.instance_variable_set(:@authorizer, nil)
31
+ resource_class.authorizer_name.should_receive(:constantize)
32
+ resource_class.authorizer
33
+ end
34
+
35
+ it "memoizes the authorizer to avoid reconstantizing" do
36
+ resource_class.authorizer
37
+ resource_class.authorizer_name.should_not_receive(:constantize)
38
+ resource_class.authorizer
39
+ end
40
+
41
+ it "raises a friendly error if the authorizer doesn't exist" do
42
+ class NoAuthorizerModel < resource_class; end ;
43
+ NoAuthorizerModel.instance_variable_set(:@authorizer, nil)
44
+ NoAuthorizerModel.authorizer_name = 'NonExistentAuthorizer'
45
+ expect { NoAuthorizerModel.authorizer }.to raise_error(Authority::NoAuthorizerError)
46
+ end
47
+
42
48
  end
43
49
 
44
50
  end
@@ -48,24 +54,28 @@ describe Authority::Abilities do
48
54
  Authority.adjectives.each do |adjective|
49
55
  method_name = "#{adjective}_by?"
50
56
 
51
- it "should respond to `#{method_name}`" do
52
- ExampleModel.should respond_to(method_name)
57
+ it "responds to `#{method_name}`" do
58
+ expect(resource_class).to respond_to(method_name)
53
59
  end
54
60
 
55
- describe "if given an options hash" do
61
+ describe "#{method_name}" do
62
+
63
+ context "when given an options hash" do
64
+
65
+ it "delegates `#{method_name}` to its authorizer class, passing the options" do
66
+ resource_class.authorizer.should_receive(method_name).with(user, :lacking => 'nothing')
67
+ resource_class.send(method_name, user, :lacking => 'nothing')
68
+ end
56
69
 
57
- it "should delegate `#{method_name}` to its authorizer class, passing the options" do
58
- ExampleModel.authorizer.should_receive(method_name).with(@user, :lacking => 'nothing')
59
- ExampleModel.send(method_name, @user, :lacking => 'nothing')
60
70
  end
61
71
 
62
- end
72
+ context "when not given an options hash" do
63
73
 
64
- describe "if not given an options hash" do
74
+ it "delegates `#{method_name}` to its authorizer class, passing no options" do
75
+ resource_class.authorizer.should_receive(method_name).with(user)
76
+ resource_class.send(method_name, user)
77
+ end
65
78
 
66
- it "should delegate `#{method_name}` to its authorizer class, passing no options" do
67
- ExampleModel.authorizer.should_receive(method_name).with(@user)
68
- ExampleModel.send(method_name, @user)
69
79
  end
70
80
 
71
81
  end
@@ -76,50 +86,55 @@ describe Authority::Abilities do
76
86
 
77
87
  describe "instance methods" do
78
88
 
89
+ let(:resource_instance) { resource_class.new }
90
+
79
91
  before :each do
80
- @example_model = ExampleModel.new
81
- @authorizer = ExampleModel.authorizer.new(@example_model)
92
+ @authorizer = resource_class.authorizer.new(resource_instance)
82
93
  end
83
94
 
84
95
  Authority.adjectives.each do |adjective|
85
96
  method_name = "#{adjective}_by?"
86
97
 
87
- it "should respond to `#{method_name}`" do
88
- @example_model.should respond_to(method_name)
98
+ it "responds to `#{method_name}`" do
99
+ expect(resource_instance).to respond_to(method_name)
89
100
  end
90
101
 
91
- describe "if given an options hash" do
102
+ describe "#{method_name}" do
103
+
104
+ context "when given an options hash" do
105
+
106
+ it "delegates `#{method_name}` to a new authorizer instance, passing the options" do
107
+ resource_class.authorizer.stub(:new).and_return(@authorizer)
108
+ @authorizer.should_receive(method_name).with(user, :with => 'mayo')
109
+ resource_instance.send(method_name, user, :with => 'mayo')
110
+ end
92
111
 
93
- it "should delegate `#{method_name}` to a new authorizer instance, passing the options" do
94
- ExampleModel.authorizer.stub(:new).and_return(@authorizer)
95
- @authorizer.should_receive(method_name).with(@user, :with => 'mayo')
96
- @example_model.send(method_name, @user, :with => 'mayo')
97
112
  end
98
113
 
99
- end
114
+ context "when not given an options hash" do
115
+
116
+ it "delegates `#{method_name}` to a new authorizer instance, passing no options" do
117
+ resource_class.authorizer.stub(:new).and_return(@authorizer)
118
+ @authorizer.should_receive(method_name).with(user)
119
+ resource_instance.send(method_name, user)
120
+ end
100
121
 
101
- describe "if not given an options hash" do
102
-
103
- it "should delegate `#{method_name}` to a new authorizer instance, passing no options" do
104
- ExampleModel.authorizer.stub(:new).and_return(@authorizer)
105
- @authorizer.should_receive(method_name).with(@user)
106
- @example_model.send(method_name, @user)
107
122
  end
108
123
 
109
124
  end
110
125
 
111
126
  end
112
127
 
113
- it "should provide an accessor for its authorizer" do
114
- @example_model.should respond_to(:authorizer)
128
+ it "provides an accessor for its authorizer" do
129
+ expect(resource_instance).to respond_to(:authorizer)
115
130
  end
116
131
 
117
132
  # When checking instance methods, we want to ensure that every check uses a new
118
133
  # instance of the authorizer. Otherwise, you might check, make a change to the
119
134
  # model instance, check again, and get an outdated answer.
120
- it "should always create a new authorizer instance when accessing the authorizer" do
121
- @example_model.class.authorizer.should_receive(:new).with(@example_model).twice
122
- 2.times { @example_model.authorizer }
135
+ it "always creates a new authorizer instance when accessing the authorizer" do
136
+ resource_instance.class.authorizer.should_receive(:new).with(resource_instance).twice
137
+ 2.times { resource_instance.authorizer }
123
138
  end
124
139
 
125
140
  end