authority 0.0.1 → 0.2.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/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
|