casbin-ruby 1.0.3 → 1.0.4

Sign up to get free protection for your applications and to get access to all the features.
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