authority 2.3.2 → 2.4.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.
data/CHANGELOG.markdown CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ Authority does its best to use [semantic versioning](http://semver.org).
4
+
5
+ ## v2.4.0
6
+
7
+ Controller method `authorize_actions_for` can now be given a method name to dynamically determine the class to authorize. For example, `authorize_actions_for :model_class` will call the `model_class` method on the controller instance at request time.
8
+
9
+ ## v2.3.2
10
+
11
+ - Updated `can?` to only pass options if it was given options.
12
+
13
+ ## v2.3.1
14
+
15
+ - Had second thought and reworked `can?(:action)` to call `Application_authorizer.authorizes_to_#{action}?`. Ensured it's backwards compatible for the few people who started using this in the last day or so.
16
+
3
17
  ## v2.3.0
4
18
 
5
19
  - 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?`.
data/README.markdown CHANGED
@@ -6,7 +6,7 @@ Authority will work fine with a standalone app or a single sign-on system. You c
6
6
 
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
- [![Build Status](https://secure.travis-ci.org/nathanl/authority.png?branch=master)](http://travis-ci.org/nathanl/authority)
9
+ <!--[![Build Status](https://secure.travis-ci.org/nathanl/authority.png?branch=master)](http://travis-ci.org/nathanl/authority) -->
10
10
  <!-- [![Dependency Status](https://gemnasium.com/nathanl/authority.png)](https://gemnasium.com/nathanl/authority) -->
11
11
  [![Code Climate](https://codeclimate.com/badge.png)](https://codeclimate.com/github/nathanl/authority)
12
12
 
@@ -353,6 +353,35 @@ end
353
353
 
354
354
  As with other authorization checks, you can also pass options here, and they'll be sent along to your authorization method: `authorize_action_for(@llama, :sporting => @hat_style)`. Generally, though, your authorization will depend on some attribute or association of the model instance, so the authorizer can check `@llama.neck_strength` and `@llama.owner.nationality`, etc, without needing any additional information.
355
355
 
356
+ Note that you can also call `authority_actions` as many times as you like, so you can specify one mapping at a time if you prefer:
357
+
358
+ ```ruby
359
+ class LlamasController < ApplicationController
360
+ def breed
361
+ # some code
362
+ end
363
+ authority_actions :breed => 'create'
364
+
365
+ def vaporize
366
+ # some code
367
+ end
368
+ authority_actions :vaporize => 'delete'
369
+ end
370
+ ```
371
+
372
+ Finally, note that if you have a controller that dynamically determines the class it's working with, you can pass the name of a controller instance method to `authorize_actions_for` instead of a class, and the class will be looked up when a request is made.
373
+
374
+ ```ruby
375
+ class LlamasController < ApplicationController
376
+
377
+ authorize_actions_for :llama_class
378
+
379
+ def llama_class
380
+ [StandardLlama, LludicriousLlama].sample
381
+ end
382
+ end
383
+ ```
384
+
356
385
  <a name="views">
357
386
  ### Views
358
387
 
@@ -387,44 +416,23 @@ Use this very sparingly, and consider it a [code smell](http://en.wikipedia.org/
387
416
  <a name="security_violations_and_logging">
388
417
  ## Security Violations & Logging
389
418
 
390
- Anytime a user attempts an unauthorized action, Authority calls whatever controller method is specified by your `security_violation_handler` option, handing it the exception. The default handler is `authority_forbidden`, which Authority adds to your `ApplicationController`. It does the following:
419
+ If you're using Authority's view helpers, users should only see links for actions they're authorized to take. If a user deliberately tries to access a restricted resource (for instance, by typing the URL directly), Authority raises and rescues an `Authority::SecurityViolation`.
420
+
421
+ When it rescues the exception, Authority calls whatever controller method is specified by your `security_violation_handler` option, handing it the exception. The default handler is `authority_forbidden`, which Authority mixes in to your `ApplicationController`. It does the following:
391
422
 
392
423
  - Renders `public/403.html`
393
424
  - Logs the violation to whatever logger you configured.
394
425
 
395
- You can define your own `authority_forbidden` method:
396
-
426
+ You can define your own `authority_forbidden` method on `ApplicationController` and/or any other controller. For example:
397
427
 
398
428
  ```ruby
399
429
  # Send 'em back where they came from with a slap on the wrist
400
- def authority_forbidden(exception)
430
+ def authority_forbidden(error)
401
431
  Authority.logger.warn(error.message)
402
432
  redirect_to request.referrer.presence || root_path, :alert => 'You are not authorized to complete that action.'
403
433
  end
404
434
  ```
405
435
 
406
- ... or specify a different handler like this:
407
-
408
- ```ruby
409
- # config/initializers/authority.rb
410
- config.security_violation_handler = :fire_ze_missiles
411
- ```
412
- Then define the method on your controller:
413
-
414
- ```ruby
415
- # app/controllers/application_controller.rb
416
- class ApplicationController < ActionController::Base
417
-
418
- def fire_ze_missiles(exception)
419
- # Log? Set a flash message? Dispatch minions to
420
- # fill their mailbox with goose droppings? It's up to you.
421
- end
422
- ...
423
- end
424
- ```
425
-
426
- If you want different error handling per controller, define `fire_ze_missiles` on each of them.
427
-
428
436
  Your method will be handed the `SecurityViolation`, which has a `message` method. In case you want to build your own message, it also exposes `user`, `action` and `resource`.
429
437
 
430
438
  <a name="credits">
data/TODO.markdown CHANGED
@@ -1,5 +1,7 @@
1
1
  # TODO
2
2
 
3
+ - Consider removing `config.security_violation_handler`, since `authority_forbidden` can already be redefined on any controller
4
+
3
5
  ## Tests
4
6
 
5
7
  - Test with Rails 4 and Ruby 2.0
data/lib/authority.rb CHANGED
@@ -31,19 +31,16 @@ module Authority
31
31
  # @param [Hash] options, arbitrary options hash to delegate to the authorizer
32
32
  # @raise [SecurityViolation] if user is not allowed to perform action on resource
33
33
  # @return [Model] resource instance
34
- def self.enforce(action, resource, user, *options)
35
- unless action_authorized?(action, resource, user, *options)
34
+ def self.enforce(action, resource, user, options = {})
35
+ unless action_authorized?(action, resource, user, options)
36
36
  raise SecurityViolation.new(user, action, resource)
37
37
  end
38
38
  resource
39
39
  end
40
40
 
41
- def self.action_authorized?(action, resource, user, *options)
42
- if options.empty?
43
- user.send("can_#{action}?", resource)
44
- else
45
- user.send("can_#{action}?", resource, Hash[*options])
46
- end
41
+ def self.action_authorized?(action, resource, user, options = {})
42
+ resource_and_maybe_options = [resource, options].tap {|args| args.pop if args.last == {}}
43
+ user.send("can_#{action}?", *resource_and_maybe_options)
47
44
  end
48
45
 
49
46
  class << self
@@ -29,11 +29,8 @@ module Authority
29
29
  Authority.adjectives.each do |adjective|
30
30
  class_eval <<-RUBY, __FILE__, __LINE__ + 1
31
31
  def self.#{adjective}_by?(user, options = {})
32
- if options.empty?
33
- default(:#{adjective}, user)
34
- else
35
- default(:#{adjective}, user, options)
36
- end
32
+ user_and_maybe_options = [user, options].tap {|args| args.pop if args.last == {}}
33
+ default(:#{adjective}, *user_and_maybe_options)
37
34
  end
38
35
  RUBY
39
36
  end
@@ -14,18 +14,23 @@ module Authority
14
14
 
15
15
  included do
16
16
  rescue_from(Authority::SecurityViolation, :with => Authority::Controller.security_violation_callback)
17
- class_attribute :authority_resource
17
+ class << self
18
+ attr_accessor :authority_resource
19
+ end
18
20
  end
19
21
 
20
22
  module ClassMethods
21
23
 
22
24
  # Sets up before_filter to ensure user is allowed to perform a given controller action
23
25
  #
24
- # @param [Class] model_class - class whose authorizer should be consulted
25
- # @param [Hash] options - can contain :actions to be merged with existing
26
+ # @param [Class OR Symbol] resource_or_finder - class whose authorizer
27
+ # should be consulted, or instance method on the controller which will
28
+ # determine that class when the request is made
29
+ # @param [Hash] options - can contain :actions to
30
+ # be merged with existing
26
31
  # ones and any other options applicable to a before_filter
27
- def authorize_actions_for(model_class, options = {})
28
- self.authority_resource = model_class
32
+ def authorize_actions_for(resource_or_finder, options = {})
33
+ self.authority_resource = resource_or_finder
29
34
  authority_actions(options[:actions] || {})
30
35
  before_filter :run_authorization_check, options
31
36
  end
@@ -86,7 +91,17 @@ module Authority
86
91
  # The `before_filter` that will be setup to run when the class method
87
92
  # `authorize_actions_for` is called
88
93
  def run_authorization_check
89
- authorize_action_for self.class.authority_resource
94
+ authorize_action_for authority_resource
95
+ end
96
+
97
+ def authority_resource
98
+ return self.class.authority_resource if self.class.authority_resource.is_a?(Class)
99
+ return send(self.class.authority_resource) if respond_to?(self.class.authority_resource)
100
+ raise MissingResource.new(
101
+ "Trying to authorize actions for '#{self.class.authority_resource}', but can't. \
102
+ Must be either a resource class OR the name of a controller instance method that \
103
+ returns one.".squeeze(' ')
104
+ )
90
105
  end
91
106
 
92
107
  # Convenience wrapper for sending configured `user_method` to extract the
@@ -97,6 +112,7 @@ module Authority
97
112
  send(Authority.configuration.user_method)
98
113
  end
99
114
 
100
- class MissingAction < StandardError ; end
115
+ class MissingAction < StandardError ; end
116
+ class MissingResource < StandardError ; end
101
117
  end
102
118
  end
@@ -11,17 +11,14 @@ module Authority
11
11
  Authority.verbs.each do |verb|
12
12
  class_eval <<-RUBY, __FILE__, __LINE__ + 1
13
13
  def can_#{verb}?(resource, options = {})
14
- if options.empty?
15
- resource.#{Authority.abilities[verb]}_by?(self)
16
- else
17
- resource.#{Authority.abilities[verb]}_by?(self, options)
18
- end
14
+ self_and_maybe_options = [self, options].tap {|args| args.pop if args.last == {}}
15
+ resource.#{Authority.abilities[verb]}_by?(*self_and_maybe_options)
19
16
  end
20
17
  RUBY
21
18
  end
22
19
 
23
20
  def can?(action, options = {})
24
- self_and_maybe_options = [self, (options == {} ? nil : options)].compact # throw out if nil
21
+ self_and_maybe_options = [self, options].tap {|args| args.pop if args.last == {}}
25
22
  begin
26
23
  ApplicationAuthorizer.send("authorizes_to_#{action}?", *self_and_maybe_options)
27
24
  rescue NoMethodError => original_exception
@@ -1,3 +1,3 @@
1
1
  module Authority
2
- VERSION = "2.3.2"
2
+ VERSION = "2.4.0"
3
3
  end
@@ -4,7 +4,7 @@
4
4
  <title>Forbidden</title>
5
5
  </head>
6
6
  <body>
7
- <h1>Access Denied, HUMAN!</h1>
8
- <p>No, seriously, you can't do that...</p>
7
+ <h1>Access Denied</h1>
8
+ <p>You do not have permission to access this resource.</p>
9
9
  </body>
10
10
  </html>
@@ -3,7 +3,8 @@ Authority.configure do |config|
3
3
  # USER_METHOD
4
4
  # ===========
5
5
  # Authority needs the name of a method, available in any controller, which
6
- # will return the currently logged-in user.
6
+ # will return the currently logged-in user. (If this varies by controller,
7
+ # just create a common alias.)
7
8
  #
8
9
  # Default is:
9
10
  #
@@ -44,14 +45,6 @@ Authority.configure do |config|
44
45
  # :delete => 'deletable'
45
46
  # }
46
47
 
47
- # SECURITY_VIOLATION_HANDLER
48
- # ==========================
49
- # If a SecurityViolation is raised, what controller method should be used to rescue it?
50
- #
51
- # Default is:
52
- #
53
- # config.security_violation_handler = :authority_forbidden # Defined in controller.rb
54
-
55
48
  # LOGGER
56
49
  # ======
57
50
  # If a user tries to perform an unauthorized action, where should we log that fact?
@@ -87,11 +87,16 @@ describe Authority::Controller do
87
87
 
88
88
  describe "authorize_actions_for" do
89
89
 
90
- it "allows specifying the model to protect" do
90
+ it "allows specifying the class of the model to protect" do
91
91
  controller_class.authorize_actions_for(resource_class)
92
92
  expect(controller_class.authority_resource).to eq(resource_class)
93
93
  end
94
94
 
95
+ it "allows specifying an instance method to find the class of the model to protect" do
96
+ controller_class.authorize_actions_for(:finder_method)
97
+ expect(controller_class.authority_resource).to eq(:finder_method)
98
+ end
99
+
95
100
  it "sets up a before_filter, passing the options it was given" do
96
101
  filter_options = {:only => [:show, :edit, :update]}
97
102
  controller_class.should_receive(:before_filter).with(:run_authorization_check, filter_options)
@@ -146,9 +151,48 @@ describe Authority::Controller do
146
151
 
147
152
  describe "run_authorization_check (used as a before_filter)" do
148
153
 
149
- it "checks authorization on the model specified" do
150
- controller_instance.should_receive(:authorize_action_for).with(resource_class)
151
- controller_instance.send(:run_authorization_check)
154
+ context "if a resource class was specified" do
155
+
156
+ it "checks authorization on the model specified" do
157
+ controller_instance.should_receive(:authorize_action_for).with(resource_class)
158
+ controller_instance.send(:run_authorization_check)
159
+ end
160
+
161
+ end
162
+
163
+ context "if a method for determining the class was specified" do
164
+
165
+ let(:resource_class) { Hash }
166
+ let(:controller_class) do
167
+ Class.new(ExampleController).tap do |c|
168
+ c.send(:include, Authority::Controller)
169
+ c.authorize_actions_for(:method_to_find_class)
170
+ end
171
+ end
172
+
173
+ context "if the controller has such an instance method" do
174
+
175
+ before :each do
176
+ controller_instance.stub(:method_to_find_class).and_return(resource_class)
177
+ end
178
+
179
+ it "checks authorization on class returned by that method" do
180
+ controller_instance.should_receive(:authorize_action_for).with(resource_class)
181
+ controller_instance.send(:run_authorization_check)
182
+ end
183
+
184
+ end
185
+
186
+ context "if the controller has no such instance method" do
187
+
188
+ it "raises an exception" do
189
+ expect{controller_instance.send(:run_authorization_check)}.to raise_error(
190
+ Authority::Controller::MissingResource
191
+ )
192
+ end
193
+
194
+ end
195
+
152
196
  end
153
197
 
154
198
  it "raises a MissingAction if there is no corresponding action for the controller" do
@@ -42,7 +42,7 @@ describe Authority do
42
42
  let(:user) { ExampleUser.new }
43
43
  let(:resource_class) { ExampleResource }
44
44
 
45
- describe "if given options" do
45
+ describe "when given options" do
46
46
 
47
47
  it "checks the user's authorization, passing along the options" do
48
48
  options = { :for => 'context' }
@@ -52,7 +52,7 @@ describe Authority do
52
52
 
53
53
  end
54
54
 
55
- describe "if not given options" do
55
+ describe "when not given options" do
56
56
 
57
57
  it "checks the user's authorization, passing no options" do
58
58
  user.should_receive(:can_delete?).with(resource_class).and_return(true)
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: 2.3.2
4
+ version: 2.4.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -10,11 +10,11 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2012-12-11 00:00:00.000000000 Z
13
+ date: 2013-02-13 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: rails
17
- requirement: &2152546080 !ruby/object:Gem::Requirement
17
+ requirement: !ruby/object:Gem::Requirement
18
18
  none: false
19
19
  requirements:
20
20
  - - ! '>='
@@ -22,7 +22,12 @@ dependencies:
22
22
  version: 3.0.0
23
23
  type: :runtime
24
24
  prerelease: false
25
- version_requirements: *2152546080
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ none: false
27
+ requirements:
28
+ - - ! '>='
29
+ - !ruby/object:Gem::Version
30
+ version: 3.0.0
26
31
  description: Authority helps you authorize actions in your Rails app. It's ORM-neutral
27
32
  and has very little fancy syntax; just group your models under one or more Authorizer
28
33
  classes and write plain Ruby methods on them.
@@ -82,15 +87,21 @@ required_ruby_version: !ruby/object:Gem::Requirement
82
87
  - - ! '>='
83
88
  - !ruby/object:Gem::Version
84
89
  version: '0'
90
+ segments:
91
+ - 0
92
+ hash: -37039868613240694
85
93
  required_rubygems_version: !ruby/object:Gem::Requirement
86
94
  none: false
87
95
  requirements:
88
96
  - - ! '>='
89
97
  - !ruby/object:Gem::Version
90
98
  version: '0'
99
+ segments:
100
+ - 0
101
+ hash: -37039868613240694
91
102
  requirements: []
92
103
  rubyforge_project:
93
- rubygems_version: 1.8.16
104
+ rubygems_version: 1.8.24
94
105
  signing_key:
95
106
  specification_version: 3
96
107
  summary: Authority helps you authorize actions in your Rails app using plain Ruby