authority 2.2.0 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/CHANGELOG.markdown +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
|