moat 0.0.8 → 0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.rubocop.yml +72 -0
- data/CONTRIBUTING.md +15 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +50 -0
- data/LICENSE +21 -0
- data/README.md +508 -0
- data/Rakefile +3 -10
- data/lib/moat.rb +100 -24
- data/lib/moat/rspec.rb +202 -0
- data/lib/moat/version.rb +3 -0
- data/moat.gemspec +18 -15
- data/spec/moat_spec.rb +296 -0
- data/spec/spec_helper.rb +1 -0
- metadata +77 -56
- data/History.txt +0 -47
- data/Manifest.txt +0 -8
- data/README.txt +0 -84
- data/bin/moat +0 -69
- data/moat-0.0.7.gem +0 -0
- data/test/test_moat.rb +0 -8
data/Rakefile
CHANGED
data/lib/moat.rb
CHANGED
@@ -1,41 +1,117 @@
|
|
1
1
|
module Moat
|
2
|
-
|
3
|
-
SITES = {}
|
2
|
+
POLICY_CLASS_SUFFIX = "Policy".freeze
|
4
3
|
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
4
|
+
class Error < StandardError; end
|
5
|
+
class PolicyNotAppliedError < Error; end
|
6
|
+
class NotFoundError < Error; end
|
7
|
+
class ActionNotFoundError < NotFoundError
|
8
|
+
attr_reader :action, :resource, :policy, :user
|
9
|
+
|
10
|
+
def initialize(options = {})
|
11
|
+
return super if options.is_a?(String)
|
12
|
+
@action = options[:action]
|
13
|
+
@resource = options[:resource]
|
14
|
+
@policy = options[:policy]
|
15
|
+
@user = options[:user]
|
16
|
+
|
17
|
+
message = options.fetch(:message) { "#{policy.name}##{action}" }
|
18
|
+
super(message)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
class PolicyNotFoundError < NotFoundError; end
|
23
|
+
class NotAuthorizedError < Error
|
24
|
+
attr_reader :action, :resource, :policy, :user
|
25
|
+
|
26
|
+
def initialize(options = {})
|
27
|
+
return super if options.is_a?(String)
|
28
|
+
@action = options[:action]
|
29
|
+
@resource = options[:resource]
|
30
|
+
@policy = options[:policy]
|
31
|
+
@user = options[:user]
|
32
|
+
|
33
|
+
message = options.fetch(:message) { "unauthorized #{policy.name}##{action} for #{resource}" }
|
34
|
+
super(message)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def policy_filter(scope, action = action_name, user: moat_user, policy: find_policy(scope))
|
39
|
+
apply_policy(scope, action, user: user, policy: policy::Filter)
|
40
|
+
end
|
41
|
+
|
42
|
+
def authorize(resource, action = "#{action_name}?", user: moat_user, policy: find_policy(resource))
|
43
|
+
if apply_policy(resource, action, user: user, policy: policy::Authorization)
|
44
|
+
resource
|
45
|
+
else
|
46
|
+
fail NotAuthorizedError, action: action, resource: resource, policy: policy, user: user
|
47
|
+
end
|
10
48
|
end
|
11
49
|
|
12
|
-
def
|
13
|
-
|
50
|
+
def moat_user
|
51
|
+
current_user
|
14
52
|
end
|
15
53
|
|
16
|
-
def
|
17
|
-
|
54
|
+
def verify_policy_applied
|
55
|
+
fail PolicyNotAppliedError unless @_moat_policy_applied
|
18
56
|
end
|
19
57
|
|
20
|
-
def
|
21
|
-
|
58
|
+
def skip_verify_policy_applied
|
59
|
+
@_moat_policy_applied = true
|
22
60
|
end
|
23
61
|
|
24
|
-
|
25
|
-
|
26
|
-
|
62
|
+
private
|
63
|
+
|
64
|
+
alias policy_applied skip_verify_policy_applied
|
65
|
+
|
66
|
+
def apply_policy(scope_or_resource, action, user:, policy:)
|
67
|
+
policy_instance = policy.new(user, scope_or_resource)
|
68
|
+
fail(ActionNotFoundError, action: action, policy: policy) unless policy_instance.respond_to?(action)
|
69
|
+
|
70
|
+
policy_applied
|
71
|
+
policy_instance.public_send(action)
|
72
|
+
end
|
73
|
+
|
74
|
+
def find_policy(object)
|
75
|
+
policy = if object.nil?
|
76
|
+
nil
|
77
|
+
elsif object.respond_to?(:policy_class)
|
78
|
+
object.policy_class
|
79
|
+
elsif object.class.respond_to?(:policy_class)
|
80
|
+
object.class.policy_class
|
27
81
|
else
|
28
|
-
|
82
|
+
infer_policy(object)
|
83
|
+
end
|
84
|
+
|
85
|
+
policy || fail(PolicyNotFoundError)
|
86
|
+
end
|
87
|
+
|
88
|
+
# Infer the policy from the object's type. If it is not found from the
|
89
|
+
# object's class directly, go up the ancestor chain.
|
90
|
+
def infer_policy(object)
|
91
|
+
initial_policy_inference_class = policy_inference_class(object)
|
92
|
+
policy_inference_class = initial_policy_inference_class
|
93
|
+
while policy_inference_class
|
94
|
+
policy = load_policy_from_class(policy_inference_class)
|
95
|
+
return policy if policy
|
96
|
+
policy_inference_class = policy_inference_class.superclass
|
29
97
|
end
|
98
|
+
|
99
|
+
fail PolicyNotFoundError, initial_policy_inference_class.name
|
30
100
|
end
|
31
101
|
|
32
|
-
def
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
alphanumeric_array = lowercase_letters + uppercase_letters + numbers
|
37
|
-
alphanumeric_array.shuffle[0..length].join
|
102
|
+
def load_policy_from_class(klass)
|
103
|
+
Object.const_get("#{policy_inference_class(klass).name}#{POLICY_CLASS_SUFFIX}")
|
104
|
+
rescue NameError
|
105
|
+
nil
|
38
106
|
end
|
39
107
|
|
108
|
+
def policy_inference_class(object)
|
109
|
+
if object.respond_to?(:model) # For ActiveRecord::Relation
|
110
|
+
object.model
|
111
|
+
elsif object.is_a?(Class)
|
112
|
+
object
|
113
|
+
else
|
114
|
+
object.class
|
115
|
+
end
|
116
|
+
end
|
40
117
|
end
|
41
|
-
|
data/lib/moat/rspec.rb
ADDED
@@ -0,0 +1,202 @@
|
|
1
|
+
module Moat
|
2
|
+
module RSpec
|
3
|
+
module PolicyMatchers
|
4
|
+
extend ::RSpec::Matchers::DSL
|
5
|
+
|
6
|
+
matcher :permit_all_authorizations do
|
7
|
+
match do |policy_class|
|
8
|
+
@incorrectly_denied = policy_authorizations - permitted_authorizations(policy_class)
|
9
|
+
@incorrectly_denied.empty?
|
10
|
+
end
|
11
|
+
failure_message do
|
12
|
+
generate_failure_message(incorrectly_denied: @incorrectly_denied)
|
13
|
+
end
|
14
|
+
|
15
|
+
match_when_negated do
|
16
|
+
false
|
17
|
+
end
|
18
|
+
failure_message_when_negated do
|
19
|
+
"Cannot negate `permit_all_authorizations`: Use `only_permit_authorizations` instead"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
matcher :deny_all_authorizations do
|
24
|
+
match do |policy_class|
|
25
|
+
@incorrectly_permitted = permitted_authorizations(policy_class)
|
26
|
+
@incorrectly_permitted.empty?
|
27
|
+
end
|
28
|
+
failure_message do
|
29
|
+
generate_failure_message(incorrectly_permitted: @incorrectly_permitted)
|
30
|
+
end
|
31
|
+
|
32
|
+
match_when_negated do
|
33
|
+
false
|
34
|
+
end
|
35
|
+
failure_message_when_negated do
|
36
|
+
"Cannot negate `deny_all_authorizations`: Use `only_permit_authorizations` instead"
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
matcher :only_permit_authorizations do |*authorizations_to_permit|
|
41
|
+
match do |policy_class|
|
42
|
+
permitted_authorizations = permitted_authorizations(policy_class)
|
43
|
+
@incorrectly_permitted = permitted_authorizations - authorizations_to_permit
|
44
|
+
@incorrectly_denied = authorizations_to_permit - permitted_authorizations
|
45
|
+
@incorrectly_permitted.empty? && @incorrectly_denied.empty?
|
46
|
+
end
|
47
|
+
failure_message do
|
48
|
+
generate_failure_message(
|
49
|
+
incorrectly_permitted: @incorrectly_permitted,
|
50
|
+
incorrectly_denied: @incorrectly_denied
|
51
|
+
)
|
52
|
+
end
|
53
|
+
|
54
|
+
match_when_negated do
|
55
|
+
false
|
56
|
+
end
|
57
|
+
failure_message_when_negated do
|
58
|
+
"Cannot negate `only_permit_authorizations`: Specify all permitted authorizations instead"
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
matcher :permit_through_all_filters do
|
63
|
+
match do |policy_class|
|
64
|
+
@incorrectly_denied = policy_filters - permitted_through_filters(policy_class)
|
65
|
+
@incorrectly_denied.empty?
|
66
|
+
end
|
67
|
+
failure_message do
|
68
|
+
generate_failure_message(incorrectly_denied: @incorrectly_denied)
|
69
|
+
end
|
70
|
+
|
71
|
+
match_when_negated do
|
72
|
+
false
|
73
|
+
end
|
74
|
+
failure_message_when_negated do
|
75
|
+
"Cannot negate `permit_through_all_filters`: Use `only_permit_through_filters` instead"
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
matcher :deny_through_all_filters do
|
80
|
+
match do |policy_class|
|
81
|
+
@incorrectly_permitted = permitted_through_filters(policy_class)
|
82
|
+
@incorrectly_permitted.empty?
|
83
|
+
end
|
84
|
+
failure_message do
|
85
|
+
generate_failure_message(incorrectly_permitted: @incorrectly_permitted)
|
86
|
+
end
|
87
|
+
|
88
|
+
match_when_negated do
|
89
|
+
false
|
90
|
+
end
|
91
|
+
failure_message_when_negated do
|
92
|
+
"Cannot negate `deny_through_all_filters`: Use `only_permit_through_filters` instead"
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
matcher :only_permit_through_filters do |*filter_whitelist|
|
97
|
+
match do |policy_class|
|
98
|
+
permitted_through_filters = permitted_through_filters(policy_class)
|
99
|
+
@incorrectly_permitted = permitted_through_filters - filter_whitelist
|
100
|
+
@incorrectly_denied = filter_whitelist - permitted_through_filters
|
101
|
+
@incorrectly_permitted.empty? && @incorrectly_denied.empty?
|
102
|
+
end
|
103
|
+
failure_message do
|
104
|
+
generate_failure_message(
|
105
|
+
incorrectly_denied: @incorrectly_denied,
|
106
|
+
incorrectly_permitted: @incorrectly_permitted
|
107
|
+
)
|
108
|
+
end
|
109
|
+
|
110
|
+
match_when_negated do
|
111
|
+
false
|
112
|
+
end
|
113
|
+
failure_message_when_negated do
|
114
|
+
"Cannot negate `only_permit_through_filters`: Specify permitted filters instead"
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
def generate_failure_message(incorrectly_permitted: [], incorrectly_denied: [])
|
119
|
+
failure_descriptions = []
|
120
|
+
unless incorrectly_permitted.empty?
|
121
|
+
failure_descriptions << "Incorrectly permitted to #{role}: #{incorrectly_permitted.to_sentence}"
|
122
|
+
end
|
123
|
+
unless incorrectly_denied.empty?
|
124
|
+
failure_descriptions << "Incorrectly denied to #{role}: #{incorrectly_denied.to_sentence}"
|
125
|
+
end
|
126
|
+
failure_descriptions.join("\n")
|
127
|
+
end
|
128
|
+
|
129
|
+
def role
|
130
|
+
::RSpec.current_example.metadata.fetch(:role)
|
131
|
+
end
|
132
|
+
|
133
|
+
def permitted_authorizations(policy_class)
|
134
|
+
policy_instance = policy_class::Authorization.new(public_send(role), policy_example_resource)
|
135
|
+
policy_authorizations.select do |authorization|
|
136
|
+
policy_instance.public_send(authorization)
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
def permitted_through_filters(policy_class)
|
141
|
+
policy_instance = policy_class::Filter.new(public_send(role), policy_example_resource.class.all)
|
142
|
+
policy_filters.select do |filter|
|
143
|
+
policy_instance.public_send(filter).include?(policy_example_resource)
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
module PolicyExampleGroup
|
149
|
+
include Moat::RSpec::PolicyMatchers
|
150
|
+
|
151
|
+
def self.included(base_class)
|
152
|
+
base_class.metadata[:type] = :policy
|
153
|
+
|
154
|
+
class << base_class
|
155
|
+
def roles(*roles, &block)
|
156
|
+
roles.each do |role|
|
157
|
+
describe(role.to_s, role: role, caller: caller) { instance_eval(&block) }
|
158
|
+
end
|
159
|
+
end
|
160
|
+
alias_method :role, :roles
|
161
|
+
|
162
|
+
def resource(&block)
|
163
|
+
fail ArgumentError, "#resource called without a block" if block.nil?
|
164
|
+
let(:policy_example_resource) { instance_eval(&block) }
|
165
|
+
end
|
166
|
+
|
167
|
+
def policy_filters(*filters)
|
168
|
+
let(:policy_filters) { filters }
|
169
|
+
end
|
170
|
+
|
171
|
+
def policy_authorizations(*authorizations)
|
172
|
+
let(:policy_authorizations) { authorizations }
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
base_class.class_eval do
|
177
|
+
subject { described_class }
|
178
|
+
|
179
|
+
let(:policy_authorizations) do
|
180
|
+
fail NotImplementedError, "List of policy_authorizations undefined"
|
181
|
+
end
|
182
|
+
|
183
|
+
let(:policy_filters) do
|
184
|
+
fail NotImplementedError, "List of policy_filters undefined"
|
185
|
+
end
|
186
|
+
|
187
|
+
let(:policy_example_resource) do
|
188
|
+
fail NotImplementedError, "A resource has not been defined"
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
RSpec.configure do |config|
|
197
|
+
config.include(
|
198
|
+
Moat::RSpec::PolicyExampleGroup,
|
199
|
+
type: :policy,
|
200
|
+
file_path: %r{spec/policies}
|
201
|
+
)
|
202
|
+
end
|
data/lib/moat/version.rb
ADDED
data/moat.gemspec
CHANGED
@@ -1,17 +1,20 @@
|
|
1
|
-
require
|
1
|
+
require File.expand_path("lib/moat/version", __dir__)
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
3
|
+
Gem::Specification.new do |gem|
|
4
|
+
gem.name = "moat"
|
5
|
+
gem.version = Moat::VERSION
|
6
|
+
gem.license = "MIT"
|
7
|
+
gem.authors = ["Poll Everywhere"]
|
8
|
+
gem.email = ["geeks@polleverywhere.com"]
|
9
|
+
gem.homepage = "https://github.com/polleverywhere/moat"
|
10
|
+
gem.summary = "A small authorization library"
|
11
|
+
gem.description = "Moat is an small authorization library built for Ruby (primarily Rails) web applications"
|
12
|
+
gem.files = `git ls-files`.split("\n")
|
13
|
+
gem.test_files = `git ls-files -- spec/*`.split("\n")
|
14
|
+
gem.require_paths = ["lib"]
|
15
|
+
gem.extra_rdoc_files = ["README.md"]
|
16
|
+
gem.rdoc_options = ["--main", "README.md"]
|
17
|
+
|
18
|
+
gem.add_development_dependency("rspec", "~> 3.5")
|
19
|
+
gem.add_development_dependency("rubocop", "~> 0.57.2")
|
17
20
|
end
|
data/spec/moat_spec.rb
ADDED
@@ -0,0 +1,296 @@
|
|
1
|
+
# require "spec_helper"
|
2
|
+
require_relative "../lib/moat"
|
3
|
+
|
4
|
+
# Will typically be a Rails controller
|
5
|
+
class MoatConsumerClass
|
6
|
+
include Moat
|
7
|
+
|
8
|
+
def current_user
|
9
|
+
@current_user ||= Object.new
|
10
|
+
end
|
11
|
+
|
12
|
+
def action_name
|
13
|
+
:read
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
class IntegerPolicy
|
18
|
+
class Filter
|
19
|
+
attr_reader :user, :scope
|
20
|
+
|
21
|
+
def initialize(user, scope)
|
22
|
+
@user = user
|
23
|
+
@scope = scope
|
24
|
+
end
|
25
|
+
|
26
|
+
def read
|
27
|
+
if user == "specified user"
|
28
|
+
scope
|
29
|
+
else
|
30
|
+
scope.select(&:even?)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def show
|
35
|
+
scope.select(&:odd?)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
class Authorization
|
40
|
+
attr_reader :resource, :user
|
41
|
+
|
42
|
+
def initialize(user, resource)
|
43
|
+
@user = user
|
44
|
+
@resource = resource
|
45
|
+
end
|
46
|
+
|
47
|
+
def read?
|
48
|
+
resource.even? || user == "specified user"
|
49
|
+
end
|
50
|
+
|
51
|
+
def show?
|
52
|
+
resource.odd?
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
class OtherIntegerPolicy
|
58
|
+
class Filter
|
59
|
+
attr_reader :user, :scope
|
60
|
+
|
61
|
+
def initialize(user, scope)
|
62
|
+
@user = user
|
63
|
+
@scope = scope
|
64
|
+
end
|
65
|
+
|
66
|
+
def read
|
67
|
+
scope.select(&:odd?)
|
68
|
+
end
|
69
|
+
|
70
|
+
def show
|
71
|
+
scope.select(&:even?)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
class Authorization
|
76
|
+
attr_reader :user, :resource
|
77
|
+
|
78
|
+
def initialize(user, resource)
|
79
|
+
@user = user
|
80
|
+
@resource = resource
|
81
|
+
end
|
82
|
+
|
83
|
+
def read?
|
84
|
+
resource.odd?
|
85
|
+
end
|
86
|
+
|
87
|
+
def show?
|
88
|
+
resource.even?
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
describe Moat do
|
94
|
+
let(:moat_consumer) { MoatConsumerClass.new }
|
95
|
+
|
96
|
+
describe "#moat_user" do
|
97
|
+
it "returns the value of #current_user" do
|
98
|
+
expect(moat_consumer.moat_user).to eql(moat_consumer.current_user)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
describe "#policy_filter" do
|
103
|
+
it "fails if scope is nil" do
|
104
|
+
expect { moat_consumer.policy_filter(nil) }.
|
105
|
+
to raise_error(Moat::PolicyNotFoundError)
|
106
|
+
end
|
107
|
+
|
108
|
+
it "fails if a corresponding policy can't be found" do
|
109
|
+
expect { moat_consumer.policy_filter(Hash) }.
|
110
|
+
to raise_error(Moat::PolicyNotFoundError, "Hash")
|
111
|
+
expect { moat_consumer.policy_filter({}) }.
|
112
|
+
to raise_error(Moat::PolicyNotFoundError, "Hash")
|
113
|
+
end
|
114
|
+
|
115
|
+
it "fails if a corresponding action can't be found" do
|
116
|
+
expect { moat_consumer.policy_filter([1, 2, 3], :invalid_action, policy: IntegerPolicy) }.
|
117
|
+
to raise_error(Moat::ActionNotFoundError, "IntegerPolicy::Filter#invalid_action")
|
118
|
+
end
|
119
|
+
|
120
|
+
it "returns the value of applying a policy scope filter to the original scope" do
|
121
|
+
expect(moat_consumer.policy_filter([1, 2, 3, 4, 5], policy: IntegerPolicy)).to eql([2, 4])
|
122
|
+
end
|
123
|
+
|
124
|
+
it "uses specified action" do
|
125
|
+
expect(moat_consumer.policy_filter([2, 3], :show, policy: IntegerPolicy)).to eql([3])
|
126
|
+
end
|
127
|
+
|
128
|
+
it "uses specified policy" do
|
129
|
+
expect(moat_consumer.policy_filter([2, 3], policy: OtherIntegerPolicy)).
|
130
|
+
to eql([3])
|
131
|
+
end
|
132
|
+
|
133
|
+
it "uses specified user" do
|
134
|
+
expect(moat_consumer.policy_filter([2, 3], user: "specified user", policy: IntegerPolicy)).to eql([2, 3])
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
describe "#authorize" do
|
139
|
+
it "fails if resource is nil" do
|
140
|
+
expect { moat_consumer.authorize(nil) }.
|
141
|
+
to raise_error(Moat::PolicyNotFoundError)
|
142
|
+
end
|
143
|
+
|
144
|
+
it "fails if a corresponding policy can't be found" do
|
145
|
+
expect { moat_consumer.authorize(Hash) }.
|
146
|
+
to raise_error(Moat::PolicyNotFoundError, "Hash")
|
147
|
+
expect { moat_consumer.authorize({}) }.
|
148
|
+
to raise_error(Moat::PolicyNotFoundError, "Hash")
|
149
|
+
end
|
150
|
+
|
151
|
+
it "fails if a corresponding action can't be found" do
|
152
|
+
expect { moat_consumer.authorize([1, 2, 3], :invalid_action?, policy: IntegerPolicy) }.
|
153
|
+
to raise_error(Moat::ActionNotFoundError, "IntegerPolicy::Authorization#invalid_action?")
|
154
|
+
end
|
155
|
+
|
156
|
+
it "fails when the value of calling the policy method is false" do
|
157
|
+
expect { moat_consumer.authorize(3) }.
|
158
|
+
to raise_error(Moat::NotAuthorizedError, "unauthorized IntegerPolicy#read? for 3")
|
159
|
+
end
|
160
|
+
|
161
|
+
it "returns the initial resource value when the value of calling the policy method is true" do
|
162
|
+
expect(moat_consumer.authorize(4)).to eql(4)
|
163
|
+
end
|
164
|
+
|
165
|
+
it "uses specified action" do
|
166
|
+
expect(moat_consumer.authorize(3, :show?)).to eql(3)
|
167
|
+
end
|
168
|
+
|
169
|
+
it "uses specified policy" do
|
170
|
+
expect(moat_consumer.authorize(3, policy: OtherIntegerPolicy)).to eql(3)
|
171
|
+
end
|
172
|
+
|
173
|
+
it "uses specified user" do
|
174
|
+
expect(moat_consumer.authorize(3, user: "specified user")).to eql(3)
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
describe "#verify_policy_applied" do
|
179
|
+
context "authorize called" do
|
180
|
+
it "does not raise an exception" do
|
181
|
+
moat_consumer.authorize(4)
|
182
|
+
expect { moat_consumer.verify_policy_applied }.not_to raise_error
|
183
|
+
end
|
184
|
+
end
|
185
|
+
context "policy_filter called" do
|
186
|
+
it "does not raise an exception" do
|
187
|
+
moat_consumer.policy_filter([1, 2], policy: IntegerPolicy)
|
188
|
+
expect { moat_consumer.verify_policy_applied }.not_to raise_error
|
189
|
+
end
|
190
|
+
end
|
191
|
+
context "neither authorize nor policy_filter called" do
|
192
|
+
it "raises an exception" do
|
193
|
+
expect { moat_consumer.verify_policy_applied }.
|
194
|
+
to raise_error(Moat::PolicyNotAppliedError)
|
195
|
+
end
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
describe "#skip_verify_policy_applied" do
|
200
|
+
it "does not raise an exception when the policy was not applied" do
|
201
|
+
moat_consumer.skip_verify_policy_applied
|
202
|
+
expect { moat_consumer.verify_policy_applied }.not_to raise_error
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
describe "policy lookup" do
|
207
|
+
class FakePolicy
|
208
|
+
class Filter
|
209
|
+
attr_reader :scope
|
210
|
+
|
211
|
+
def initialize(_user, scope)
|
212
|
+
@scope = scope
|
213
|
+
end
|
214
|
+
|
215
|
+
def read
|
216
|
+
scope.to_a
|
217
|
+
end
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
it "allows an object to specify a policy class" do
|
222
|
+
class DefinesPolicyClass
|
223
|
+
def self.to_a
|
224
|
+
[1, 2]
|
225
|
+
end
|
226
|
+
|
227
|
+
def self.policy_class
|
228
|
+
FakePolicy
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|
232
|
+
expect(moat_consumer.policy_filter(DefinesPolicyClass)).to eql([1, 2])
|
233
|
+
end
|
234
|
+
|
235
|
+
it "allows an object's class to specify a policy class" do
|
236
|
+
class DefinesPolicyClass
|
237
|
+
def self.policy_class
|
238
|
+
FakePolicy
|
239
|
+
end
|
240
|
+
|
241
|
+
def to_a
|
242
|
+
[3, 4]
|
243
|
+
end
|
244
|
+
end
|
245
|
+
|
246
|
+
expect(moat_consumer.policy_filter(DefinesPolicyClass.new)).to eql([3, 4])
|
247
|
+
end
|
248
|
+
|
249
|
+
it "infers a policy if object is a class" do
|
250
|
+
class Fake
|
251
|
+
def self.to_a
|
252
|
+
[5, 6]
|
253
|
+
end
|
254
|
+
end
|
255
|
+
|
256
|
+
expect(moat_consumer.policy_filter(Fake)).to eql([5, 6])
|
257
|
+
end
|
258
|
+
|
259
|
+
it "infers a policy from an object's ancestor" do
|
260
|
+
class Fake
|
261
|
+
def self.to_a
|
262
|
+
[7, 8]
|
263
|
+
end
|
264
|
+
end
|
265
|
+
class FakeChild < Fake
|
266
|
+
end
|
267
|
+
|
268
|
+
expect(moat_consumer.policy_filter(FakeChild)).to eql([7, 8])
|
269
|
+
end
|
270
|
+
|
271
|
+
it "infers a policy from an object's class's ancestor" do
|
272
|
+
class Fake
|
273
|
+
end
|
274
|
+
class FakeChild < Fake
|
275
|
+
def to_a
|
276
|
+
[9, 10]
|
277
|
+
end
|
278
|
+
end
|
279
|
+
|
280
|
+
expect(moat_consumer.policy_filter(FakeChild.new)).to eql([9, 10])
|
281
|
+
end
|
282
|
+
|
283
|
+
it "infers a policy from an object's `model` method" do
|
284
|
+
class DefinesModelName
|
285
|
+
def self.model
|
286
|
+
Fake
|
287
|
+
end
|
288
|
+
|
289
|
+
def self.to_a
|
290
|
+
[11, 12]
|
291
|
+
end
|
292
|
+
end
|
293
|
+
expect(moat_consumer.policy_filter(DefinesModelName)).to eql([11, 12])
|
294
|
+
end
|
295
|
+
end
|
296
|
+
end
|