casbin-ruby 1.0.3 → 1.0.4

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.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +242 -0
  3. data/lib/casbin-ruby.rb +11 -0
  4. data/lib/casbin-ruby/config/config.rb +115 -0
  5. data/lib/casbin-ruby/core_enforcer.rb +356 -0
  6. data/lib/casbin-ruby/effect/allow_and_deny_effector.rb +23 -0
  7. data/lib/casbin-ruby/effect/allow_override_effector.rb +23 -0
  8. data/lib/casbin-ruby/effect/default_effector.rb +37 -0
  9. data/lib/casbin-ruby/effect/deny_override_effector.rb +23 -0
  10. data/lib/casbin-ruby/effect/effector.rb +18 -0
  11. data/lib/casbin-ruby/effect/priority_effector.rb +25 -0
  12. data/lib/casbin-ruby/enforcer.rb +189 -0
  13. data/lib/casbin-ruby/internal_enforcer.rb +73 -0
  14. data/lib/casbin-ruby/management_enforcer.rb +297 -0
  15. data/lib/casbin-ruby/model/assertion.rb +33 -0
  16. data/lib/casbin-ruby/model/function_map.rb +30 -0
  17. data/lib/casbin-ruby/model/model.rb +80 -0
  18. data/lib/casbin-ruby/model/policy.rb +161 -0
  19. data/lib/casbin-ruby/persist/adapter.rb +39 -0
  20. data/lib/casbin-ruby/persist/adapters/file_adapter.rb +53 -0
  21. data/lib/casbin-ruby/persist/batch_adapter.rb +16 -0
  22. data/lib/casbin-ruby/persist/filtered_adapter.rb +17 -0
  23. data/lib/casbin-ruby/rbac/default_role_manager/role.rb +54 -0
  24. data/lib/casbin-ruby/rbac/default_role_manager/role_manager.rb +146 -0
  25. data/lib/casbin-ruby/rbac/role_manager.rb +22 -0
  26. data/lib/casbin-ruby/synced_enforcer.rb +39 -0
  27. data/lib/casbin-ruby/util.rb +80 -0
  28. data/lib/casbin-ruby/util/builtin_operators.rb +105 -0
  29. data/lib/casbin-ruby/util/evaluator.rb +27 -0
  30. data/lib/casbin-ruby/util/thread_lock.rb +19 -0
  31. data/lib/casbin-ruby/version.rb +5 -0
  32. data/spec/casbin/config/config_spec.rb +66 -0
  33. data/spec/casbin/core_enforcer_spec.rb +473 -0
  34. data/spec/casbin/enforcer_spec.rb +302 -0
  35. data/spec/casbin/model/function_map_spec.rb +28 -0
  36. data/spec/casbin/rbac/default_role_manager/role_manager_spec.rb +131 -0
  37. data/spec/casbin/rbac/default_role_manager/role_spec.rb +84 -0
  38. data/spec/casbin/util/builtin_operators_spec.rb +205 -0
  39. data/spec/casbin/util_spec.rb +98 -0
  40. data/spec/support/model_helper.rb +9 -0
  41. metadata +51 -3
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Casbin
4
+ VERSION = '1.0.4'
5
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'casbin-ruby/config/config'
4
+
5
+ describe Casbin::Config::Config do
6
+ let(:path) { File.expand_path('test.ini', __dir__) }
7
+ let(:config) { described_class.new_config(path) }
8
+
9
+ describe '#new_config_from_text' do
10
+ let(:config) { described_class.new_config_from_text(text) }
11
+ let(:text) do
12
+ text = nil
13
+ File.open(path, 'r:UTF-8') do |f|
14
+ text = f.readlines.join
15
+ end
16
+
17
+ text
18
+ end
19
+
20
+ it 'reads config from text' do
21
+ expect(config.get('debug')).to eq 'true'
22
+ expect(config.get('redis::redis.key')).to eq 'push1,push2'
23
+ expect(config.get('math::math.i64')).to eq '64'
24
+ expect(config.get('other::name')).to eq 'ATC自动化测试^-^&($#……#'
25
+ expect(config.get('multi1::name')).to eq 'r.sub==p.sub && r.obj==p.obj'
26
+ end
27
+ end
28
+
29
+ describe '#get' do
30
+ it 'default::key' do
31
+ expect(config.get('debug')).to eq 'true'
32
+ expect(config.get('url')).to eq 'act.wiki'
33
+ end
34
+
35
+ it 'redis::key' do
36
+ expect(config.get('redis::redis.key')).to eq 'push1,push2'
37
+ expect(config.get('mysql::mysql.dev.host')).to eq '127.0.0.1'
38
+ expect(config.get('mysql::mysql.master.host')).to eq '10.0.0.1'
39
+ end
40
+
41
+ it 'math::key test' do
42
+ expect(config.get('math::math.i64')).to eq '64'
43
+ expect(config.get('math::math.f64')).to eq '64.1'
44
+ end
45
+
46
+ it 'other::key test' do
47
+ expect(config.get('other::name')).to eq 'ATC自动化测试^-^&($#……#'
48
+ expect(config.get('other::key1')).to eq 'test key'
49
+ end
50
+
51
+ it 'multi line' do
52
+ expect(config.get('multi1::name')).to eq 'r.sub==p.sub && r.obj==p.obj'
53
+ expect(config.get('multi2::name')).to eq 'r.sub==p.sub && r.obj==p.obj'
54
+ expect(config.get('multi3::name')).to eq 'r.sub==p.sub && r.obj==p.obj'
55
+ expect(config.get('multi4::name')).to eq ''
56
+ expect(config.get('multi5::name')).to eq 'r.sub==p.sub && r.obj==p.obj'
57
+ end
58
+ end
59
+
60
+ describe '#set' do
61
+ it 'set other::key1' do
62
+ config.set('other::key1', 'new test key')
63
+ expect(config.get('other::key1')).to eq 'new test key'
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,473 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'casbin-ruby/core_enforcer'
4
+ require 'support/model_helper'
5
+
6
+ describe Casbin::CoreEnforcer do
7
+ let(:model) { Casbin::Model::Model.new }
8
+ let(:adapter) { Casbin::Persist::Adapter.new }
9
+ let(:enforcer) { described_class.new model, adapter }
10
+ let(:watcher) { instance_double 'watcher' }
11
+
12
+ describe '#initalize' do
13
+ shared_examples 'creates new enforcer' do
14
+ it { expect(enforcer).not_to be_nil }
15
+ end
16
+
17
+ context 'when model is a string (path)' do
18
+ let(:model) { model_config 'basic' }
19
+
20
+ context 'when adapter is a string (path)' do
21
+ let(:adapter) { policy_file 'basic' }
22
+
23
+ it_behaves_like 'creates new enforcer'
24
+ end
25
+
26
+ context 'when adapter is a special object' do
27
+ it_behaves_like 'creates new enforcer'
28
+ end
29
+ end
30
+
31
+ context 'when model is a special object' do
32
+ context 'when adapter is a string (path)' do
33
+ let(:adapter) { policy_file 'basic' }
34
+
35
+ it 'raises exception' do
36
+ expect { enforcer }.to raise_error StandardError, 'Invalid parameters for enforcer.'
37
+ end
38
+ end
39
+
40
+ context 'when adapter is a special object' do
41
+ let(:model) { model_config 'basic' }
42
+
43
+ it_behaves_like 'creates new enforcer'
44
+ end
45
+ end
46
+ end
47
+
48
+ describe '#enforce' do
49
+ subject { enforcer.enforce(*request) }
50
+
51
+ shared_examples 'correctly enforces rules' do |requests|
52
+ requests.each do |request_data, result|
53
+ context "with #{request_data.inspect}" do
54
+ let(:request) { request_data }
55
+
56
+ it { is_expected.to eq(result) }
57
+ end
58
+ end
59
+ end
60
+
61
+ context 'with basic' do
62
+ let(:model) { model_config 'basic' }
63
+ let(:adapter) { policy_file 'basic' }
64
+
65
+ requests = {
66
+ %w[alice data1 read] => true,
67
+ %w[bob data2 write] => true,
68
+ %w[alice data1 write] => false,
69
+ %w[bob data2 read] => false,
70
+
71
+ %w[admin2 data1 read] => false
72
+ }
73
+
74
+ it_behaves_like 'correctly enforces rules', requests
75
+ end
76
+
77
+ context 'with basic with root' do
78
+ let(:model) { model_config 'basic_with_root' }
79
+ let(:adapter) { policy_file 'basic' }
80
+
81
+ requests = {
82
+ %w[alice data1 read] => true,
83
+ %w[bob data2 write] => true,
84
+ %w[alice data1 write] => false,
85
+ %w[bob data2 read] => false,
86
+
87
+ %w[admin2 data1 read] => false,
88
+
89
+ %w[root data1 read] => true,
90
+ %w[root data1 write] => true,
91
+ %w[root data2 read] => true,
92
+ %w[root data2 write] => true
93
+ }
94
+
95
+ it_behaves_like 'correctly enforces rules', requests
96
+ end
97
+
98
+ context 'with basic without users' do
99
+ let(:model) { model_config 'basic_without_users' }
100
+ let(:adapter) { policy_file 'basic_without_users' }
101
+
102
+ requests = {
103
+ %w[data1 read] => true,
104
+ %w[data1 write] => false,
105
+ %w[data2 read] => false,
106
+ %w[data2 write] => true,
107
+ %w[data3 read] => false,
108
+ %w[data3 write] => false
109
+ }
110
+
111
+ it_behaves_like 'correctly enforces rules', requests
112
+ end
113
+
114
+ context 'with basic without resources' do
115
+ let(:model) { model_config 'basic_without_resources' }
116
+ let(:adapter) { policy_file 'basic_without_resources' }
117
+
118
+ requests = {
119
+ %w[alice read] => true,
120
+ %w[alice write] => false,
121
+ %w[bob read] => false,
122
+ %w[bob write] => true,
123
+ %w[charlie read] => false,
124
+ %w[charlie write] => false
125
+ }
126
+
127
+ it_behaves_like 'correctly enforces rules', requests
128
+ end
129
+
130
+ context 'with RBAC' do
131
+ let(:model) { model_config 'rbac' }
132
+ let(:adapter) { policy_file 'rbac' }
133
+
134
+ requests = {
135
+ %w[alice data1 read] => true,
136
+ %w[alice data1 write] => false,
137
+ %w[alice data2 read] => true,
138
+
139
+ %w[bob data1 read] => false,
140
+
141
+ %w[data2_admin data2 read] => true,
142
+ %w[data2_admin data1 read] => false
143
+ }
144
+
145
+ it_behaves_like 'correctly enforces rules', requests
146
+ end
147
+
148
+ context 'with RBAC with domains' do
149
+ let(:model) { model_config 'rbac_with_domains' }
150
+ let(:adapter) { policy_file 'rbac_with_domains' }
151
+
152
+ requests = {
153
+ %w[alice domain1 data1 read] => true,
154
+ %w[alice domain2 data1 read] => false,
155
+ %w[alice domain1 data2 read] => false,
156
+ %w[alice domain1 data1 delete] => false,
157
+
158
+ %w[bob domain1 data1 read] => false,
159
+ %w[bob domain2 data2 read] => true,
160
+ %w[bob domain1 data1 write] => false,
161
+ %w[bob domain2 data2 write] => true,
162
+
163
+ %w[admin domain1 data1 read] => true,
164
+ %w[admin domain2 data2 read] => true,
165
+ %w[admin domain1 data1 write] => true,
166
+ %w[admin domain2 data2 write] => true
167
+ }
168
+
169
+ it_behaves_like 'correctly enforces rules', requests
170
+ end
171
+
172
+ context 'with RBAC with resource roles' do
173
+ let(:model) { model_config 'rbac_with_resource_roles' }
174
+ let(:adapter) { policy_file 'rbac_with_resource_roles' }
175
+
176
+ requests = {
177
+ %w[alice data1 read] => true,
178
+ %w[alice data1 write] => true,
179
+ %w[alice data2 read] => false,
180
+ %w[alice data2 write] => true,
181
+ %w[alice data3 read] => false,
182
+ %w[alice data3 write] => false,
183
+
184
+ %w[bob data1 read] => false,
185
+ %w[bob data1 write] => false,
186
+ %w[bob data2 read] => false,
187
+ %w[bob data2 write] => true,
188
+ %w[bob data3 read] => false,
189
+ %w[bob data3 write] => false,
190
+
191
+ %w[data_group_admin data1 read] => false,
192
+ %w[data_group_admin data1 write] => true,
193
+ %w[data_group_admin data2 read] => false,
194
+ %w[data_group_admin data2 write] => true,
195
+ %w[data_group_admin data3 read] => false,
196
+ %w[data_group_admin data3 write] => false,
197
+
198
+ %w[diana data1 read] => false
199
+ }
200
+
201
+ it_behaves_like 'correctly enforces rules', requests
202
+ end
203
+
204
+ context 'with RBAC with pattern' do
205
+ let(:model) { model_config 'rbac_with_pattern' }
206
+ let(:adapter) { policy_file 'rbac_with_pattern' }
207
+
208
+ requests = {
209
+ %w[alice /book/1 GET] => true,
210
+ %w[alice /book/1 POST] => false,
211
+ %w[alice /other/1 GET] => false
212
+ }
213
+
214
+ before do
215
+ enforcer.add_named_matching_func('g2',
216
+ ->(key1, key2) { Casbin::Util::BuiltinOperators.key_match2 key1, key2 })
217
+ end
218
+
219
+ it_behaves_like 'correctly enforces rules', requests
220
+ end
221
+
222
+ # This does not implemented in Python version. Examples was taken from here:
223
+ # https://casbin.org/en/editor (select "RBAC with all pattern" option)
224
+ #
225
+ # We should add the separate matching function for domain.
226
+ # https://github.com/casbin/casbin/blob/0c7aac93d766aeddea324d7a16fd8be1c700bca5/enforcer.go#L661
227
+ xcontext 'with RBAC with all pattern' do
228
+ let(:model) { model_config 'rbac_with_all_pattern' }
229
+ let(:adapter) { policy_file 'rbac_with_all_pattern' }
230
+
231
+ requests = {
232
+ %w[/book/1 domain1 data1 read] => true,
233
+ %w[/book/1 domain2 data2 write] => true,
234
+
235
+ %w[/domain1/book/1 domain1 data1 read] => true,
236
+ %w[/domain1/book/1 domain2 data2 write] => false
237
+ }
238
+
239
+ before do
240
+ matching_func = ->(key1, key2) { Util::BuiltinOperators.key_match2 key1, key2 }
241
+ enforcer.role_manager.add_matching_func matching_func
242
+ # enforcer.role_manager.add_domain_matching_func matching_func
243
+ end
244
+
245
+ it_behaves_like 'correctly enforces rules', requests
246
+ end
247
+
248
+ context 'with ABAC' do
249
+ let(:model) { model_config 'abac' }
250
+
251
+ requests = {
252
+ ['alice', { 'Owner' => 'alice' }, 'read'] => true,
253
+ ['alice', { 'Owner' => 'alice' }, 'write'] => true,
254
+ ['alice', { 'Owner' => 'diana' }, 'read'] => false,
255
+ ['alice', { 'Owner' => 'diana' }, 'write'] => false
256
+ }
257
+
258
+ it_behaves_like 'correctly enforces rules', requests
259
+ end
260
+
261
+ context 'with ABAC with eval' do
262
+ let(:model) { model_config 'abac_with_eval' }
263
+ let(:adapter) { policy_file 'abac_with_eval' }
264
+
265
+ requests = {
266
+ [{ 'Age' => 12, 'Position' => { 'Rank' => 1 } }, '/data1', 'read'] => false,
267
+ [{ 'Age' => 22, 'Position' => { 'Rank' => 1 } }, '/data1', 'read'] => true,
268
+ [{ 'Age' => 22, 'Position' => { 'Rank' => 1 } }, '/data1', 'write'] => false,
269
+
270
+ [{ 'Age' => 22, 'Position' => { 'Rank' => 1 } }, '/data2', 'read'] => false,
271
+ [{ 'Age' => 22, 'Position' => { 'Rank' => 1 } }, '/data2', 'write'] => true,
272
+ [{ 'Age' => 62, 'Position' => { 'Rank' => 1 } }, '/data2', 'read'] => false,
273
+
274
+ [{ 'Age' => 22, 'Position' => { 'Rank' => 1 } }, '/data3', 'read'] => false,
275
+
276
+ [{ 'Age' => 22, 'Position' => { 'Rank' => 1 } }, '/special_data', 'read'] => false,
277
+ [{ 'Age' => 22, 'Position' => { 'Rank' => 2 } }, '/special_data', 'read'] => true
278
+ }
279
+
280
+ it_behaves_like 'correctly enforces rules', requests
281
+ end
282
+
283
+ context 'with REST' do
284
+ let(:model) { model_config 'rest' }
285
+ let(:adapter) { policy_file 'rest' }
286
+
287
+ requests = {
288
+ %w[alice /alice_data/item GET] => true,
289
+ %w[alice /alice_data/item POST] => false,
290
+ %w[alice /alice_data/resource1 GET] => true,
291
+ %w[alice /alice_data/resource1 POST] => true,
292
+ %w[alice /cathy_data/item PUT] => false,
293
+
294
+ %w[bob /alice_data/resource1 GET] => false,
295
+ %w[bob /alice_data/resource2 GET] => true,
296
+ %w[bob /alice_data/resource2 POST] => false,
297
+ %w[bob /bob_data/resource DELETE] => false,
298
+ %w[bob /bob_data/resource POST] => true,
299
+
300
+ %w[cathy /cathy_data GET] => true,
301
+ %w[cathy /cathy_data POST] => true,
302
+ %w[cathy /cathy_data DELETE] => false,
303
+ %w[cathy /cathy_data/resource GET] => false,
304
+ %w[cathy /alice_data/resource1 GET] => false
305
+ }
306
+
307
+ it_behaves_like 'correctly enforces rules', requests
308
+ end
309
+
310
+ context 'with REST (keyMatch2)' do
311
+ let(:model) { model_config 'rest2' }
312
+ let(:adapter) { policy_file 'rest2' }
313
+
314
+ requests = {
315
+ %w[alice /alice_data/hello GET] => true,
316
+ %w[alice /alice_data/other_hello GET] => true,
317
+ %w[alice /alice_data/hello POST] => false,
318
+ %w[bob /alice_data/hello GET] => false,
319
+ %w[alice /alice_data2/hello GET] => false,
320
+
321
+ %w[alice /alice_data2/1/using/some GET] => true,
322
+ %w[alice /alice_data2/1/using/some POST] => false,
323
+ %w[bob /alice_data2/1/using/some GET] => false,
324
+ %w[alice /alice_data2/1//some GET] => false,
325
+ %w[alice /alice_data2/1/some GET] => false
326
+ }
327
+
328
+ it_behaves_like 'correctly enforces rules', requests
329
+ end
330
+
331
+ context 'with deny-override' do
332
+ let(:model) { model_config 'deny_override' }
333
+ let(:adapter) { policy_file 'deny_override' }
334
+
335
+ requests = {
336
+ %w[alice data1 read] => true,
337
+ %w[alice data1 write] => true,
338
+ %w[alice data2 read] => true,
339
+ %w[alice data2 write] => false,
340
+ %w[alice data3 read] => true,
341
+ %w[alice data3 write] => true,
342
+
343
+ %w[bob data1 read] => true,
344
+ %w[bob data1 write] => true,
345
+ %w[bob data2 read] => true,
346
+ %w[bob data2 write] => true,
347
+ %w[bob data3 read] => true,
348
+ %w[bob data3 write] => true
349
+ }
350
+
351
+ it_behaves_like 'correctly enforces rules', requests
352
+ end
353
+
354
+ context 'with allow-and-deny' do
355
+ let(:model) { model_config 'allow_and_deny' }
356
+ let(:adapter) { policy_file 'allow_and_deny' }
357
+
358
+ requests = {
359
+ %w[alice data1 read] => true,
360
+ %w[alice data1 write] => false,
361
+ %w[alice data2 read] => true,
362
+ %w[alice data2 write] => false,
363
+ %w[alice data3 read] => false,
364
+ %w[alice data3 write] => false,
365
+
366
+ %w[bob data1 read] => false,
367
+ %w[bob data1 write] => false,
368
+ %w[bob data2 read] => false,
369
+ %w[bob data2 write] => true,
370
+ %w[bob data3 read] => false,
371
+ %w[bob data3 write] => false
372
+ }
373
+
374
+ it_behaves_like 'correctly enforces rules', requests
375
+ end
376
+
377
+ context 'with implicit priority' do
378
+ let(:model) { model_config 'priorities/implicit' }
379
+ let(:adapter) { policy_file 'priorities/implicit' }
380
+
381
+ requests = {
382
+ %w[admin data1 read] => true,
383
+ %w[admin data2 read] => false
384
+ }
385
+
386
+ it_behaves_like 'correctly enforces rules', requests
387
+ end
388
+
389
+ # This does not implemented in Python version. Examples was taken from here:
390
+ # https://casbin.org/docs/en/priority-model#load-policy-with-priority-explicitly
391
+ #
392
+ # Related PR in Golang version - https://github.com/casbin/casbin/pull/714/files
393
+ # (we should add sorting by `p_priority` after policy loading).
394
+ xcontext 'with explicit priority' do
395
+ let(:model) { model_config 'priorities/explicit' }
396
+ let(:adapter) { policy_file 'priorities/explicit' }
397
+
398
+ requests = {
399
+ %w[alice data1 write] => true,
400
+ %w[bob data2 read] => false,
401
+ %w[bob data2 write] => true
402
+ }
403
+
404
+ it_behaves_like 'correctly enforces rules', requests
405
+ end
406
+
407
+ context 'with IP matching' do
408
+ let(:model) { model_config 'ip' }
409
+ let(:adapter) { policy_file 'ip' }
410
+
411
+ requests = {
412
+ %w[192.168.2.1 data1 read] => true,
413
+ %w[192.168.2.101 data1 read] => true,
414
+ %w[192.168.1.1 data1 read] => false,
415
+ %w[192.168.2.101 data1 write] => false,
416
+ %w[192.168.2.1 data2 read] => false,
417
+
418
+ %w[10.0.2.3 data2 write] => true,
419
+ %w[10.0.5.5 data2 write] => true,
420
+ %w[10.0.5.5 data2 read] => false,
421
+ %w[10.1.5.5 data2 write] => false,
422
+ %w[10.0.5.5 data1 read] => false
423
+ }
424
+
425
+ it_behaves_like 'correctly enforces rules', requests
426
+ end
427
+
428
+ context 'with glob' do
429
+ let(:model) { model_config 'glob' }
430
+ let(:adapter) { policy_file 'glob' }
431
+
432
+ requests = {
433
+ %w[u1 /foo/1 read] => true,
434
+ %w[u1 /foo/1/2 read] => false,
435
+ %w[u1 /foobar read] => false,
436
+ %w[u1 /some/foo/1 read] => false,
437
+ %w[u1 other read] => false,
438
+ %w[u1 /foo/1 write] => false,
439
+
440
+ %w[u2 /foo/1 read] => false,
441
+ %w[u2 /foo/1/2 read] => false,
442
+ %w[u2 /foobar read] => true,
443
+ %w[u2 /some/foo/1 read] => false,
444
+ %w[u2 other read] => false,
445
+ %w[u2 /foo/1 write] => false,
446
+
447
+ %w[u3 /foo/1 read] => false,
448
+ %w[u3 /foo/1/2 read] => false,
449
+ %w[u3 /foobar read] => false,
450
+ %w[u3 /some/foo/1 read] => true,
451
+ %w[u3 other read] => false,
452
+ %w[u3 /foo/1 write] => false,
453
+
454
+ %w[u4 /foo/1 read] => false,
455
+ %w[u4 /foo/1/2 read] => false,
456
+ %w[u4 /foobar read] => false,
457
+ %w[u4 /some/foo/1 read] => false,
458
+ %w[u4 other read] => true,
459
+ %w[u4 /foo/1 write] => false
460
+
461
+ # It seems that `**` does not work properly (the behaviour is different from Golang version)
462
+ # %w[u5 /foo/1 read] => true,
463
+ # %w[u5 /foo/1/2 read] => true,
464
+ # %w[u5 /foobar read] => false,
465
+ # %w[u5 /some/foo/1 read] => false,
466
+ # %w[u5 other read] => false,
467
+ # %w[u5 /foo/1 write] => false
468
+ }
469
+
470
+ it_behaves_like 'correctly enforces rules', requests
471
+ end
472
+ end
473
+ end