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 +54 -19
- data/lib/authority.rb +52 -3
- data/lib/authority/abilities.rb +10 -6
- data/lib/authority/authorizer.rb +6 -6
- data/lib/authority/configuration.rb +34 -0
- data/lib/authority/controller.rb +44 -0
- data/lib/authority/railtie.rb +12 -0
- data/lib/authority/user_abilities.rb +13 -0
- data/lib/authority/version.rb +1 -1
- data/lib/generators/authority/install_generator.rb +21 -0
- data/lib/generators/templates/403.html +10 -0
- data/lib/generators/templates/authority.rb +84 -0
- data/spec/authority/abilities_spec.rb +18 -14
- data/spec/authority/authorizer_spec.rb +16 -3
- data/spec/authority/configuration_spec.rb +64 -0
- data/spec/authority/controller_spec.rb +95 -0
- data/spec/authority/user_abilities_spec.rb +25 -0
- data/spec/authority_spec.rb +50 -2
- data/spec/spec_helper.rb +2 -0
- data/spec/support/ability_model.rb +3 -0
- data/spec/support/example_controller.rb +5 -0
- data/spec/support/mock_rails.rb +7 -0
- data/spec/support/user.rb +3 -0
- metadata +25 -8
- data/spec/support/actor.rb +0 -17
data/README.md
CHANGED
@@ -1,9 +1,22 @@
|
|
1
1
|
# Authority
|
2
2
|
|
3
|
-
## SUPER
|
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
|
-
|
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
|
-
|
66
|
+
Then run the generator:
|
43
67
|
|
44
|
-
|
68
|
+
$ rails g authority:install
|
45
69
|
|
46
|
-
|
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)`.
|
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)`
|
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
|
88
|
-
- All
|
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
|
-
-
|
125
|
-
-
|
126
|
-
-
|
127
|
-
-
|
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.
|
134
|
-
4.
|
135
|
-
5.
|
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
|
-
|
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/
|
10
|
-
require 'authority/
|
56
|
+
require 'authority/configuration'
|
57
|
+
require 'authority/controller'
|
58
|
+
require 'authority/railtie' if defined?(Rails)
|
11
59
|
require 'authority/version'
|
60
|
+
|
data/lib/authority/abilities.rb
CHANGED
@@ -10,12 +10,12 @@ module Authority
|
|
10
10
|
|
11
11
|
module ClassMethods
|
12
12
|
|
13
|
-
|
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?(
|
18
|
-
authorizer.#{adjective}_by?(
|
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
|
-
|
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?(
|
33
|
-
|
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
|
data/lib/authority/authorizer.rb
CHANGED
@@ -7,18 +7,18 @@ module Authority
|
|
7
7
|
@resource = resource
|
8
8
|
end
|
9
9
|
|
10
|
-
|
10
|
+
Authority.adjectives.each do |adjective|
|
11
11
|
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
12
|
-
def self.#{adjective}_by?(
|
13
|
-
|
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
|
-
|
18
|
+
Authority.adjectives.each do |adjective|
|
19
19
|
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
20
|
-
def #{adjective}_by?(
|
21
|
-
|
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
|
data/lib/authority/version.rb
CHANGED
@@ -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,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/
|
3
|
+
require 'support/user'
|
4
4
|
|
5
5
|
describe Authority::Abilities do
|
6
6
|
|
7
7
|
before :each do
|
8
|
-
@
|
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
|
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(@
|
50
|
-
AbilityModel.send(method_name, @
|
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
|
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(@
|
74
|
-
@ability_model.send(method_name, @
|
73
|
+
@authorizer.should_receive(method_name).with(@user)
|
74
|
+
@ability_model.send(method_name, @user)
|
75
75
|
end
|
76
76
|
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
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
|
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
|
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
|
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
|
data/spec/authority_spec.rb
CHANGED
@@ -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
|
-
|
5
|
-
|
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
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
|
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-
|
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: &
|
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: *
|
25
|
+
version_requirements: *2152320300
|
26
26
|
- !ruby/object:Gem::Dependency
|
27
27
|
name: bundler
|
28
|
-
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: *
|
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/
|
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/
|
108
|
+
- spec/support/example_controller.rb
|
109
|
+
- spec/support/mock_rails.rb
|
110
|
+
- spec/support/user.rb
|
data/spec/support/actor.rb
DELETED
@@ -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
|