authority 2.2.0 → 2.3.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.markdown +6 -0
- data/Gemfile +1 -1
- data/README.markdown +27 -11
- data/TODO.markdown +5 -6
- data/lib/authority.rb +12 -21
- data/lib/authority/abilities.rb +3 -1
- data/lib/authority/controller.rb +31 -22
- data/lib/authority/security_violation.rb +15 -0
- data/lib/authority/user_abilities.rb +4 -0
- data/lib/authority/version.rb +1 -1
- data/spec/authority/abilities_spec.rb +76 -61
- data/spec/authority/authorizer_spec.rb +43 -39
- data/spec/authority/configuration_spec.rb +11 -11
- data/spec/authority/controller_spec.rb +151 -86
- data/spec/authority/integration_spec.rb +34 -29
- data/spec/authority/user_abilities_spec.rb +30 -20
- data/spec/authority_spec.rb +37 -41
- data/spec/spec_helper.rb +1 -0
- data/spec/support/{example_model.rb → example_classes.rb} +5 -1
- metadata +6 -7
- data/spec/support/example_controllers.rb +0 -22
- data/spec/support/user.rb +0 -3
@@ -1,17 +1,14 @@
|
|
1
1
|
require 'spec_helper'
|
2
|
-
require 'support/
|
3
|
-
require 'support/user'
|
2
|
+
require 'support/example_classes'
|
4
3
|
|
5
4
|
describe Authority::Authorizer do
|
6
5
|
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
@user = User.new
|
11
|
-
end
|
6
|
+
let(:model_instance) { ExampleResource.new }
|
7
|
+
let(:authorizer) { model_instance.authorizer }
|
8
|
+
let(:user) { ExampleUser.new }
|
12
9
|
|
13
|
-
it "
|
14
|
-
|
10
|
+
it "takes a resource instance in its initializer" do
|
11
|
+
expect(authorizer.resource).to eq(model_instance)
|
15
12
|
end
|
16
13
|
|
17
14
|
describe "instance methods" do
|
@@ -19,25 +16,28 @@ describe Authority::Authorizer do
|
|
19
16
|
Authority.adjectives.each do |adjective|
|
20
17
|
method_name = "#{adjective}_by?"
|
21
18
|
|
22
|
-
it "
|
23
|
-
|
19
|
+
it "responds to `#{method_name}`" do
|
20
|
+
expect(authorizer).to respond_to(method_name)
|
24
21
|
end
|
25
22
|
|
23
|
+
describe "#{method_name}" do
|
24
|
+
|
25
|
+
context "when given an options hash" do
|
26
26
|
|
27
|
-
|
27
|
+
it "delegates `#{method_name}` to the corresponding class method, passing the options" do
|
28
|
+
authorizer.class.should_receive(method_name).with(user, :under => 'God')
|
29
|
+
authorizer.send(method_name, user, :under => 'God')
|
30
|
+
end
|
28
31
|
|
29
|
-
it "should delegate `#{method_name}` to the corresponding class method, passing the options" do
|
30
|
-
@authorizer.class.should_receive(method_name).with(@user, :under => 'God')
|
31
|
-
@authorizer.send(method_name, @user, :under => 'God')
|
32
32
|
end
|
33
33
|
|
34
|
-
|
34
|
+
context "when not given an options hash" do
|
35
35
|
|
36
|
-
|
36
|
+
it "delegates `#{method_name}` to the corresponding class method, passing no options" do
|
37
|
+
authorizer.class.should_receive(method_name).with(user)
|
38
|
+
authorizer.send(method_name, user)
|
39
|
+
end
|
37
40
|
|
38
|
-
it "should delegate `#{method_name}` to the corresponding class method, passing no options" do
|
39
|
-
@authorizer.class.should_receive(method_name).with(@user)
|
40
|
-
@authorizer.send(method_name, @user)
|
41
41
|
end
|
42
42
|
|
43
43
|
end
|
@@ -51,26 +51,30 @@ describe Authority::Authorizer do
|
|
51
51
|
Authority.adjectives.each do |adjective|
|
52
52
|
method_name = "#{adjective}_by?"
|
53
53
|
|
54
|
-
it "
|
55
|
-
Authority::Authorizer.
|
54
|
+
it "responds to `#{method_name}`" do
|
55
|
+
expect(Authority::Authorizer).to respond_to(method_name)
|
56
56
|
end
|
57
57
|
|
58
|
-
describe "
|
58
|
+
describe "#{method_name}" do
|
59
|
+
|
60
|
+
context "when given an options hash" do
|
61
|
+
|
62
|
+
it "delegates `#{method_name}` to the authorizer's `default` method, passing the options" do
|
63
|
+
able = method_name.sub('_by?', '').to_sym
|
64
|
+
Authority::Authorizer.should_receive(:default).with(able, user, :with => 'gusto')
|
65
|
+
Authority::Authorizer.send(method_name, user, :with => 'gusto')
|
66
|
+
end
|
59
67
|
|
60
|
-
it "should delegate `#{method_name}` to the authorizer's `default` method, passing the options" do
|
61
|
-
able = method_name.sub('_by?', '').to_sym
|
62
|
-
Authority::Authorizer.should_receive(:default).with(able, @user, :with => 'gusto')
|
63
|
-
Authority::Authorizer.send(method_name, @user, :with => 'gusto')
|
64
68
|
end
|
65
69
|
|
66
|
-
|
70
|
+
context "when not given an options hash" do
|
67
71
|
|
68
|
-
|
72
|
+
it "delegates `#{method_name}` to the authorizer's `default` method, passing no options" do
|
73
|
+
able = method_name.sub('_by?', '').to_sym
|
74
|
+
Authority::Authorizer.should_receive(:default).with(able, user)
|
75
|
+
Authority::Authorizer.send(method_name, user)
|
76
|
+
end
|
69
77
|
|
70
|
-
it "should delegate `#{method_name}` to the authorizer's `default` method, passing no options" do
|
71
|
-
able = method_name.sub('_by?', '').to_sym
|
72
|
-
Authority::Authorizer.should_receive(:default).with(able, @user)
|
73
|
-
Authority::Authorizer.send(method_name, @user)
|
74
78
|
end
|
75
79
|
|
76
80
|
end
|
@@ -81,17 +85,17 @@ describe Authority::Authorizer do
|
|
81
85
|
|
82
86
|
describe "the default method" do
|
83
87
|
|
84
|
-
|
88
|
+
context "when given an options hash" do
|
85
89
|
|
86
|
-
it "
|
87
|
-
Authority::Authorizer.default(:implodable,
|
90
|
+
it "returns false" do
|
91
|
+
expect(Authority::Authorizer.default(:implodable, user, {:for => "my_object"})).to be_false
|
88
92
|
end
|
89
93
|
end
|
90
94
|
|
91
|
-
|
95
|
+
context "when not given an options hash" do
|
92
96
|
|
93
|
-
it "
|
94
|
-
Authority::Authorizer.default(:implodable,
|
97
|
+
it "returns false" do
|
98
|
+
expect(Authority::Authorizer.default(:implodable, user)).to be_false
|
95
99
|
end
|
96
100
|
|
97
101
|
end
|
@@ -3,21 +3,21 @@ require 'spec_helper'
|
|
3
3
|
describe Authority::Configuration do
|
4
4
|
describe "the default configuration" do
|
5
5
|
|
6
|
-
it "
|
7
|
-
Authority.configuration.controller_action_map.
|
6
|
+
it "has a default authority controller actions map" do
|
7
|
+
expect(Authority.configuration.controller_action_map).to be_a(Hash)
|
8
8
|
end
|
9
9
|
|
10
|
-
it "
|
11
|
-
Authority.configuration.user_method.
|
10
|
+
it "has a default controller method for accessing the user object" do
|
11
|
+
expect(Authority.configuration.user_method).to eq(:current_user)
|
12
12
|
end
|
13
13
|
|
14
14
|
describe "logging security violations" do
|
15
15
|
|
16
|
-
it "
|
16
|
+
it "logs to standard error by default" do
|
17
17
|
Authority.instance_variable_set :@configuration, nil
|
18
18
|
null = File.exists?('/dev/null') ? '/dev/null' : 'NUL:' # Allow for Windows
|
19
|
-
|
20
|
-
Logger.should_receive(:new).with(STDERR).and_return(
|
19
|
+
logger = Logger.new(null)
|
20
|
+
Logger.should_receive(:new).with(STDERR).and_return(logger)
|
21
21
|
Authority.configure
|
22
22
|
Authority.configuration.logger
|
23
23
|
end
|
@@ -40,20 +40,20 @@ describe Authority::Configuration do
|
|
40
40
|
|
41
41
|
# This shouldn't be used during runtime, only during configuration
|
42
42
|
# It won't do anything outside of configuration anyway
|
43
|
-
it "
|
44
|
-
Authority.configuration.abilities[:eat].
|
43
|
+
it "allows adding to the default list of abilities" do
|
44
|
+
expect(Authority.configuration.abilities[:eat]).to eq('edible')
|
45
45
|
end
|
46
46
|
|
47
47
|
end
|
48
48
|
end
|
49
49
|
|
50
|
-
describe "helping those upgrading to 2.0" do
|
50
|
+
describe "helping those upgrading from versions prior to 2.0" do
|
51
51
|
|
52
52
|
before :all do
|
53
53
|
Authority.instance_variable_set :@configuration, nil
|
54
54
|
end
|
55
55
|
|
56
|
-
it "
|
56
|
+
it "raises a helpful exception if `config.default_strategy` is called" do
|
57
57
|
expect { Authority.configure { |config| config.default_strategy = Proc.new { false }}}.to raise_error(
|
58
58
|
ArgumentError, "`config.default_strategy=` was removed in Authority 2.0; see README and CHANGELOG"
|
59
59
|
)
|
@@ -1,147 +1,212 @@
|
|
1
1
|
require 'spec_helper'
|
2
|
-
require 'support/
|
3
|
-
require 'support/example_controllers'
|
2
|
+
require 'support/example_classes'
|
4
3
|
require 'support/mock_rails'
|
5
|
-
require 'support/user'
|
6
4
|
require 'active_support/core_ext/proc'
|
7
5
|
|
8
6
|
describe Authority::Controller do
|
9
7
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
# Here be dragons!
|
14
|
-
@fake_exception = Exception.new
|
15
|
-
@sample_controller = SampleController.new
|
16
|
-
# If a callback is passed to a controller's `rescue_from` method as the value for
|
17
|
-
# the `with` option (like `SomeController.rescue_from FooException, :with => some_callback`),
|
18
|
-
# Rails will use ActiveSupport's `Proc#bind` to ensure that when the proc refers to
|
19
|
-
# `self`, it will be the controller, not the proc itself.
|
20
|
-
# I need this callback's `self` to be the controller for the purposes of
|
21
|
-
# this test, so I'm stealing that behavior.
|
22
|
-
@callback = Authority::Controller.security_violation_callback.bind(@sample_controller)
|
23
|
-
|
24
|
-
Authority.configuration.security_violation_handler = :fire_ze_missiles
|
25
|
-
@sample_controller.should_receive(:fire_ze_missiles).with(@fake_exception)
|
26
|
-
@callback.call(@fake_exception)
|
27
|
-
end
|
8
|
+
class ExampleController
|
9
|
+
def self.rescue_from(*args) ; end
|
10
|
+
def self.before_filter(*args) ; end
|
28
11
|
end
|
29
12
|
|
30
|
-
|
13
|
+
# Get a fresh descendant class for each test, in case we've modified it
|
14
|
+
let(:controller_class) { Class.new(ExampleController) }
|
15
|
+
|
16
|
+
context "when including" do
|
31
17
|
|
32
18
|
before :each do
|
33
19
|
Authority::Controller.stub(:security_violation_callback).and_return(Proc.new {|exception| })
|
34
20
|
end
|
35
21
|
|
36
|
-
|
37
|
-
|
38
|
-
|
22
|
+
after :each do
|
23
|
+
controller_class.send(:include, Authority::Controller)
|
24
|
+
end
|
25
|
+
|
26
|
+
it "specifies rescuing security violations with a standard callback" do
|
27
|
+
controller_class.should_receive(:rescue_from).with(
|
28
|
+
Authority::SecurityViolation, :with => Authority::Controller.security_violation_callback
|
29
|
+
)
|
39
30
|
end
|
40
31
|
|
41
32
|
end
|
42
33
|
|
43
|
-
|
34
|
+
context "after including" do
|
35
|
+
|
36
|
+
let(:controller_class) do
|
37
|
+
Class.new(ExampleController).tap do |c|
|
38
|
+
c.send(:include, Authority::Controller)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
let(:resource_class) { ExampleResource }
|
43
|
+
|
44
|
+
describe "the security violation callback" do
|
45
|
+
|
46
|
+
it "calls whatever method on the controller that the configuration specifies" do
|
47
|
+
# Here be dragons!
|
48
|
+
fake_exception = Exception.new
|
49
|
+
controller_instance = controller_class.new
|
50
|
+
# If a callback is passed to a controller's `rescue_from` method as the value for
|
51
|
+
# the `with` option (like `SomeController.rescue_from FooException, :with => some_callback`),
|
52
|
+
# Rails will use ActiveSupport's `Proc#bind` to ensure that when the proc refers to
|
53
|
+
# `self`, it will be the controller, not the proc itself.
|
54
|
+
# I need this callback's `self` to be the controller for the purposes of
|
55
|
+
# this test, so I'm stealing that behavior.
|
56
|
+
callback = Authority::Controller.security_violation_callback.bind(controller_instance)
|
57
|
+
|
58
|
+
Authority.configuration.security_violation_handler = :fire_ze_missiles
|
59
|
+
controller_instance.should_receive(:fire_ze_missiles).with(fake_exception)
|
60
|
+
callback.call(fake_exception)
|
61
|
+
end
|
62
|
+
end
|
44
63
|
|
45
64
|
describe "the authority controller action map" do
|
46
65
|
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
66
|
+
before(:each) { controller_class.instance_variable_set(:@authority_action_map, nil) }
|
67
|
+
|
68
|
+
it "is created on demand" do
|
69
|
+
expect(controller_class.authority_action_map).to be_a(Hash)
|
51
70
|
end
|
52
71
|
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
72
|
+
it "is created as a copy of the configured controller action map" do
|
73
|
+
expect(controller_class.authority_action_map).to eq(Authority.configuration.controller_action_map)
|
74
|
+
expect(controller_class.authority_action_map).not_to be(Authority.configuration.controller_action_map)
|
75
|
+
end
|
76
|
+
|
77
|
+
it "is unique per controller" do
|
78
|
+
child_controller = Class.new(controller_class)
|
79
|
+
expect(child_controller.authority_action_map).not_to be(
|
80
|
+
controller_class.authority_action_map
|
81
|
+
)
|
58
82
|
end
|
59
83
|
|
60
84
|
end
|
61
85
|
|
62
|
-
describe "
|
63
|
-
it "should allow specifying the model to protect" do
|
64
|
-
ExampleController.authorize_actions_for ExampleModel
|
65
|
-
ExampleController.authority_resource.should eq(ExampleModel)
|
66
|
-
end
|
86
|
+
describe "class methods" do
|
67
87
|
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
88
|
+
describe "authorize_actions_for" do
|
89
|
+
|
90
|
+
it "allows specifying the model to protect" do
|
91
|
+
controller_class.authorize_actions_for(resource_class)
|
92
|
+
expect(controller_class.authority_resource).to eq(resource_class)
|
93
|
+
end
|
94
|
+
|
95
|
+
it "sets up a before_filter, passing the options it was given" do
|
96
|
+
filter_options = {:only => [:show, :edit, :update]}
|
97
|
+
controller_class.should_receive(:before_filter).with(:run_authorization_check, filter_options)
|
98
|
+
controller_class.authorize_actions_for(resource_class, filter_options)
|
99
|
+
end
|
100
|
+
|
101
|
+
it "passes the action hash to the `authority_action` method" do
|
102
|
+
child_controller = Class.new(controller_class)
|
103
|
+
new_actions = {:synthesize => :create, :annihilate => 'delete'}
|
104
|
+
child_controller.should_receive(:authority_actions).with(new_actions)
|
105
|
+
child_controller.authorize_actions_for(resource_class, :actions => new_actions)
|
106
|
+
end
|
73
107
|
|
74
|
-
it "should allow specifying the authority action map in the `authorize_actions_for` declaration" do
|
75
|
-
ExampleController.authorize_actions_for ExampleModel, :actions => {:eat => 'delete'}
|
76
|
-
ExampleController.authority_action_map[:eat].should eq('delete')
|
77
108
|
end
|
78
109
|
|
79
|
-
|
80
|
-
|
81
|
-
|
110
|
+
describe "authority_action" do
|
111
|
+
|
112
|
+
it "modifies this controller's authority action map" do
|
113
|
+
new_actions = {:show => :display, :synthesize => :create, :annihilate => 'delete'}
|
114
|
+
controller_class.authority_actions(new_actions)
|
115
|
+
expect(controller_class.authority_action_map).to eq(
|
116
|
+
Authority.configuration.controller_action_map.merge(new_actions)
|
117
|
+
)
|
118
|
+
end
|
119
|
+
|
120
|
+
it "does not modify any other controller" do
|
121
|
+
child_controller = Class.new(controller_class)
|
122
|
+
child_controller.authority_actions(:smite => 'delete')
|
123
|
+
expect(controller_class.authority_action_map[:smite]).to eq(nil)
|
124
|
+
end
|
125
|
+
|
82
126
|
end
|
127
|
+
|
83
128
|
end
|
84
129
|
|
85
130
|
describe "instance methods" do
|
86
|
-
before :each do
|
87
|
-
@user = User.new
|
88
|
-
@controller = ExampleController.new
|
89
|
-
@controller.stub!(:action_name).and_return(:edit)
|
90
|
-
@controller.stub!(Authority.configuration.user_method).and_return(@user)
|
91
|
-
end
|
92
131
|
|
93
|
-
|
94
|
-
|
95
|
-
|
132
|
+
let(:controller_class) do
|
133
|
+
Class.new(ExampleController).tap do |c|
|
134
|
+
c.send(:include, Authority::Controller)
|
135
|
+
c.authorize_actions_for(resource_class)
|
136
|
+
end
|
96
137
|
end
|
97
138
|
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
139
|
+
let(:controller_instance) do
|
140
|
+
controller_class.new.tap do |cc|
|
141
|
+
cc.stub(Authority.configuration.user_method).and_return(user)
|
142
|
+
end
|
102
143
|
end
|
103
144
|
|
104
|
-
|
105
|
-
|
106
|
-
|
145
|
+
let(:user) { ExampleUser.new }
|
146
|
+
|
147
|
+
describe "run_authorization_check (used as a before_filter)" do
|
148
|
+
|
149
|
+
it "checks authorization on the model specified" do
|
150
|
+
controller_instance.should_receive(:authorize_action_for).with(resource_class)
|
151
|
+
controller_instance.send(:run_authorization_check)
|
152
|
+
end
|
153
|
+
|
154
|
+
it "raises a MissingAction if there is no corresponding action for the controller" do
|
155
|
+
controller_instance.stub(:action_name).and_return('sculpt')
|
156
|
+
expect { controller_instance.send(:run_authorization_check) }.to raise_error(
|
157
|
+
Authority::Controller::MissingAction
|
158
|
+
)
|
159
|
+
end
|
107
160
|
|
108
|
-
it "should raise a MissingAction if there is no corresponding action for the controller" do
|
109
|
-
@controller.stub(:action_name).and_return('sculpt')
|
110
|
-
expect { @controller.send(:run_authorization_check) }.to raise_error(Authority::Controller::MissingAction)
|
111
161
|
end
|
112
162
|
|
113
|
-
|
114
|
-
|
115
|
-
|
163
|
+
describe "authorize_action_for" do
|
164
|
+
|
165
|
+
before(:each) { controller_instance.stub(:action_name).and_return(:destroy) }
|
166
|
+
|
167
|
+
it "calls Authority.enforce to authorize the action" do
|
168
|
+
Authority.should_receive(:enforce)
|
169
|
+
controller_instance.send(:authorize_action_for, resource_class)
|
170
|
+
end
|
171
|
+
|
172
|
+
it "passes along any options it was given" do
|
173
|
+
options = {:for => 'insolence'}
|
174
|
+
Authority.should_receive(:enforce).with('delete', resource_class, user, options)
|
175
|
+
controller_instance.send(:authorize_action_for, resource_class, options)
|
176
|
+
end
|
177
|
+
|
116
178
|
end
|
117
179
|
|
118
|
-
describe "
|
119
|
-
|
120
|
-
|
121
|
-
|
180
|
+
describe "authority_user" do
|
181
|
+
|
182
|
+
it "gets the user for the current request from the configured user_method" do
|
183
|
+
controller_instance.should_receive(Authority.configuration.user_method)
|
184
|
+
controller_instance.send(:authority_user)
|
122
185
|
end
|
186
|
+
|
123
187
|
end
|
124
188
|
|
125
189
|
describe "authority_forbidden action" do
|
126
190
|
|
127
|
-
|
128
|
-
@mock_error = mock(:message => 'oh noes! an error!')
|
129
|
-
end
|
191
|
+
let(:mock_error) { mock(:message => 'oh noes! an error!') }
|
130
192
|
|
131
|
-
it "
|
193
|
+
it "logs an error" do
|
132
194
|
Authority.configuration.logger.should_receive(:warn)
|
133
|
-
|
134
|
-
|
195
|
+
controller_instance.stub(:render)
|
196
|
+
controller_instance.send(:authority_forbidden, mock_error)
|
135
197
|
end
|
136
198
|
|
137
|
-
it "
|
199
|
+
it "renders the public/403.html file" do
|
138
200
|
forbidden_page = Rails.root.join('public/403.html')
|
139
201
|
Authority.configuration.logger.stub(:warn)
|
140
|
-
|
141
|
-
|
202
|
+
controller_instance.should_receive(:render).with(:file => forbidden_page, :status => 403, :layout => false)
|
203
|
+
controller_instance.send(:authority_forbidden, mock_error)
|
142
204
|
end
|
205
|
+
|
143
206
|
end
|
207
|
+
|
144
208
|
end
|
209
|
+
|
145
210
|
end
|
146
211
|
|
147
212
|
end
|