Passed
Push — master ( 9b9c6c...ef704e )
by Daniel
35:52 queued 24:20
created

InheritedPermissionsTest   A

Complexity

Total Complexity 16

Size/Duplication

Total Lines 425
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
dl 0
loc 425
rs 10
c 0
b 0
f 0
wmc 16

12 Methods

Rating   Name   Duplication   Size   Complexity  
B testMobiusHierarchy() 0 34 1
B testRestrictedDraftUnrestrictedLive() 0 29 1
B testEditPermissions() 0 36 1
A tearDown() 0 6 1
B testPermissionsPersistCache() 0 40 1
B testUnrestrictedDraftOverridesLive() 0 27 1
B testPermissionsFlushCache() 0 54 5
B setUp() 0 26 1
B testDeletePermissions() 0 36 1
B testViewPermissions() 0 41 1
B testUnstagedViewPermissions() 0 41 1
A generateCacheKey() 0 7 1
1
<?php
2
3
namespace SilverStripe\Security\Tests;
4
5
use SilverStripe\Core\Injector\Injector;
6
use SilverStripe\Dev\SapphireTest;
7
use SilverStripe\Security\Group;
8
use SilverStripe\Security\InheritedPermissions;
9
use SilverStripe\Security\Member;
10
use SilverStripe\Security\PermissionChecker;
11
use SilverStripe\Security\Tests\InheritedPermissionsTest\TestPermissionNode;
12
use SilverStripe\Security\Tests\InheritedPermissionsTest\TestDefaultPermissionChecker;
13
use SilverStripe\Security\Tests\InheritedPermissionsTest\UnstagedNode;
14
use SilverStripe\Versioned\Versioned;
15
use Psr\SimpleCache\CacheInterface;
16
use ReflectionClass;
17
18
class InheritedPermissionsTest extends SapphireTest
19
{
20
    protected static $fixture_file = 'InheritedPermissionsTest.yml';
21
22
    protected static $extra_dataobjects = [
23
        TestPermissionNode::class,
24
        UnstagedNode::class,
25
    ];
26
27
    /**
28
     * @var TestDefaultPermissionChecker
29
     */
30
    protected $rootPermissions = null;
31
32
    protected function setUp()
33
    {
34
        $this->rootPermissions = new TestDefaultPermissionChecker();
35
36
        // Register root permissions
37
        $permission1 = InheritedPermissions::create(TestPermissionNode::class)
0 ignored issues
show
Bug introduced by
SilverStripe\Security\Te...stPermissionNode::class of type string is incompatible with the type array expected by parameter $args of SilverStripe\Security\In...edPermissions::create(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

37
        $permission1 = InheritedPermissions::create(/** @scrutinizer ignore-type */ TestPermissionNode::class)
Loading history...
38
            ->setGlobalEditPermissions(['TEST_NODE_ACCESS'])
39
            ->setDefaultPermissions($this->rootPermissions);
40
        Injector::inst()->registerService(
41
            $permission1,
42
            PermissionChecker::class . '.testpermissions'
43
        );
44
45
        // Reset root permission
46
        $permission2 = InheritedPermissions::create(UnstagedNode::class)
47
            ->setGlobalEditPermissions(['TEST_NODE_ACCESS'])
48
            ->setDefaultPermissions($this->rootPermissions);
49
        Injector::inst()->registerService(
50
            $permission2,
51
            PermissionChecker::class . '.unstagedpermissions'
52
        );
53
54
        parent::setUp();
55
56
        $permission1->clearCache();
57
        $permission2->clearCache();
58
    }
59
60
    protected function tearDown()
61
    {
62
        Injector::inst()->unregisterNamedObject(PermissionChecker::class . '.testpermissions');
63
        Injector::inst()->unregisterNamedObject(PermissionChecker::class . '.unstagedpermissions');
64
        $this->rootPermissions = null;
65
        parent::tearDown();
66
    }
67
68
    public function testEditPermissions()
69
    {
70
        $editor = $this->objFromFixture(Member::class, 'editor');
71
72
        $about = $this->objFromFixture(TestPermissionNode::class, 'about');
73
        $aboutStaff = $this->objFromFixture(TestPermissionNode::class, 'about-staff');
74
        $history = $this->objFromFixture(TestPermissionNode::class, 'history');
75
        $products = $this->objFromFixture(TestPermissionNode::class, 'products');
76
        $product1 = $this->objFromFixture(TestPermissionNode::class, 'products-product1');
77
        $product4 = $this->objFromFixture(TestPermissionNode::class, 'products-product4');
78
79
        // Test logged out users cannot edit
80
        Member::actAs(null, function () use ($aboutStaff) {
81
            $this->assertFalse($aboutStaff->canEdit());
82
        });
83
84
        // Can't edit a page that is locked to admins
85
        $this->assertFalse($about->canEdit($editor));
86
87
        // Can edit a page that is locked to editors
88
        $this->assertTrue($products->canEdit($editor));
89
90
        // Can edit a child of that page that inherits
91
        $this->assertTrue($product1->canEdit($editor));
92
93
        // Can't edit a child of that page that has its permissions overridden
94
        $this->assertFalse($product4->canEdit($editor));
95
96
        // Test that root node respects root permissions
97
        $this->assertTrue($history->canEdit($editor));
98
99
        TestPermissionNode::getInheritedPermissions()->clearCache();
100
        $this->rootPermissions->setCanEdit(false);
101
102
        // With root edit false, permissions are now denied for CanEditType = Inherit
103
        $this->assertFalse($history->canEdit($editor));
104
    }
105
106
    public function testDeletePermissions()
107
    {
108
        $editor = $this->objFromFixture(Member::class, 'editor');
109
110
        $about = $this->objFromFixture(TestPermissionNode::class, 'about');
111
        $aboutStaff = $this->objFromFixture(TestPermissionNode::class, 'about-staff');
112
        $history = $this->objFromFixture(TestPermissionNode::class, 'history');
113
        $products = $this->objFromFixture(TestPermissionNode::class, 'products');
114
        $product1 = $this->objFromFixture(TestPermissionNode::class, 'products-product1');
115
        $product4 = $this->objFromFixture(TestPermissionNode::class, 'products-product4');
116
117
        // Test logged out users cannot edit
118
        Member::actAs(null, function () use ($aboutStaff) {
119
            $this->assertFalse($aboutStaff->canDelete());
120
        });
121
122
        // Can't edit a page that is locked to admins
123
        $this->assertFalse($about->canDelete($editor));
124
125
        // Can't delete a page if a child (product4) is un-deletable
126
        $this->assertFalse($products->canDelete($editor));
127
128
        // Can edit a child of that page that inherits
129
        $this->assertTrue($product1->canDelete($editor));
130
131
        // Can't edit a child of that page that has its permissions overridden
132
        $this->assertFalse($product4->canDelete($editor));
133
134
        // Test that root node respects root permissions
135
        $this->assertTrue($history->canDelete($editor));
136
137
        TestPermissionNode::getInheritedPermissions()->clearCache();
138
        $this->rootPermissions->setCanEdit(false);
139
140
        // With root edit false, permissions are now denied for CanEditType = Inherit
141
        $this->assertFalse($history->canDelete($editor));
142
    }
143
144
    public function testViewPermissions()
145
    {
146
        $history = $this->objFromFixture(TestPermissionNode::class, 'history');
147
        $contact = $this->objFromFixture(TestPermissionNode::class, 'contact');
148
        $contactForm = $this->objFromFixture(TestPermissionNode::class, 'contact-form');
149
        $secret = $this->objFromFixture(TestPermissionNode::class, 'secret');
150
        $secretNested = $this->objFromFixture(TestPermissionNode::class, 'secret-nested');
151
        $protected = $this->objFromFixture(TestPermissionNode::class, 'protected');
152
        $protectedChild = $this->objFromFixture(TestPermissionNode::class, 'protected-child');
153
        $editor = $this->objFromFixture(Member::class, 'editor');
154
155
        // Not logged in user can only access Inherit or Anyone pages
156
        Member::actAs(
157
            null,
158
            function () use ($protectedChild, $secretNested, $protected, $secret, $history, $contact, $contactForm) {
159
                $this->assertTrue($history->canView());
160
                $this->assertTrue($contact->canView());
161
                $this->assertTrue($contactForm->canView());
162
                // Protected
163
                $this->assertFalse($secret->canView());
164
                $this->assertFalse($secretNested->canView());
165
                $this->assertFalse($protected->canView());
166
                $this->assertFalse($protectedChild->canView());
167
            }
168
        );
169
170
        // Editor can view pages restricted to logged in users
171
        $this->assertTrue($secret->canView($editor));
172
        $this->assertTrue($secretNested->canView($editor));
173
174
        // Cannot read admin-only pages
175
        $this->assertFalse($protected->canView($editor));
176
        $this->assertFalse($protectedChild->canView($editor));
177
178
        // Check root permissions
179
        $this->assertTrue($history->canView($editor));
180
181
        TestPermissionNode::getInheritedPermissions()->clearCache();
182
        $this->rootPermissions->setCanView(false);
183
184
        $this->assertFalse($history->canView($editor));
185
    }
186
187
    public function testUnstagedViewPermissions()
188
    {
189
        $history = $this->objFromFixture(UnstagedNode::class, 'history');
190
        $contact = $this->objFromFixture(UnstagedNode::class, 'contact');
191
        $contactForm = $this->objFromFixture(UnstagedNode::class, 'contact-form');
192
        $secret = $this->objFromFixture(UnstagedNode::class, 'secret');
193
        $secretNested = $this->objFromFixture(UnstagedNode::class, 'secret-nested');
194
        $protected = $this->objFromFixture(UnstagedNode::class, 'protected');
195
        $protectedChild = $this->objFromFixture(UnstagedNode::class, 'protected-child');
196
        $editor = $this->objFromFixture(Member::class, 'editor');
197
198
        // Not logged in user can only access Inherit or Anyone pages
199
        Member::actAs(
200
            null,
201
            function () use ($protectedChild, $secretNested, $protected, $secret, $history, $contact, $contactForm) {
202
                $this->assertTrue($history->canView());
203
                $this->assertTrue($contact->canView());
204
                $this->assertTrue($contactForm->canView());
205
                // Protected
206
                $this->assertFalse($secret->canView());
207
                $this->assertFalse($secretNested->canView());
208
                $this->assertFalse($protected->canView());
209
                $this->assertFalse($protectedChild->canView());
210
            }
211
        );
212
213
        // Editor can view pages restricted to logged in users
214
        $this->assertTrue($secret->canView($editor));
215
        $this->assertTrue($secretNested->canView($editor));
216
217
        // Cannot read admin-only pages
218
        $this->assertFalse($protected->canView($editor));
219
        $this->assertFalse($protectedChild->canView($editor));
220
221
        // Check root permissions
222
        $this->assertTrue($history->canView($editor));
223
224
        UnstagedNode::getInheritedPermissions()->clearCache();
225
        $this->rootPermissions->setCanView(false);
226
227
        $this->assertFalse($history->canView($editor));
228
    }
229
230
    /**
231
     * Test that draft permissions deny unrestricted live permissions
232
     */
233
    public function testRestrictedDraftUnrestrictedLive()
234
    {
235
        Versioned::set_stage(Versioned::DRAFT);
236
237
        // Should be editable by non-admin editor
238
        /** @var TestPermissionNode $products */
239
        $products = $this->objFromFixture(TestPermissionNode::class, 'products');
240
        /** @var TestPermissionNode $products1 */
241
        $products1 = $this->objFromFixture(TestPermissionNode::class, 'products-product1');
242
        $editor = $this->objFromFixture(Member::class, 'editor');
243
244
        // Ensure the editor can edit
245
        $this->assertTrue($products->canEdit($editor));
246
        $this->assertTrue($products1->canEdit($editor));
247
248
        // Write current version to live
249
        $products->writeToStage(Versioned::LIVE);
0 ignored issues
show
Bug introduced by
The method writeToStage() does not exist on SilverStripe\Security\Te...Test\TestPermissionNode. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

249
        $products->/** @scrutinizer ignore-call */ 
250
                   writeToStage(Versioned::LIVE);
Loading history...
250
        $products1->writeToStage(Versioned::LIVE);
251
252
        // Draft version restrict to admins
253
        $products->EditorGroups()->setByIDList([
0 ignored issues
show
Bug introduced by
The method EditorGroups() does not exist on SilverStripe\Security\Te...Test\TestPermissionNode. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

253
        $products->/** @scrutinizer ignore-call */ 
254
                   EditorGroups()->setByIDList([
Loading history...
254
            $this->idFromFixture(Group::class, 'admins')
255
        ]);
256
        $products->write();
257
258
        // Ensure editor can no longer edit
259
        TestPermissionNode::getInheritedPermissions()->clearCache();
260
        $this->assertFalse($products->canEdit($editor));
261
        $this->assertFalse($products1->canEdit($editor));
262
    }
263
264
    /**
265
     * Test that draft permissions permit access over live permissions
266
     */
267
    public function testUnrestrictedDraftOverridesLive()
268
    {
269
        Versioned::set_stage(Versioned::DRAFT);
270
271
        // Should be editable by non-admin editor
272
        /** @var TestPermissionNode $about */
273
        $about = $this->objFromFixture(TestPermissionNode::class, 'about');
274
        /** @var TestPermissionNode $aboutStaff */
275
        $aboutStaff = $this->objFromFixture(TestPermissionNode::class, 'about-staff');
276
        $editor = $this->objFromFixture(Member::class, 'editor');
277
278
        // Ensure the editor can't edit
279
        $this->assertFalse($about->canEdit($editor));
280
        $this->assertFalse($aboutStaff->canEdit($editor));
281
282
        // Write current version to live
283
        $about->writeToStage(Versioned::LIVE);
284
        $aboutStaff->writeToStage(Versioned::LIVE);
285
286
        // Unrestrict draft
287
        $about->CanEditType = InheritedPermissions::LOGGED_IN_USERS;
0 ignored issues
show
Bug Best Practice introduced by
The property CanEditType does not exist on SilverStripe\Security\Te...Test\TestPermissionNode. Since you implemented __set, consider adding a @property annotation.
Loading history...
288
        $about->write();
289
290
        // Ensure editor can no longer edit
291
        TestPermissionNode::getInheritedPermissions()->clearCache();
292
        $this->assertTrue($about->canEdit($editor));
293
        $this->assertTrue($aboutStaff->canEdit($editor));
294
    }
295
296
    /**
297
     * Ensure that flipping parent / child relationship on live doesn't
298
     * cause infinite loop
299
     */
300
    public function testMobiusHierarchy()
301
    {
302
        Versioned::set_stage(Versioned::DRAFT);
303
304
        /** @var TestPermissionNode $history */
305
        $history = $this->objFromFixture(TestPermissionNode::class, 'history');
306
        /** @var TestPermissionNode $historyGallery */
307
        $historyGallery = $this->objFromFixture(TestPermissionNode::class, 'history-gallery');
308
309
        // Publish current state to live
310
        $history->writeToStage(Versioned::LIVE);
311
        $historyGallery->writeToStage(Versioned::LIVE);
312
313
        // Flip relation
314
        $historyGallery->ParentID = 0;
0 ignored issues
show
Bug Best Practice introduced by
The property ParentID does not exist on SilverStripe\Security\Te...Test\TestPermissionNode. Since you implemented __set, consider adding a @property annotation.
Loading history...
315
        $historyGallery->write();
316
        $history->ParentID = $historyGallery->ID;
317
        $history->write();
318
319
        // Test viewability (not logged in users)
320
        Member::actAs(null, function () use ($history, $historyGallery) {
321
            $this->assertTrue($history->canView());
322
            $this->assertTrue($historyGallery->canView());
323
        });
324
325
        // Change permission on draft root and ensure it affects both
326
        $historyGallery->CanViewType = InheritedPermissions::LOGGED_IN_USERS;
0 ignored issues
show
Bug Best Practice introduced by
The property CanViewType does not exist on SilverStripe\Security\Te...Test\TestPermissionNode. Since you implemented __set, consider adding a @property annotation.
Loading history...
327
        $historyGallery->write();
328
        TestPermissionNode::getInheritedPermissions()->clearCache();
329
330
        // Test viewability (not logged in users)
331
        Member::actAs(null, function () use ($history, $historyGallery) {
332
            $this->assertFalse($historyGallery->canView());
333
            $this->assertFalse($history->canView());
334
        });
335
    }
336
337
    public function testPermissionsPersistCache()
338
    {
339
        /* @var CacheInterface $cache */
340
        $cache = Injector::inst()->create(CacheInterface::class . '.InheritedPermissions');
341
        $cache->clear();
342
343
        $member = $this->objFromFixture(Member::class, 'editor');
344
345
        /** @var TestPermissionNode $history */
346
        $history = $this->objFromFixture(TestPermissionNode::class, 'history');
347
        /** @var TestPermissionNode $historyGallery */
348
        $historyGallery = $this->objFromFixture(TestPermissionNode::class, 'history-gallery');
349
        $permissionChecker = new InheritedPermissions(TestPermissionNode::class, $cache);
350
351
        $viewKey = $this->generateCacheKey($permissionChecker, InheritedPermissions::VIEW, $member->ID);
352
        $editKey = $this->generateCacheKey($permissionChecker, InheritedPermissions::EDIT, $member->ID);
353
354
        $this->assertNull($cache->get($editKey));
355
        $this->assertNull($cache->get($viewKey));
356
357
        $permissionChecker->canEditMultiple([$history->ID, $historyGallery->ID], $member);
358
        $this->assertNull($cache->get($editKey));
359
        $this->assertNull($cache->get($viewKey));
360
361
        unset($permissionChecker);
362
        $this->assertTrue(is_array($cache->get($editKey)));
363
        $this->assertNull($cache->get($viewKey));
364
        $this->assertArrayHasKey($history->ID, $cache->get($editKey));
365
        $this->assertArrayHasKey($historyGallery->ID, $cache->get($editKey));
366
367
        $permissionChecker = new InheritedPermissions(TestPermissionNode::class, $cache);
368
        $permissionChecker->canViewMultiple([$history->ID], $member);
369
        $this->assertNotNull($cache->get($editKey));
370
        $this->assertNull($cache->get($viewKey));
371
372
        unset($permissionChecker);
373
        $this->assertTrue(is_array($cache->get($viewKey)));
374
        $this->assertTrue(is_array($cache->get($editKey)));
375
        $this->assertArrayHasKey($history->ID, $cache->get($viewKey));
376
        $this->assertArrayNotHasKey($historyGallery->ID, $cache->get($viewKey));
377
    }
378
379
    public function testPermissionsFlushCache()
380
    {
381
        /* @var CacheInterface $cache */
382
        $cache = Injector::inst()->create(CacheInterface::class . '.InheritedPermissions');
383
        $cache->clear();
384
385
        $permissionChecker = new InheritedPermissions(TestPermissionNode::class, $cache);
386
        $member1 = $this->objFromFixture(Member::class, 'editor');
387
        $member2 = $this->objFromFixture(Member::class, 'admin');
388
        $editKey1 = $this->generateCacheKey($permissionChecker, InheritedPermissions::EDIT, $member1->ID);
389
        $editKey2 = $this->generateCacheKey($permissionChecker, InheritedPermissions::EDIT, $member2->ID);
390
        $viewKey1 = $this->generateCacheKey($permissionChecker, InheritedPermissions::VIEW, $member1->ID);
391
        $viewKey2 = $this->generateCacheKey($permissionChecker, InheritedPermissions::VIEW, $member2->ID);
392
393
        foreach ([$editKey1, $editKey2, $viewKey1, $viewKey2] as $key) {
394
            $this->assertNull($cache->get($key));
395
        }
396
397
        /** @var TestPermissionNode $history */
398
        $history = $this->objFromFixture(TestPermissionNode::class, 'history');
399
        /** @var TestPermissionNode $historyGallery */
400
        $historyGallery = $this->objFromFixture(TestPermissionNode::class, 'history-gallery');
401
402
        $permissionChecker->canEditMultiple([$history->ID, $historyGallery->ID], $member1);
403
        $permissionChecker->canViewMultiple([$history->ID, $historyGallery->ID], $member1);
404
        $permissionChecker->canEditMultiple([$history->ID, $historyGallery->ID], $member2);
405
        $permissionChecker->canViewMultiple([$history->ID, $historyGallery->ID], $member2);
406
407
        unset($permissionChecker);
408
409
        foreach ([$editKey1, $editKey2, $viewKey1, $viewKey2] as $key) {
410
            $this->assertNotNull($cache->get($key));
411
        }
412
        $permissionChecker = new InheritedPermissions(TestPermissionNode::class, $cache);
413
414
        // Non existent ID
415
        $permissionChecker->flushMemberCache('dummy');
0 ignored issues
show
Bug introduced by
'dummy' of type string is incompatible with the type array expected by parameter $memberIDs of SilverStripe\Security\In...ons::flushMemberCache(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

415
        $permissionChecker->flushMemberCache(/** @scrutinizer ignore-type */ 'dummy');
Loading history...
416
        foreach ([$editKey1, $editKey2, $viewKey1, $viewKey2] as $key) {
417
            $this->assertNotNull($cache->get($key));
418
        }
419
420
        // Precision strike
421
        $permissionChecker->flushMemberCache([$member1->ID]);
422
        // Member1 should be clear
423
        $this->assertNull($cache->get($editKey1));
424
        $this->assertNull($cache->get($viewKey1));
425
        // Member 2 is unaffected
426
        $this->assertNotNull($cache->get($editKey2));
427
        $this->assertNotNull($cache->get($viewKey2));
428
429
        // Nuclear
430
        $permissionChecker->flushMemberCache();
431
        foreach ([$editKey1, $editKey2, $viewKey1, $viewKey2] as $key) {
432
            $this->assertNull($cache->get($key));
433
        }
434
    }
435
436
    protected function generateCacheKey(InheritedPermissions $inst, $type, $memberID)
437
    {
438
        $reflection = new ReflectionClass(InheritedPermissions::class);
439
        $method = $reflection->getMethod('generateCacheKey');
440
        $method->setAccessible(true);
441
442
        return $method->invokeArgs($inst, [$type, $memberID]);
443
    }
444
}
445