authority 0.0.1 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -1,9 +1,22 @@
1
1
  # Authority
2
2
 
3
- ## SUPER ALPHA VERSION
3
+ ## SUPER BETA VERSION. Stabler release coming soon.
4
+
5
+ ## TL;DR
6
+
7
+ No time for reading! Reading is for chumps! Here's the skinny:
8
+
9
+ - Install in your Rails project
10
+ - Put this in your controllers: `check_authorization_on ModelName`
11
+ - Put this in your models: `include Authority::Abilities`
12
+ - For each model you have, create a corresponding Authorization file. For example, for `app/models/lolcat.rb`, create `app/authorizations/lolcat_authorization.rb` with an empty class inheriting from `Authorization`.
13
+ - Add class methods to that authorization to set rules that can be enforced just by looking at the resource class, like "this user cannot create Lolcats, period."
14
+ - Add instance methods to that authorization 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'".
4
15
 
5
16
  ## Overview
6
17
 
18
+ Still here? Reading is fun! You always knew that. Time for a deeper look at things.
19
+
7
20
  Authority gives you a clean and easy way to say, in your Rails app, **who** is allowed to do **what** with your models.
8
21
 
9
22
  It assumes that you already have some kind of user object in your application.
@@ -25,9 +38,20 @@ The goals of Authority are:
25
38
 
26
39
  - 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.
27
40
 
41
+ ## The flow of Authority
42
+
43
+ In broad terms, the authorization process flows like this:
44
+
45
+ - A request comes to a model, either the class or an instance, saying "can this user do this action to you?"
46
+ - The model passes that question to its Authorizer
47
+ - The Authorizer checks whatever user properties and business rules are relevant to answer that question.
48
+ - The answer is passed back up to the model, then back to the original caller
49
+
28
50
  ## Installation
29
51
 
30
- Add this line to your application's Gemfile:
52
+ 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?
53
+
54
+ Now, add this line to your application's Gemfile:
31
55
 
32
56
  gem 'authority'
33
57
 
@@ -39,20 +63,17 @@ Or install it yourself as:
39
63
 
40
64
  $ gem install authority
41
65
 
42
- ## How it works
66
+ Then run the generator:
43
67
 
44
- In broad terms, the authorization process flows like this:
68
+ $ rails g authority:install
45
69
 
46
- - A request comes to a model, either the class or an instance, saying "can this user do this action to you?"
47
- - The model passes that question to its Authorizer
48
- - The Authorizer checks whatever user properties and business rules are relevant to answer that question.
49
- - The answer is passed back up to the model, then back to the original caller
70
+ Hooray! New files! Go look at them.
50
71
 
51
72
  ## Usage
52
73
 
53
74
  ### Users
54
75
 
55
- 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)`. (TODO: Add this module).
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)`.
56
77
 
57
78
  ### Models
58
79
 
@@ -72,7 +93,7 @@ If that's all you need, one line does it.
72
93
 
73
94
  #### In-action usage
74
95
 
75
- 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)` (TODO: determine this)
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)`
76
97
 
77
98
  ### Authorizers
78
99
 
@@ -84,8 +105,9 @@ Authorizers should be added under `app/authorizers`, one for each of your models
84
105
 
85
106
  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:
86
107
 
87
- - All class-level methods defined on `Authority::Authorizer` return false by default; you must override them in your Authorizers to grant permissions. This whitelisting approach will keep you from accidentally allowing things you didn't intend.
88
- - All instance-level methods defined on `Authority::Authorizer` call their corresponding class-level method by default.
108
+ - 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.
89
111
 
90
112
  This combination means that, with this code:
91
113
 
@@ -119,17 +141,30 @@ If you update your authorizer as follows:
119
141
  current_user.can_create?(@laser_cannon) # true; inherited instance method calls class method
120
142
  current_user.can_delete?(@laser_cannon) # Only Larry, and only on Fridays
121
143
 
144
+ ## Integration Notes
145
+
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:..."
147
+
122
148
  ## TODO
123
149
 
124
- - Add a module, to be included in whatever user class an app has, which defines all the `can_(verb)` methods.
125
- - Determine exact syntax for checking rules during a controller action.
126
- - Add ability to customize default authorization
127
- - Add customizable logger for authorization violations
150
+ - Document syntax for checking rules during a controller action
151
+ - Rename Authorization to Authorizer
152
+ - Update generator to create an authorizer for every model
153
+ - Generator
154
+ - Add generators or hook into existing rails generators
155
+ - Add generator to installation instructions
156
+ - Generate well-commented default configuration file like Devise does (shout out!)
157
+ - Generate 403.html, with option to skip if exists
158
+ - Note that you MUST call configure; internals aren't included until you do.
159
+ - Write about configuration file and options in Configuration section.
128
160
 
129
161
  ## Contributing
130
162
 
131
163
  1. Fork it
132
164
  2. Create your feature branch (`git checkout -b my-new-feature`)
133
- 3. Commit your changes (`git commit -am 'Added some feature'`)
134
- 4. Push to the branch (`git push origin my-new-feature`)
135
- 5. Create new Pull Request
165
+ 3. `bundle install` to get all dependencies
166
+ 4. `rspec spec` to run all tests.
167
+ 5. Make your changes and update/add tests as necessary.
168
+ 6. Commit your changes (`git commit -am 'Added some feature'`)
169
+ 7. Push to the branch (`git push origin my-new-feature`)
170
+ 8. Create new Pull Request
data/lib/authority.rb CHANGED
@@ -1,11 +1,60 @@
1
1
  require 'active_support/concern'
2
2
  require 'active_support/core_ext/class/attribute'
3
+ require 'active_support/core_ext/hash/keys'
3
4
  require 'active_support/core_ext/string/inflections'
5
+ require 'logger'
4
6
 
5
7
  module Authority
6
- ADJECTIVES = %w[creatable readable updatable deletable]
8
+
9
+ # NOTE: once this method is called, the library has started meta programming
10
+ # and abilities should no longer be modified
11
+ def self.abilities
12
+ configuration.abilities.freeze
13
+ end
14
+
15
+ def self.verbs
16
+ abilities.keys
17
+ end
18
+
19
+ def self.adjectives
20
+ abilities.values
21
+ end
22
+
23
+ def self.enforce(action, resource, user)
24
+ action_authorized = user.send("can_#{action}?", resource)
25
+ unless action_authorized
26
+ message = "#{user} is not authorized to #{action} this resource: #{resource.inspect}"
27
+ raise SecurityTransgression.new(message)
28
+ end
29
+ resource
30
+ end
31
+
32
+ class << self
33
+ attr_accessor :configuration
34
+ end
35
+
36
+ def self.configure
37
+ self.configuration ||= Configuration.new
38
+ yield(configuration) if block_given?
39
+ require_authority_internals!
40
+
41
+ configuration
42
+ end
43
+
44
+ private
45
+
46
+ def self.require_authority_internals!
47
+ require 'authority/abilities'
48
+ require 'authority/authorizer'
49
+ require 'authority/user_abilities'
50
+ end
51
+
52
+ class SecurityTransgression < StandardError ; end
53
+
7
54
  end
8
55
 
9
- require 'authority/abilities'
10
- require 'authority/authorizer'
56
+ require 'authority/configuration'
57
+ require 'authority/controller'
58
+ require 'authority/railtie' if defined?(Rails)
11
59
  require 'authority/version'
60
+
@@ -10,12 +10,12 @@ module Authority
10
10
 
11
11
  module ClassMethods
12
12
 
13
- ADJECTIVES.each do |adjective|
13
+ Authority.adjectives.each do |adjective|
14
14
 
15
15
  # Metaprogram needed methods, allowing for nice backtraces
16
16
  class_eval <<-RUBY, __FILE__, __LINE__ + 1
17
- def #{adjective}_by?(actor)
18
- authorizer.#{adjective}_by?(actor)
17
+ def #{adjective}_by?(user)
18
+ authorizer.#{adjective}_by?(user)
19
19
  end
20
20
  RUBY
21
21
  end
@@ -25,12 +25,16 @@ module Authority
25
25
  end
26
26
  end
27
27
 
28
- ADJECTIVES.each do |adjective|
28
+ Authority.adjectives.each do |adjective|
29
29
 
30
30
  # Metaprogram needed methods, allowing for nice backtraces
31
31
  class_eval <<-RUBY, __FILE__, __LINE__ + 1
32
- def #{adjective}_by?(actor)
33
- self.class.authorizer.new(self).#{adjective}_by?(actor)
32
+ def #{adjective}_by?(user)
33
+ authorizer.#{adjective}_by?(user)
34
+ end
35
+
36
+ def authorizer
37
+ self.class.authorizer.new(self)
34
38
  end
35
39
  RUBY
36
40
  end
@@ -7,18 +7,18 @@ module Authority
7
7
  @resource = resource
8
8
  end
9
9
 
10
- ADJECTIVES.each do |adjective|
10
+ Authority.adjectives.each do |adjective|
11
11
  class_eval <<-RUBY, __FILE__, __LINE__ + 1
12
- def self.#{adjective}_by?(actor)
13
- false
12
+ def self.#{adjective}_by?(user)
13
+ Authority.configuration.default_strategy.call(:#{adjective}, self, user)
14
14
  end
15
15
  RUBY
16
16
  end
17
17
 
18
- ADJECTIVES.each do |adjective|
18
+ Authority.adjectives.each do |adjective|
19
19
  class_eval <<-RUBY, __FILE__, __LINE__ + 1
20
- def #{adjective}_by?(actor)
21
- false
20
+ def #{adjective}_by?(user)
21
+ self.class.#{adjective}_by?(user)
22
22
  end
23
23
  RUBY
24
24
  end
@@ -0,0 +1,34 @@
1
+ module Authority
2
+ class Configuration
3
+
4
+ attr_accessor :default_strategy, :abilities, :authority_actions, :user_method, :logger
5
+
6
+ def initialize
7
+ @default_strategy = Proc.new { |able, authorizer, user|
8
+ false
9
+ }
10
+
11
+ @abilities = {
12
+ :create => 'creatable',
13
+ :read => 'readable',
14
+ :update => 'updatable',
15
+ :delete => 'deletable'
16
+ }
17
+
18
+ @authority_actions = {
19
+ :index => 'read',
20
+ :show => 'read',
21
+ :new => 'create',
22
+ :create => 'create',
23
+ :edit => 'update',
24
+ :update => 'update',
25
+ :destroy => 'delete'
26
+ }
27
+
28
+ @user_method = :current_user
29
+
30
+ @logger = Logger.new(STDERR)
31
+ end
32
+
33
+ end
34
+ end
@@ -0,0 +1,44 @@
1
+ module Authority
2
+ module Controller
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ rescue_from Authority::SecurityTransgression, :with => 'forbidden'
7
+ class_attribute :authority_resource
8
+ class_attribute :authority_actions
9
+ end
10
+
11
+ module ClassMethods
12
+ def check_authorization_on(model_class, options = {})
13
+ self.authority_resource = model_class
14
+ self.authority_actions = Authority.configuration.authority_actions.merge(options[:actions] || {}).symbolize_keys
15
+ before_filter :run_authorization_check, options
16
+ end
17
+
18
+ def authority_action(action_map)
19
+ self.authority_actions.merge!(action_map).symbolize_keys
20
+ end
21
+ end
22
+
23
+ protected
24
+
25
+ def authority_forbidden(error)
26
+ Authority.configuration.logger.warn(error.message)
27
+ render :file => Rails.root.join('public', '403.html'), :status => 403
28
+ end
29
+
30
+ def run_authorization_check
31
+ check_authorization_for self.class.authority_resource, send(Authority.configuration.user_method)
32
+ end
33
+
34
+ def check_authorization_for(authority_resource, user)
35
+ authority_action = self.class.authority_actions[action_name.to_sym]
36
+ if authority_action.nil?
37
+ raise MissingAction.new("No authority action defined for #{action_name}")
38
+ end
39
+ Authority.enforce(authority_action, authority_resource, user)
40
+ end
41
+
42
+ class MissingAction < StandardError ; end
43
+ end
44
+ end
@@ -0,0 +1,12 @@
1
+ require 'rails'
2
+
3
+ module Authority
4
+ class Railtie < ::Rails::Railtie
5
+
6
+ initializer "authority.controller" do
7
+ ApplicationController.send(:include, Authority::Controller)
8
+ end
9
+
10
+ end
11
+ end
12
+
@@ -0,0 +1,13 @@
1
+ module Authority
2
+ module UserAbilities
3
+
4
+ Authority.verbs.each do |verb|
5
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
6
+ def can_#{verb}?(resource)
7
+ resource.#{Authority.abilities[verb]}_by?(self)
8
+ end
9
+ RUBY
10
+ end
11
+
12
+ end
13
+ end
@@ -1,3 +1,3 @@
1
1
  module Authority
2
- VERSION = "0.0.1"
2
+ VERSION = "0.2.0"
3
3
  end
@@ -0,0 +1,21 @@
1
+ require 'rails/generators/base'
2
+
3
+ module Authority
4
+ module Generators
5
+ class InstallGenerator < Rails::Generators::Base
6
+
7
+ source_root File.expand_path("../../templates", __FILE__)
8
+
9
+ desc "Creates an Authority initializer for your application."
10
+
11
+ def copy_initializer
12
+ template "authority.rb", "config/initializers/authority.rb"
13
+ end
14
+
15
+ def copy_forbidden
16
+ template "403.html", "public/403.html"
17
+ end
18
+
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,10 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>Forbidden</title>
5
+ </head>
6
+ <body>
7
+ <h1>Access Denied, HUMAN!</h1>
8
+ <p>No, seriously, you can't do that...</p>
9
+ </body>
10
+ </html>
@@ -0,0 +1,84 @@
1
+ Authority.configure do |config|
2
+
3
+ # USER_METHOD
4
+ # ===========
5
+ # Authority needs the name of a method, available in any controller, which
6
+ # will return the currently logged-in user.
7
+ #
8
+ # Default is:
9
+ #
10
+ # config.user_method = :current_user
11
+
12
+ # DEFAULT_STRATEGY
13
+ # ================
14
+ # When no class-level method is defined on an Authorizer, a default_strategy
15
+ # proc will be called to determine what to do.
16
+ # Depending on your app, you may be able to put all the logic you need here.
17
+ #
18
+ # The arguments passed to this proc will be:
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`
23
+ # user - user object (whatever that is in your application; found using config.user_method)
24
+ #
25
+ # For example:
26
+ #
27
+ # config.default_strategy = Proc.new { |able, authorizer, user|
28
+ # # Does the user have any roles which give this permission?
29
+ # (Permissions.find_by_name_and_authorizer(able, authorizer).roles & user.roles).any?
30
+ # }
31
+ #
32
+ # OR
33
+ #
34
+ # config.default_strategy = Proc.new { |able, authorizer, user|
35
+ # able != 'implodable_by?' && user.has_hairstyle?('pompadour')
36
+ # }
37
+ #
38
+ # Default strategy simply returns false, as follows:
39
+ #
40
+ # config.default_strategy = Proc.new { |able, authorizer, user| false }
41
+
42
+ # AUTHORITY_ACTIONS
43
+ # For a given controller method, what verb must a user be able to do?
44
+ # For example, a user can access 'show' if they 'can_read' the resource.
45
+ #
46
+ # Defaults are as follows:
47
+ #
48
+ # config.authority_actions = {
49
+ # :index => 'read',
50
+ # :show => 'read',
51
+ # :new => 'create',
52
+ # :create => 'create',
53
+ # :edit => 'update',
54
+ # :update => 'update',
55
+ # :destroy => 'delete'
56
+ # }
57
+
58
+ # ABILITIES
59
+ # Teach Authority how to understand the verbs and adjectives in your system. Perhaps you
60
+ # need {:microwave => 'microwavable'}. I'm not saying you do, of course. Stop looking at
61
+ # me like that.
62
+ #
63
+ # Defaults are as follows:
64
+ #
65
+ # config.abilities = {
66
+ # :create => 'creatable',
67
+ # :read => 'readable',
68
+ # :update => 'updatable',
69
+ # :delete => 'deletable'
70
+ # }
71
+
72
+ # LOGGER
73
+ # If a user tries to perform an unauthorized action, where should we log that fact?
74
+ # Provide a logger object which responds to `.warn(message)`
75
+ #
76
+ # Default is:
77
+ #
78
+ # config.logger = Logger.new(STDERR)
79
+ #
80
+ # Suggested setting for a Rails app is:
81
+ config.logger = Rails.logger
82
+
83
+ end
84
+
@@ -1,11 +1,11 @@
1
1
  require 'spec_helper'
2
2
  require 'support/ability_model'
3
- require 'support/actor'
3
+ require 'support/user'
4
4
 
5
5
  describe Authority::Abilities do
6
6
 
7
7
  before :each do
8
- @actor = Actor.new
8
+ @user = User.new
9
9
  end
10
10
 
11
11
  describe "authorizer" do
@@ -38,7 +38,7 @@ describe Authority::Abilities do
38
38
 
39
39
  describe "class methods" do
40
40
 
41
- Authority::ADJECTIVES.each do |adjective|
41
+ Authority.adjectives.each do |adjective|
42
42
  method_name = "#{adjective}_by?"
43
43
 
44
44
  it "should respond to `#{method_name}`" do
@@ -46,8 +46,8 @@ describe Authority::Abilities do
46
46
  end
47
47
 
48
48
  it "should delegate `#{method_name}` to its authorizer class" do
49
- AbilityModel.authorizer.should_receive(method_name).with(@actor)
50
- AbilityModel.send(method_name, @actor)
49
+ AbilityModel.authorizer.should_receive(method_name).with(@user)
50
+ AbilityModel.send(method_name, @user)
51
51
  end
52
52
 
53
53
  end
@@ -61,7 +61,7 @@ describe Authority::Abilities do
61
61
  @authorizer = AbilityModel.authorizer.new(@ability_model)
62
62
  end
63
63
 
64
- Authority::ADJECTIVES.each do |adjective|
64
+ Authority.adjectives.each do |adjective|
65
65
  method_name = "#{adjective}_by?"
66
66
 
67
67
  it "should respond to `#{method_name}`" do
@@ -70,17 +70,21 @@ describe Authority::Abilities do
70
70
 
71
71
  it "should delegate `#{method_name}` to a new authorizer instance" do
72
72
  AbilityModel.authorizer.stub(:new).and_return(@authorizer)
73
- @authorizer.should_receive(method_name).with(@actor)
74
- @ability_model.send(method_name, @actor)
73
+ @authorizer.should_receive(method_name).with(@user)
74
+ @ability_model.send(method_name, @user)
75
75
  end
76
76
 
77
- it "should always create a new authorizer instance when checking `#{method_name}`" do
78
- 2.times do
79
- @ability_model.class.authorizer.should_receive(:new).with(@ability_model).and_return(@authorizer)
80
- @ability_model.send(method_name, @actor)
81
- end
82
- end
77
+ end
78
+
79
+ it "should provide an accessor for its authorizer" do
80
+ @ability_model.should respond_to(:authorizer)
81
+ end
83
82
 
83
+ # TODO: Nathan will comment more clearly in the future
84
+ # aka "don't memoize" (to prevent dirty models from contaminating authorization)
85
+ it "should always create a new authorizer instance when accessing the authorizer" do
86
+ @ability_model.class.authorizer.should_receive(:new).with(@ability_model).twice
87
+ 2.times { @ability_model.authorizer }
84
88
  end
85
89
 
86
90
  end
@@ -1,11 +1,13 @@
1
1
  require 'spec_helper'
2
2
  require 'support/ability_model'
3
+ require 'support/user'
3
4
 
4
5
  describe Authority::Authorizer do
5
6
 
6
7
  before :each do
7
8
  @ability_model = AbilityModel.new
8
- @authorizer = Authority::Authorizer.new(@ability_model)
9
+ @authorizer = @ability_model.authorizer
10
+ @user = User.new
9
11
  end
10
12
 
11
13
  it "should take a resource instance in its initializer" do
@@ -14,25 +16,36 @@ describe Authority::Authorizer do
14
16
 
15
17
  describe "class methods" do
16
18
 
17
- Authority::ADJECTIVES.each do |adjective|
19
+ Authority.adjectives.each do |adjective|
18
20
  method_name = "#{adjective}_by?"
19
21
 
20
22
  it "should respond to `#{method_name}`" do
21
23
  Authority::Authorizer.should respond_to(method_name)
22
24
  end
23
25
 
26
+ it "should run the default authorization strategy block" do
27
+ able = method_name.sub('_by?', '').to_sym
28
+ Authority.configuration.default_strategy.should_receive(:call).with(able, Authority::Authorizer, @user)
29
+ Authority::Authorizer.send(method_name, @user)
30
+ end
31
+
24
32
  end
25
33
 
26
34
  end
27
35
 
28
36
  describe "instance methods" do
29
37
 
30
- Authority::ADJECTIVES.each do |adjective|
38
+ Authority.adjectives.each do |adjective|
31
39
  method_name = "#{adjective}_by?"
32
40
 
33
41
  it "should respond to `#{method_name}`" do
34
42
  @authorizer.should respond_to(method_name)
35
43
  end
44
+
45
+ it "should delegate `#{method_name}` to the corresponding class method by default" do
46
+ @authorizer.class.should_receive(method_name).with(@user)
47
+ @authorizer.send(method_name, @user)
48
+ end
36
49
 
37
50
  end
38
51
 
@@ -0,0 +1,64 @@
1
+ require 'spec_helper'
2
+
3
+ describe Authority::Configuration do
4
+ describe "the default configuration" do
5
+
6
+ it "should have a default authorization strategy block" do
7
+ Authority.configuration.default_strategy.should respond_to(:call)
8
+ end
9
+
10
+ it "should return false when calling the default authorization strategy block" do
11
+ Authority.configuration.default_strategy.call(:action, Authority::Authorizer, User.new).should be_false
12
+ end
13
+
14
+ it "should have a default authority controller actions map" do
15
+ Authority.configuration.authority_actions.should be_a(Hash)
16
+ end
17
+
18
+ it "should have a default controller method for accessing the user object" do
19
+ Authority.configuration.user_method.should eq(:current_user)
20
+ end
21
+
22
+ describe "logging security violations" do
23
+
24
+ it "should log to standard error by default" do
25
+ Authority.instance_variable_set :@configuration, nil
26
+ null = File.exists?('/dev/null') ? '/dev/null' : 'NUL:' # Allow for Windows
27
+ @logger = Logger.new(null)
28
+ Logger.should_receive(:new).with(STDERR).and_return(@logger)
29
+ Authority.configure
30
+ Authority.configuration.logger
31
+ end
32
+
33
+ end
34
+
35
+ end
36
+
37
+ describe "customizing the configuration" do
38
+ before :all do
39
+ Authority.instance_variable_set :@configuration, nil
40
+ Authority.configure do |config|
41
+ config.abilities[:eat] = 'edible'
42
+ config.default_strategy = Proc.new { |able, authorizer, user|
43
+ true
44
+ }
45
+ end
46
+
47
+ after :all do
48
+ Authority.instance_variable_set :@configuration, nil
49
+ Authority.configure
50
+ end
51
+
52
+ it "should allow customizing the authorization block" do
53
+ Authority.configuration.default_strategy.call(:action, Authority::Authorizer, User.new).should be_true
54
+ end
55
+
56
+ # This shouldn't be used during runtime, only during configuration
57
+ # It won't do anything outside of configuration anyway
58
+ it "should allow adding to the default list of abilities" do
59
+ Authority.configuration.abilities[:eat].should eq('edible')
60
+ end
61
+
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,95 @@
1
+ require 'spec_helper'
2
+ require 'support/ability_model'
3
+ require 'support/example_controller'
4
+ require 'support/mock_rails'
5
+ require 'support/user'
6
+
7
+ describe Authority::Controller do
8
+
9
+ describe "when including" do
10
+ it "should specify rescuing security transgressions" do
11
+ class DummyController < ExampleController ; end
12
+ DummyController.should_receive(:rescue_from).with(Authority::SecurityTransgression, :with => 'forbidden')
13
+ DummyController.send(:include, Authority::Controller)
14
+ end
15
+ end
16
+
17
+ describe "after including" do
18
+ before :all do
19
+ ExampleController.send(:include, Authority::Controller)
20
+ end
21
+
22
+ describe "DSL (class) methods" do
23
+ it "should allow specifying the model to protect" do
24
+ ExampleController.check_authorization_on AbilityModel
25
+ ExampleController.authority_resource.should eq(AbilityModel)
26
+ end
27
+
28
+ it "should pass the options provided to the before filter that is set up" do
29
+ @options = {:only => [:show, :edit, :update]}
30
+ ExampleController.should_receive(:before_filter).with(:run_authorization_check, @options)
31
+ ExampleController.check_authorization_on AbilityModel, @options
32
+ end
33
+
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)
38
+ end
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')
43
+ end
44
+
45
+ it "should have a write into the authority actions map usuable in a DSL format" do
46
+ ExampleController.authority_action :smite => 'delete'
47
+ ExampleController.authority_actions[:smite].should eq('delete')
48
+ end
49
+ end
50
+
51
+ describe "instance methods" do
52
+ before :each do
53
+ @user = User.new
54
+ @controller = ExampleController.new
55
+ @controller.stub!(:action_name).and_return(:edit)
56
+ @controller.stub!(Authority.configuration.user_method).and_return(@user)
57
+ end
58
+
59
+ it "should check authorization on the model specified" do
60
+ @controller.should_receive(:check_authorization_for).with(AbilityModel, @user)
61
+ @controller.send(:run_authorization_check)
62
+ end
63
+
64
+ it "should raise a SecurityTransgression if authorization fails" do
65
+ expect { @controller.send(:run_authorization_check) }.to raise_error(Authority::SecurityTransgression)
66
+ end
67
+
68
+ it "should raise a MissingAction if there is no corresponding action for the controller" do
69
+ @controller.stub(:action_name).and_return('sculpt')
70
+ expect { @controller.send(:run_authorization_check) }.to raise_error(Authority::Controller::MissingAction)
71
+ end
72
+
73
+ describe "authority_forbidden action" do
74
+
75
+ before :each do
76
+ @mock_error = mock(:message => 'oh noes! an error!')
77
+ end
78
+
79
+ it "should log an error" do
80
+ Authority.configuration.logger.should_receive(:warn)
81
+ @controller.stub(:render)
82
+ @controller.send(:authority_forbidden, @mock_error)
83
+ end
84
+
85
+ it "should render the public/403.html file" do
86
+ forbidden_page = Rails.root.join('public/403.html')
87
+ @controller.should_receive(:render).with(:file => forbidden_page, :status => 403)
88
+ @controller.send(:authority_forbidden, @mock_error)
89
+ end
90
+ end
91
+ end
92
+ end
93
+
94
+ end
95
+
@@ -0,0 +1,25 @@
1
+ require 'spec_helper'
2
+ require 'support/ability_model'
3
+ require 'support/user'
4
+
5
+ describe Authority::UserAbilities do
6
+
7
+ before :each do
8
+ @ability_model = AbilityModel.new
9
+ @user = User.new
10
+ end
11
+
12
+ Authority.verbs.each do |verb|
13
+ method_name = "can_#{verb}?"
14
+
15
+ it "should define the `#{method_name}` method" do
16
+ @user.should respond_to(method_name)
17
+ end
18
+
19
+ it "should delegate the authorization check to the resource provided" do
20
+ @ability_model.should_receive("#{Authority.abilities[verb]}_by?").with(@user)
21
+ @user.send(method_name, @ability_model)
22
+ end
23
+ end
24
+
25
+ end
@@ -1,7 +1,55 @@
1
1
  require 'spec_helper'
2
+ require 'support/ability_model'
3
+ require 'support/user'
2
4
 
3
5
  describe Authority do
4
- it "should have a constant of abilities" do
5
- Authority::ADJECTIVES.should be_an(Array)
6
+
7
+ it "should have a default list of abilities" do
8
+ Authority.abilities.should be_a(Hash)
9
+ end
10
+
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")
13
+ end
14
+
15
+ it "should have a convenience accessor for the ability verbs" do
16
+ Authority.verbs.sort.should eq([:create, :delete, :read, :update])
6
17
  end
18
+
19
+ it "should have a convenience accessor for the ability adjectives" do
20
+ Authority.adjectives.sort.should eq(%w[creatable deletable readable updatable])
21
+ end
22
+
23
+ describe "configuring Authority" do
24
+
25
+ it "should have a configuration accessor" do
26
+ Authority.should respond_to(:configuration)
27
+ end
28
+
29
+ it "should have a `configure` method" do
30
+ Authority.should respond_to(:configure)
31
+ end
32
+
33
+ it "should require the remainder of library internals after configuration" do
34
+ Authority.should_receive(:require_authority_internals!)
35
+ Authority.configure
36
+ end
37
+ end
38
+
39
+ describe "enforcement" do
40
+
41
+ before :each do
42
+ @user = User.new
43
+ end
44
+
45
+ it "should raise a SecurityTransgression if the action is unauthorized" do
46
+ expect { Authority.enforce(:update, AbilityModel, @user) }.to raise_error(Authority::SecurityTransgression)
47
+ end
48
+
49
+ it "should not raise a SecurityTransgression if the action is authorized" do
50
+ expect { Authority.enforce(:read, AbilityModel, @user) }.not_to raise_error(Authority::SecurityTransgression)
51
+ end
52
+
53
+ end
54
+
7
55
  end
data/spec/spec_helper.rb CHANGED
@@ -2,6 +2,8 @@ $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
2
2
  require 'rspec'
3
3
  require 'authority'
4
4
 
5
+ Authority.configure
6
+
5
7
  RSpec.configure do |config|
6
8
  config.mock_with :rspec
7
9
  end
@@ -3,4 +3,7 @@ class AbilityModel
3
3
  end
4
4
 
5
5
  class AbilityModelAuthorizer < Authority::Authorizer
6
+ def self.readable_by?(user)
7
+ true
8
+ end
6
9
  end
@@ -0,0 +1,5 @@
1
+ class ExampleController
2
+ def self.rescue_from(*args) ; end
3
+
4
+ def self.before_filter(*args) ; end
5
+ end
@@ -0,0 +1,7 @@
1
+ require 'pathname'
2
+
3
+ module Rails
4
+ def self.root
5
+ Pathname.new('.')
6
+ end
7
+ end
@@ -0,0 +1,3 @@
1
+ class User
2
+ include Authority::UserAbilities
3
+ 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.0.1
4
+ version: 0.2.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-03-12 00:00:00.000000000 Z
13
+ date: 2012-03-13 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: rails
17
- requirement: &2152357800 !ruby/object:Gem::Requirement
17
+ requirement: &2152320300 !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: *2152357800
25
+ version_requirements: *2152320300
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: bundler
28
- requirement: &2152357040 !ruby/object:Gem::Requirement
28
+ requirement: &2152319760 !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: *2152357040
36
+ version_requirements: *2152319760
37
37
  description: Gem for managing authorization on model actions in Rails
38
38
  email:
39
39
  - nathanmlong@gmail.com
@@ -52,13 +52,25 @@ files:
52
52
  - lib/authority.rb
53
53
  - lib/authority/abilities.rb
54
54
  - lib/authority/authorizer.rb
55
+ - lib/authority/configuration.rb
56
+ - lib/authority/controller.rb
57
+ - lib/authority/railtie.rb
58
+ - lib/authority/user_abilities.rb
55
59
  - lib/authority/version.rb
60
+ - lib/generators/authority/install_generator.rb
61
+ - lib/generators/templates/403.html
62
+ - lib/generators/templates/authority.rb
56
63
  - spec/authority/abilities_spec.rb
57
64
  - spec/authority/authorizer_spec.rb
65
+ - spec/authority/configuration_spec.rb
66
+ - spec/authority/controller_spec.rb
67
+ - spec/authority/user_abilities_spec.rb
58
68
  - spec/authority_spec.rb
59
69
  - spec/spec_helper.rb
60
70
  - spec/support/ability_model.rb
61
- - spec/support/actor.rb
71
+ - spec/support/example_controller.rb
72
+ - spec/support/mock_rails.rb
73
+ - spec/support/user.rb
62
74
  homepage: https://github.com/nathanl/authority
63
75
  licenses: []
64
76
  post_install_message:
@@ -87,7 +99,12 @@ summary: Authority gives you a clean and easy way to say, in your Rails app, **w
87
99
  test_files:
88
100
  - spec/authority/abilities_spec.rb
89
101
  - spec/authority/authorizer_spec.rb
102
+ - spec/authority/configuration_spec.rb
103
+ - spec/authority/controller_spec.rb
104
+ - spec/authority/user_abilities_spec.rb
90
105
  - spec/authority_spec.rb
91
106
  - spec/spec_helper.rb
92
107
  - spec/support/ability_model.rb
93
- - spec/support/actor.rb
108
+ - spec/support/example_controller.rb
109
+ - spec/support/mock_rails.rb
110
+ - spec/support/user.rb
@@ -1,17 +0,0 @@
1
- class Actor
2
- def can_create?(resource)
3
- resource.creatable_by?(self)
4
- end
5
-
6
- def can_read?(resource)
7
- resource.readable_by?(self)
8
- end
9
-
10
- def can_update?(resource)
11
- resource.updatable_by?(self)
12
- end
13
-
14
- def can_delete?(resource)
15
- resource.deletable_by?(self)
16
- end
17
- end