authority 2.2.0 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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