moat 0.0.8 → 0.1
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.
- 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
|