Passed
Push — master ( 0208b2...1155ca )
by Robbie
09:03
created

MemberTest::testKeepsEncryptionOnEmptyPasswords()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 17
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 12
nc 1
nop 0
dl 0
loc 17
rs 9.4285
c 0
b 0
f 0
1
<?php
2
3
namespace SilverStripe\Security\Tests;
4
5
use SilverStripe\Control\Cookie;
6
use SilverStripe\Core\Config\Config;
7
use SilverStripe\Core\Convert;
8
use SilverStripe\Core\Injector\Injector;
9
use SilverStripe\Dev\FunctionalTest;
10
use SilverStripe\Forms\ListboxField;
11
use SilverStripe\i18n\i18n;
12
use SilverStripe\ORM\DataObject;
13
use SilverStripe\ORM\DB;
14
use SilverStripe\ORM\FieldType\DBDatetime;
15
use SilverStripe\ORM\ValidationException;
16
use SilverStripe\ORM\ValidationResult;
17
use SilverStripe\Security\Group;
18
use SilverStripe\Security\IdentityStore;
19
use SilverStripe\Security\Member;
20
use SilverStripe\Security\Member_Validator;
21
use SilverStripe\Security\MemberAuthenticator\MemberAuthenticator;
22
use SilverStripe\Security\MemberAuthenticator\SessionAuthenticationHandler;
23
use SilverStripe\Security\MemberPassword;
24
use SilverStripe\Security\PasswordEncryptor_Blowfish;
25
use SilverStripe\Security\Permission;
26
use SilverStripe\Security\RememberLoginHash;
27
use SilverStripe\Security\Security;
28
use SilverStripe\Security\Tests\MemberTest\FieldsExtension;
29
30
class MemberTest extends FunctionalTest
31
{
32
    protected static $fixture_file = 'MemberTest.yml';
33
34
    protected $orig = array();
35
36
    protected static $illegal_extensions = [
37
        Member::class => '*',
38
    ];
39
40
    public static function setUpBeforeClass()
41
    {
42
        parent::setUpBeforeClass();
43
44
        //Setting the locale has to happen in the constructor (using the setUp and tearDown methods doesn't work)
45
        //This is because the test relies on the yaml file being interpreted according to a particular date format
46
        //and this setup occurs before the setUp method is run
47
        i18n::config()->set('default_locale', 'en_US');
48
    }
49
50
    /**
51
     * @skipUpgrade
52
     */
53
    protected function setUp()
54
    {
55
        parent::setUp();
56
57
        Member::config()->set('unique_identifier_field', 'Email');
58
        Member::set_password_validator(null);
59
    }
60
61
    public function testPasswordEncryptionUpdatedOnChangedPassword()
62
    {
63
        Config::modify()->set(Security::class, 'password_encryption_algorithm', 'none');
64
        $member = Member::create();
65
        $member->Password = 'password';
66
        $member->write();
67
        $this->assertEquals('password', $member->Password);
68
        $this->assertEquals('none', $member->PasswordEncryption);
69
        Config::modify()->set(Security::class, 'password_encryption_algorithm', 'blowfish');
70
        $member->Password = 'newpassword';
71
        $member->write();
72
        $this->assertNotEquals('password', $member->Password);
73
        $this->assertNotEquals('newpassword', $member->Password);
74
        $this->assertEquals('blowfish', $member->PasswordEncryption);
75
    }
76
77
    public function testWriteDoesntMergeNewRecordWithExistingMember()
78
    {
79
        $this->expectException(ValidationException::class);
80
        $m1 = new Member();
81
        $m1->Email = '[email protected]';
82
        $m1->write();
83
84
        $m2 = new Member();
85
        $m2->Email = '[email protected]';
86
        $m2->write();
87
    }
88
89
    /**
90
     * @expectedException \SilverStripe\ORM\ValidationException
91
     */
92
    public function testWriteDoesntMergeExistingMemberOnIdentifierChange()
93
    {
94
        $m1 = new Member();
95
        $m1->Email = '[email protected]';
96
        $m1->write();
97
98
        $m2 = new Member();
99
        $m2->Email = '[email protected]';
100
        $m2->write();
101
102
        $m2->Email = '[email protected]';
103
        $m2->write();
104
    }
105
106
    public function testDefaultPasswordEncryptionOnMember()
107
    {
108
        $memberWithPassword = new Member();
109
        $memberWithPassword->Password = 'mypassword';
110
        $memberWithPassword->write();
111
        $this->assertEquals(
112
            Security::config()->get('password_encryption_algorithm'),
113
            $memberWithPassword->PasswordEncryption,
114
            'Password encryption is set for new member records on first write (with setting "Password")'
115
        );
116
117
        $memberNoPassword = new Member();
118
        $memberNoPassword->write();
119
        $this->assertEquals(
120
            Security::config()->get('password_encryption_algorithm'),
121
            $memberNoPassword->PasswordEncryption,
122
            'Password encryption is not set for new member records on first write, when not setting a "Password")'
123
        );
124
    }
125
126
    public function testKeepsEncryptionOnEmptyPasswords()
127
    {
128
        $member = new Member();
129
        $member->Password = 'mypassword';
130
        $member->PasswordEncryption = 'sha1_v2.4';
131
        $member->write();
132
133
        $member->Password = '';
134
        $member->write();
135
136
        $this->assertEquals(
137
            Security::config()->get('password_encryption_algorithm'),
138
            $member->PasswordEncryption
139
        );
140
        $auth = new MemberAuthenticator();
141
        $result = $auth->checkPassword($member, '');
142
        $this->assertTrue($result->isValid());
143
    }
144
145
    public function testSetPassword()
146
    {
147
        /** @var Member $member */
148
        $member = $this->objFromFixture(Member::class, 'test');
149
        $member->Password = "test1";
150
        $member->write();
151
        $auth = new MemberAuthenticator();
152
        $result = $auth->checkPassword($member, 'test1');
153
        $this->assertTrue($result->isValid());
154
    }
155
156
    /**
157
     * Test that password changes are logged properly
158
     */
159
    public function testPasswordChangeLogging()
160
    {
161
        /** @var Member $member */
162
        $member = $this->objFromFixture(Member::class, 'test');
163
        $this->assertNotNull($member);
164
        $member->Password = "test1";
165
        $member->write();
166
167
        $member->Password = "test2";
168
        $member->write();
169
170
        $member->Password = "test3";
171
        $member->write();
172
173
        $passwords = DataObject::get(
174
            MemberPassword::class,
175
            "\"MemberID\" = $member->ID",
176
            "\"Created\" DESC, \"ID\" DESC"
177
        )
178
            ->getIterator();
179
        $this->assertNotNull($passwords);
180
        $passwords->rewind();
181
        $this->assertTrue($passwords->current()->checkPassword('test3'), "Password test3 not found in MemberRecord");
182
183
        $passwords->next();
184
        $this->assertTrue($passwords->current()->checkPassword('test2'), "Password test2 not found in MemberRecord");
185
186
        $passwords->next();
187
        $this->assertTrue($passwords->current()->checkPassword('test1'), "Password test1 not found in MemberRecord");
188
189
        $passwords->next();
190
        $this->assertInstanceOf('SilverStripe\\ORM\\DataObject', $passwords->current());
191
        $this->assertTrue(
192
            $passwords->current()->checkPassword('1nitialPassword'),
193
            "Password 1nitialPassword not found in MemberRecord"
194
        );
195
196
        //check we don't retain orphaned records when a member is deleted
197
        $member->delete();
198
199
        $passwords = MemberPassword::get()->filter('MemberID', $member->OldID);
200
201
        $this->assertCount(0, $passwords);
202
    }
203
204
    /**
205
     * Test that changed passwords will send an email
206
     */
207
    public function testChangedPasswordEmaling()
208
    {
209
        Member::config()->update('notify_password_change', true);
210
211
        $this->clearEmails();
212
213
        /** @var Member $member */
214
        $member = $this->objFromFixture(Member::class, 'test');
215
        $this->assertNotNull($member);
216
        $valid = $member->changePassword('32asDF##$$%%');
217
        $this->assertTrue($valid->isValid());
218
219
        $this->assertEmailSent(
220
            '[email protected]',
221
            null,
222
            'Your password has been changed',
223
            '/testuser@example\.com/'
224
        );
225
    }
226
227
    /**
228
     * Test that triggering "forgotPassword" sends an Email with a reset link
229
        */
230
    public function testForgotPasswordEmaling()
231
    {
232
        $this->clearEmails();
233
        $this->autoFollowRedirection = false;
234
235
        /** @var Member $member */
236
        $member = $this->objFromFixture(Member::class, 'test');
237
        $this->assertNotNull($member);
238
239
        // Initiate a password-reset
240
        $response = $this->post('Security/lostpassword/LostPasswordForm', array('Email' => $member->Email));
241
242
        $this->assertEquals($response->getStatusCode(), 302);
243
244
        // We should get redirected to Security/passwordsent
245
        $this->assertContains(
246
            'Security/lostpassword/passwordsent/[email protected]',
247
            urldecode($response->getHeader('Location'))
248
        );
249
250
        // Check existance of reset link
251
        $this->assertEmailSent(
252
            "[email protected]",
253
            null,
254
            'Your password reset link',
255
            '/Security\/changepassword\?m=' . $member->ID . '&amp;t=[^"]+/'
256
        );
257
    }
258
259
    /**
260
     * Test that passwords validate against NZ e-government guidelines
261
     *  - don't allow the use of the last 6 passwords
262
     *  - require at least 3 of lowercase, uppercase, digits and punctuation
263
     *  - at least 7 characters long
264
     */
265
    public function testValidatePassword()
266
    {
267
        /**
268
         * @var Member $member
269
        */
270
        $member = $this->objFromFixture(Member::class, 'test');
271
        $this->assertNotNull($member);
272
273
        Member::set_password_validator(new MemberTest\TestPasswordValidator());
274
275
        // BAD PASSWORDS
276
277
        $result = $member->changePassword('shorty');
278
        $this->assertFalse($result->isValid());
279
        $this->assertArrayHasKey("TOO_SHORT", $result->getMessages());
280
281
        $result = $member->changePassword('longone');
282
        $this->assertArrayNotHasKey("TOO_SHORT", $result->getMessages());
283
        $this->assertArrayHasKey("LOW_CHARACTER_STRENGTH", $result->getMessages());
284
        $this->assertFalse($result->isValid());
285
286
        $result = $member->changePassword('w1thNumb3rs');
287
        $this->assertArrayNotHasKey("LOW_CHARACTER_STRENGTH", $result->getMessages());
288
        $this->assertTrue($result->isValid());
289
290
        // Clear out the MemberPassword table to ensure that the system functions properly in that situation
291
        DB::query("DELETE FROM \"MemberPassword\"");
292
293
        // GOOD PASSWORDS
294
295
        $result = $member->changePassword('withSym###Ls');
296
        $this->assertArrayNotHasKey("LOW_CHARACTER_STRENGTH", $result->getMessages());
297
        $this->assertTrue($result->isValid());
298
299
        $result = $member->changePassword('withSym###Ls2');
300
        $this->assertTrue($result->isValid());
301
302
        $result = $member->changePassword('withSym###Ls3');
303
        $this->assertTrue($result->isValid());
304
305
        $result = $member->changePassword('withSym###Ls4');
306
        $this->assertTrue($result->isValid());
307
308
        $result = $member->changePassword('withSym###Ls5');
309
        $this->assertTrue($result->isValid());
310
311
        $result = $member->changePassword('withSym###Ls6');
312
        $this->assertTrue($result->isValid());
313
314
        $result = $member->changePassword('withSym###Ls7');
315
        $this->assertTrue($result->isValid());
316
317
        // CAN'T USE PASSWORDS 2-7, but I can use pasword 1
318
319
        $result = $member->changePassword('withSym###Ls2');
320
        $this->assertFalse($result->isValid());
321
        $this->assertArrayHasKey("PREVIOUS_PASSWORD", $result->getMessages());
322
323
        $result = $member->changePassword('withSym###Ls5');
324
        $this->assertFalse($result->isValid());
325
        $this->assertArrayHasKey("PREVIOUS_PASSWORD", $result->getMessages());
326
327
        $result = $member->changePassword('withSym###Ls7');
328
        $this->assertFalse($result->isValid());
329
        $this->assertArrayHasKey("PREVIOUS_PASSWORD", $result->getMessages());
330
331
        $result = $member->changePassword('withSym###Ls');
332
        $this->assertTrue($result->isValid());
333
334
        // HAVING DONE THAT, PASSWORD 2 is now available from the list
335
336
        $result = $member->changePassword('withSym###Ls2');
337
        $this->assertTrue($result->isValid());
338
339
        $result = $member->changePassword('withSym###Ls3');
340
        $this->assertTrue($result->isValid());
341
342
        $result = $member->changePassword('withSym###Ls4');
343
        $this->assertTrue($result->isValid());
344
345
        Member::set_password_validator(null);
346
    }
347
348
    /**
349
     * Test that the PasswordExpiry date is set when passwords are changed
350
     */
351
    public function testPasswordExpirySetting()
352
    {
353
        Member::config()->set('password_expiry_days', 90);
354
355
        /** @var Member $member */
356
        $member = $this->objFromFixture(Member::class, 'test');
357
        $this->assertNotNull($member);
358
        $valid = $member->changePassword("Xx?1234234");
359
        $this->assertTrue($valid->isValid());
360
361
        $expiryDate = date('Y-m-d', time() + 90*86400);
362
        $this->assertEquals($expiryDate, $member->PasswordExpiry);
363
364
        Member::config()->set('password_expiry_days', null);
365
        $valid = $member->changePassword("Xx?1234235");
366
        $this->assertTrue($valid->isValid());
367
368
        $this->assertNull($member->PasswordExpiry);
369
    }
370
371
    public function testIsPasswordExpired()
372
    {
373
        /** @var Member $member */
374
        $member = $this->objFromFixture(Member::class, 'test');
375
        $this->assertNotNull($member);
376
        $this->assertFalse($member->isPasswordExpired());
377
378
        $member = $this->objFromFixture(Member::class, 'noexpiry');
379
        $member->PasswordExpiry = null;
0 ignored issues
show
Bug Best Practice introduced by
The property PasswordExpiry does not exist on SilverStripe\ORM\DataObject. Since you implemented __set, consider adding a @property annotation.
Loading history...
380
        $this->assertFalse($member->isPasswordExpired());
0 ignored issues
show
Bug introduced by
The method isPasswordExpired() does not exist on SilverStripe\ORM\DataObject. 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

380
        $this->assertFalse($member->/** @scrutinizer ignore-call */ isPasswordExpired());
Loading history...
381
382
        $member = $this->objFromFixture(Member::class, 'expiredpassword');
383
        $this->assertTrue($member->isPasswordExpired());
384
385
        // Check the boundary conditions
386
        // If PasswordExpiry == today, then it's expired
387
        $member->PasswordExpiry = date('Y-m-d');
388
        $this->assertTrue($member->isPasswordExpired());
389
390
        // If PasswordExpiry == tomorrow, then it's not
391
        $member->PasswordExpiry = date('Y-m-d', time() + 86400);
392
        $this->assertFalse($member->isPasswordExpired());
393
    }
394
    public function testInGroups()
395
    {
396
        /** @var Member $staffmember */
397
        $staffmember = $this->objFromFixture(Member::class, 'staffmember');
398
        /** @var Member $ceomember */
399
        $ceomember = $this->objFromFixture(Member::class, 'ceomember');
400
401
        $staffgroup = $this->objFromFixture(Group::class, 'staffgroup');
402
        $managementgroup = $this->objFromFixture(Group::class, 'managementgroup');
403
        $ceogroup = $this->objFromFixture(Group::class, 'ceogroup');
404
405
        $this->assertTrue(
406
            $staffmember->inGroups(array($staffgroup, $managementgroup)),
407
            'inGroups() succeeds if a membership is detected on one of many passed groups'
408
        );
409
        $this->assertFalse(
410
            $staffmember->inGroups(array($ceogroup, $managementgroup)),
411
            'inGroups() fails if a membership is detected on none of the passed groups'
412
        );
413
        $this->assertFalse(
414
            $ceomember->inGroups(array($staffgroup, $managementgroup), true),
415
            'inGroups() fails if no direct membership is detected on any of the passed groups (in strict mode)'
416
        );
417
    }
418
419
    /**
420
     * Assertions to check that Member_GroupSet is functionally equivalent to ManyManyList
421
     */
422
    public function testRemoveGroups()
423
    {
424
        $staffmember = $this->objFromFixture(Member::class, 'staffmember');
425
426
        $staffgroup = $this->objFromFixture(Group::class, 'staffgroup');
427
        $managementgroup = $this->objFromFixture(Group::class, 'managementgroup');
428
429
        $this->assertTrue(
430
            $staffmember->inGroups(array($staffgroup, $managementgroup)),
0 ignored issues
show
Bug introduced by
The method inGroups() does not exist on SilverStripe\ORM\DataObject. 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

430
            $staffmember->/** @scrutinizer ignore-call */ 
431
                          inGroups(array($staffgroup, $managementgroup)),
Loading history...
431
            'inGroups() succeeds if a membership is detected on one of many passed groups'
432
        );
433
434
        $staffmember->Groups()->remove($managementgroup);
0 ignored issues
show
Bug introduced by
The method Groups() does not exist on SilverStripe\ORM\DataObject. 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

434
        $staffmember->/** @scrutinizer ignore-call */ 
435
                      Groups()->remove($managementgroup);
Loading history...
435
        $this->assertFalse(
436
            $staffmember->inGroup($managementgroup),
0 ignored issues
show
Bug introduced by
The method inGroup() does not exist on SilverStripe\ORM\DataObject. 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

436
            $staffmember->/** @scrutinizer ignore-call */ 
437
                          inGroup($managementgroup),
Loading history...
437
            'member was not removed from group using ->Groups()->remove()'
438
        );
439
440
        $staffmember->Groups()->removeAll();
441
        $this->assertCount(
442
            0,
443
            $staffmember->Groups(),
444
            'member was not removed from all groups using ->Groups()->removeAll()'
445
        );
446
    }
447
448
    public function testAddToGroupByCode()
449
    {
450
        /** @var Member $grouplessMember */
451
        $grouplessMember = $this->objFromFixture(Member::class, 'grouplessmember');
452
        /** @var Group $memberlessGroup */
453
        $memberlessGroup = $this->objFromFixture(Group::class, 'memberlessgroup');
454
455
        $this->assertFalse($grouplessMember->Groups()->exists());
456
        $this->assertFalse($memberlessGroup->Members()->exists());
457
458
        $grouplessMember->addToGroupByCode('memberless');
459
460
        $this->assertEquals($memberlessGroup->Members()->count(), 1);
461
        $this->assertEquals($grouplessMember->Groups()->count(), 1);
462
463
        $grouplessMember->addToGroupByCode('somegroupthatwouldneverexist', 'New Group');
464
        $this->assertEquals($grouplessMember->Groups()->count(), 2);
465
466
        /** @var Group $group */
467
        $group = DataObject::get_one(
468
            Group::class,
469
            array(
470
            '"Group"."Code"' => 'somegroupthatwouldneverexist'
471
            )
472
        );
473
        $this->assertNotNull($group);
474
        $this->assertEquals($group->Code, 'somegroupthatwouldneverexist');
475
        $this->assertEquals($group->Title, 'New Group');
476
    }
477
478
    public function testRemoveFromGroupByCode()
479
    {
480
        /** @var Member $grouplessMember */
481
        $grouplessMember = $this->objFromFixture(Member::class, 'grouplessmember');
482
        /** @var Group $memberlessGroup */
483
        $memberlessGroup = $this->objFromFixture(Group::class, 'memberlessgroup');
484
485
        $this->assertFalse($grouplessMember->Groups()->exists());
486
        $this->assertFalse($memberlessGroup->Members()->exists());
487
488
        $grouplessMember->addToGroupByCode('memberless');
489
490
        $this->assertEquals($memberlessGroup->Members()->count(), 1);
491
        $this->assertEquals($grouplessMember->Groups()->count(), 1);
492
493
        $grouplessMember->addToGroupByCode('somegroupthatwouldneverexist', 'New Group');
494
        $this->assertEquals($grouplessMember->Groups()->count(), 2);
495
496
        /** @var Group $group */
497
        $group = DataObject::get_one(Group::class, "\"Code\" = 'somegroupthatwouldneverexist'");
498
        $this->assertNotNull($group);
499
        $this->assertEquals($group->Code, 'somegroupthatwouldneverexist');
500
        $this->assertEquals($group->Title, 'New Group');
501
502
        $grouplessMember->removeFromGroupByCode('memberless');
503
        $this->assertEquals($memberlessGroup->Members()->count(), 0);
504
        $this->assertEquals($grouplessMember->Groups()->count(), 1);
505
506
        $grouplessMember->removeFromGroupByCode('somegroupthatwouldneverexist');
507
        $this->assertEquals($grouplessMember->Groups()->count(), 0);
508
    }
509
510
    public function testInGroup()
511
    {
512
        /** @var Member $staffmember */
513
        $staffmember = $this->objFromFixture(Member::class, 'staffmember');
514
        /** @var Member $managementmember */
515
        $managementmember = $this->objFromFixture(Member::class, 'managementmember');
516
        /** @var Member $accountingmember */
517
        $accountingmember = $this->objFromFixture(Member::class, 'accountingmember');
518
        /** @var Member $ceomember */
519
        $ceomember = $this->objFromFixture(Member::class, 'ceomember');
520
521
        /** @var Group $staffgroup */
522
        $staffgroup = $this->objFromFixture(Group::class, 'staffgroup');
523
        /** @var Group $managementgroup */
524
        $managementgroup = $this->objFromFixture(Group::class, 'managementgroup');
525
        /** @var Group $ceogroup */
526
        $ceogroup = $this->objFromFixture(Group::class, 'ceogroup');
527
528
        $this->assertTrue(
529
            $staffmember->inGroup($staffgroup),
530
            'Direct group membership is detected'
531
        );
532
        $this->assertTrue(
533
            $managementmember->inGroup($staffgroup),
534
            'Users of child group are members of a direct parent group (if not in strict mode)'
535
        );
536
        $this->assertTrue(
537
            $accountingmember->inGroup($staffgroup),
538
            'Users of child group are members of a direct parent group (if not in strict mode)'
539
        );
540
        $this->assertTrue(
541
            $ceomember->inGroup($staffgroup),
542
            'Users of indirect grandchild group are members of a parent group (if not in strict mode)'
543
        );
544
        $this->assertTrue(
545
            $ceomember->inGroup($ceogroup, true),
546
            'Direct group membership is dected (if in strict mode)'
547
        );
548
        $this->assertFalse(
549
            $ceomember->inGroup($staffgroup, true),
550
            'Users of child group are not members of a direct parent group (if in strict mode)'
551
        );
552
        $this->assertFalse(
553
            $staffmember->inGroup($managementgroup),
554
            'Users of parent group are not members of a direct child group'
555
        );
556
        $this->assertFalse(
557
            $staffmember->inGroup($ceogroup),
558
            'Users of parent group are not members of an indirect grandchild group'
559
        );
560
        $this->assertFalse(
561
            $accountingmember->inGroup($managementgroup),
562
            'Users of group are not members of any siblings'
563
        );
564
        $this->assertFalse(
565
            $staffmember->inGroup('does-not-exist'),
566
            'Non-existant group returns false'
567
        );
568
    }
569
570
    /**
571
     * Tests that the user is able to view their own record, and in turn, they can
572
     * edit and delete their own record too.
573
     */
574
    public function testCanManipulateOwnRecord()
575
    {
576
        $member = $this->objFromFixture(Member::class, 'test');
577
        $member2 = $this->objFromFixture(Member::class, 'staffmember');
578
579
        /* Not logged in, you can't view, delete or edit the record */
580
        $this->assertFalse($member->canView());
581
        $this->assertFalse($member->canDelete());
582
        $this->assertFalse($member->canEdit());
583
584
        /* Logged in users can edit their own record */
585
        $this->logInAs($member);
586
        $this->assertTrue($member->canView());
587
        $this->assertFalse($member->canDelete());
588
        $this->assertTrue($member->canEdit());
589
590
        /* Other uses cannot view, delete or edit others records */
591
        $this->logInAs($member2);
592
        $this->assertFalse($member->canView());
593
        $this->assertFalse($member->canDelete());
594
        $this->assertFalse($member->canEdit());
595
596
        $this->logOut();
597
    }
598
599
    public function testAuthorisedMembersCanManipulateOthersRecords()
600
    {
601
        $member = $this->objFromFixture(Member::class, 'test');
602
        $member2 = $this->objFromFixture(Member::class, 'staffmember');
603
604
        /* Group members with SecurityAdmin permissions can manipulate other records */
605
        $this->logInAs($member);
606
        $this->assertTrue($member2->canView());
607
        $this->assertTrue($member2->canDelete());
608
        $this->assertTrue($member2->canEdit());
609
610
        $this->logOut();
611
    }
612
613
    public function testExtendedCan()
614
    {
615
        $member = $this->objFromFixture(Member::class, 'test');
616
617
        /* Normal behaviour is that you can't view a member unless canView() on an extension returns true */
618
        $this->assertFalse($member->canView());
619
        $this->assertFalse($member->canDelete());
620
        $this->assertFalse($member->canEdit());
621
622
        /* Apply a extension that allows viewing in any case (most likely the case for member profiles) */
623
        Member::add_extension(MemberTest\ViewingAllowedExtension::class);
624
        $member2 = $this->objFromFixture(Member::class, 'staffmember');
625
626
        $this->assertTrue($member2->canView());
627
        $this->assertFalse($member2->canDelete());
628
        $this->assertFalse($member2->canEdit());
629
630
        /* Apply a extension that denies viewing of the Member */
631
        Member::remove_extension(MemberTest\ViewingAllowedExtension::class);
632
        Member::add_extension(MemberTest\ViewingDeniedExtension::class);
633
        $member3 = $this->objFromFixture(Member::class, 'managementmember');
634
635
        $this->assertFalse($member3->canView());
636
        $this->assertFalse($member3->canDelete());
637
        $this->assertFalse($member3->canEdit());
638
639
        /* Apply a extension that allows viewing and editing but denies deletion */
640
        Member::remove_extension(MemberTest\ViewingDeniedExtension::class);
641
        Member::add_extension(MemberTest\EditingAllowedDeletingDeniedExtension::class);
642
        $member4 = $this->objFromFixture(Member::class, 'accountingmember');
643
644
        $this->assertTrue($member4->canView());
645
        $this->assertFalse($member4->canDelete());
646
        $this->assertTrue($member4->canEdit());
647
648
        Member::remove_extension(MemberTest\EditingAllowedDeletingDeniedExtension::class);
649
    }
650
651
    /**
652
     * Tests for {@link Member::getName()} and {@link Member::setName()}
653
     */
654
    public function testName()
655
    {
656
        /** @var Member $member */
657
        $member = $this->objFromFixture(Member::class, 'test');
658
        $member->setName('Test Some User');
659
        $this->assertEquals('Test Some User', $member->getName());
660
        $member->setName('Test');
661
        $this->assertEquals('Test', $member->getName());
662
        $member->FirstName = 'Test';
663
        $member->Surname = '';
664
        $this->assertEquals('Test', $member->getName());
665
    }
666
667
    public function testMembersWithSecurityAdminAccessCantEditAdminsUnlessTheyreAdminsThemselves()
668
    {
669
        $adminMember = $this->objFromFixture(Member::class, 'admin');
670
        $otherAdminMember = $this->objFromFixture(Member::class, 'other-admin');
671
        $securityAdminMember = $this->objFromFixture(Member::class, 'test');
672
        $ceoMember = $this->objFromFixture(Member::class, 'ceomember');
673
674
        // Careful: Don't read as english language.
675
        // More precisely this should read canBeEditedBy()
676
677
        $this->assertTrue($adminMember->canEdit($adminMember), 'Admins can edit themselves');
678
        $this->assertTrue($otherAdminMember->canEdit($adminMember), 'Admins can edit other admins');
679
        $this->assertTrue($securityAdminMember->canEdit($adminMember), 'Admins can edit other members');
680
681
        $this->assertTrue($securityAdminMember->canEdit($securityAdminMember), 'Security-Admins can edit themselves');
682
        $this->assertFalse($adminMember->canEdit($securityAdminMember), 'Security-Admins can not edit other admins');
683
        $this->assertTrue($ceoMember->canEdit($securityAdminMember), 'Security-Admins can edit other members');
684
    }
685
686
    public function testOnChangeGroups()
687
    {
688
        /** @var Group $staffGroup */
689
        $staffGroup = $this->objFromFixture(Group::class, 'staffgroup');
690
        /** @var Member $staffMember */
691
        $staffMember = $this->objFromFixture(Member::class, 'staffmember');
692
        /** @var Member $adminMember */
693
        $adminMember = $this->objFromFixture(Member::class, 'admin');
694
695
        // Construct admin and non-admin gruops
696
        $newAdminGroup = new Group(array('Title' => 'newadmin'));
697
        $newAdminGroup->write();
698
        Permission::grant($newAdminGroup->ID, 'ADMIN');
699
        $newOtherGroup = new Group(array('Title' => 'othergroup'));
700
        $newOtherGroup->write();
701
702
        $this->assertTrue(
703
            $staffMember->onChangeGroups(array($staffGroup->ID)),
704
            'Adding existing non-admin group relation is allowed for non-admin members'
705
        );
706
        $this->assertTrue(
707
            $staffMember->onChangeGroups(array($newOtherGroup->ID)),
708
            'Adding new non-admin group relation is allowed for non-admin members'
709
        );
710
        $this->assertFalse(
711
            $staffMember->onChangeGroups(array($newAdminGroup->ID)),
712
            'Adding new admin group relation is not allowed for non-admin members'
713
        );
714
715
        $this->logInAs($adminMember);
716
        $this->assertTrue(
717
            $staffMember->onChangeGroups(array($newAdminGroup->ID)),
718
            'Adding new admin group relation is allowed for normal users, when granter is logged in as admin'
719
        );
720
        $this->logOut();
721
722
        $this->assertTrue(
723
            $adminMember->onChangeGroups(array($newAdminGroup->ID)),
724
            'Adding new admin group relation is allowed for admin members'
725
        );
726
    }
727
728
    /**
729
     * Ensure DirectGroups listbox disallows admin-promotion
730
     */
731
    public function testAllowedGroupsListbox()
732
    {
733
        /** @var Group $adminGroup */
734
        $adminGroup = $this->objFromFixture(Group::class, 'admingroup');
735
        /** @var Member $staffMember */
736
        $staffMember = $this->objFromFixture(Member::class, 'staffmember');
737
        /** @var Member $adminMember */
738
        $adminMember = $this->objFromFixture(Member::class, 'admin');
739
740
        // Ensure you can see the DirectGroups box
741
        $this->logInWithPermission('EDIT_PERMISSIONS');
742
743
        // Non-admin member field contains non-admin groups
744
        /** @var ListboxField $staffListbox */
745
        $staffListbox = $staffMember->getCMSFields()->dataFieldByName('DirectGroups');
746
        $this->assertArrayNotHasKey($adminGroup->ID, $staffListbox->getSource());
747
748
        // admin member field contains admin group
749
        /** @var ListboxField $adminListbox */
750
        $adminListbox = $adminMember->getCMSFields()->dataFieldByName('DirectGroups');
751
        $this->assertArrayHasKey($adminGroup->ID, $adminListbox->getSource());
752
753
        // If logged in as admin, staff listbox has admin group
754
        $this->logInWithPermission('ADMIN');
755
        $staffListbox = $staffMember->getCMSFields()->dataFieldByName('DirectGroups');
756
        $this->assertArrayHasKey($adminGroup->ID, $staffListbox->getSource());
0 ignored issues
show
Bug introduced by
The method getSource() does not exist on SilverStripe\Forms\FormField. 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

756
        $this->assertArrayHasKey($adminGroup->ID, $staffListbox->/** @scrutinizer ignore-call */ getSource());
Loading history...
757
    }
758
759
    /**
760
     * Test Member_GroupSet::add
761
     */
762
    public function testOnChangeGroupsByAdd()
763
    {
764
        /** @var Member $staffMember */
765
        $staffMember = $this->objFromFixture(Member::class, 'staffmember');
766
        /** @var Member $adminMember */
767
        $adminMember = $this->objFromFixture(Member::class, 'admin');
768
769
        // Setup new admin group
770
        $newAdminGroup = new Group(array('Title' => 'newadmin'));
771
        $newAdminGroup->write();
772
        Permission::grant($newAdminGroup->ID, 'ADMIN');
773
774
        // Setup non-admin group
775
        $newOtherGroup = new Group(array('Title' => 'othergroup'));
776
        $newOtherGroup->write();
777
778
        // Test staff can be added to other group
779
        $this->assertFalse($staffMember->inGroup($newOtherGroup));
780
        $staffMember->Groups()->add($newOtherGroup);
781
        $this->assertTrue(
782
            $staffMember->inGroup($newOtherGroup),
783
            'Adding new non-admin group relation is allowed for non-admin members'
784
        );
785
786
        // Test staff member can't be added to admin groups
787
        $this->assertFalse($staffMember->inGroup($newAdminGroup));
788
        $staffMember->Groups()->add($newAdminGroup);
789
        $this->assertFalse(
790
            $staffMember->inGroup($newAdminGroup),
791
            'Adding new admin group relation is not allowed for non-admin members'
792
        );
793
794
        // Test staff member can be added to admin group by admins
795
        $this->logInAs($adminMember);
796
        $staffMember->Groups()->add($newAdminGroup);
797
        $this->assertTrue(
798
            $staffMember->inGroup($newAdminGroup),
799
            'Adding new admin group relation is allowed for normal users, when granter is logged in as admin'
800
        );
801
802
        // Test staff member can be added if they are already admin
803
        $this->logOut();
804
        $this->assertFalse($adminMember->inGroup($newAdminGroup));
805
        $adminMember->Groups()->add($newAdminGroup);
806
        $this->assertTrue(
807
            $adminMember->inGroup($newAdminGroup),
808
            'Adding new admin group relation is allowed for admin members'
809
        );
810
    }
811
812
    /**
813
     * Test Member_GroupSet::add
814
     */
815
    public function testOnChangeGroupsBySetIDList()
816
    {
817
        /** @var Member $staffMember */
818
        $staffMember = $this->objFromFixture(Member::class, 'staffmember');
819
820
        // Setup new admin group
821
        $newAdminGroup = new Group(array('Title' => 'newadmin'));
822
        $newAdminGroup->write();
823
        Permission::grant($newAdminGroup->ID, 'ADMIN');
824
825
        // Test staff member can't be added to admin groups
826
        $this->assertFalse($staffMember->inGroup($newAdminGroup));
827
        $staffMember->Groups()->setByIDList(array($newAdminGroup->ID));
828
        $this->assertFalse(
829
            $staffMember->inGroup($newAdminGroup),
830
            'Adding new admin group relation is not allowed for non-admin members'
831
        );
832
    }
833
834
    /**
835
     * Test that extensions using updateCMSFields() are applied correctly
836
     */
837
    public function testUpdateCMSFields()
838
    {
839
        Member::add_extension(FieldsExtension::class);
840
841
        $member = Member::singleton();
842
        $fields = $member->getCMSFields();
843
844
        /**
845
 * @skipUpgrade
846
*/
847
        $this->assertNotNull($fields->dataFieldByName('Email'), 'Scaffolded fields are retained');
848
        $this->assertNull($fields->dataFieldByName('Salt'), 'Field modifications run correctly');
849
        $this->assertNotNull($fields->dataFieldByName('TestMemberField'), 'Extension is applied correctly');
850
851
        Member::remove_extension(FieldsExtension::class);
852
    }
853
854
    /**
855
     * Test that all members are returned
856
     */
857
    public function testMap_in_groupsReturnsAll()
858
    {
859
        $members = Member::map_in_groups();
860
        $this->assertEquals(13, $members->count(), 'There are 12 members in the mock plus a fake admin');
861
    }
862
863
    /**
864
     * Test that only admin members are returned
865
     */
866
    public function testMap_in_groupsReturnsAdmins()
867
    {
868
        $adminID = $this->objFromFixture(Group::class, 'admingroup')->ID;
869
        $members = Member::map_in_groups($adminID)->toArray();
870
871
        $admin = $this->objFromFixture(Member::class, 'admin');
872
        $otherAdmin = $this->objFromFixture(Member::class, 'other-admin');
873
874
        $this->assertTrue(
875
            in_array($admin->getTitle(), $members),
876
            $admin->getTitle() . ' should be in the returned list.'
877
        );
878
        $this->assertTrue(
879
            in_array($otherAdmin->getTitle(), $members),
880
            $otherAdmin->getTitle() . ' should be in the returned list.'
881
        );
882
        $this->assertEquals(2, count($members), 'There should be 2 members from the admin group');
883
    }
884
885
    /**
886
     * Add the given array of member extensions as class names.
887
     * This is useful for re-adding extensions after being removed
888
     * in a test case to produce an unbiased test.
889
     *
890
     * @param  array $extensions
891
     * @return array The added extensions
892
     */
893
    protected function addExtensions($extensions)
894
    {
895
        if ($extensions) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $extensions of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
896
            foreach ($extensions as $extension) {
897
                Member::add_extension($extension);
898
            }
899
        }
900
        return $extensions;
901
    }
902
903
    /**
904
     * Remove given extensions from Member. This is useful for
905
     * removing extensions that could produce a biased
906
     * test result, as some extensions applied by project
907
     * code or modules can do this.
908
     *
909
     * @param  array $extensions
910
     * @return array The removed extensions
911
     */
912
    protected function removeExtensions($extensions)
913
    {
914
        if ($extensions) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $extensions of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
915
            foreach ($extensions as $extension) {
916
                Member::remove_extension($extension);
917
            }
918
        }
919
        return $extensions;
920
    }
921
922
    public function testGenerateAutologinTokenAndStoreHash()
923
    {
924
        $m = new Member();
925
        $m->write();
926
927
        $token = $m->generateAutologinTokenAndStoreHash();
928
929
        $this->assertEquals($m->encryptWithUserSettings($token), $m->AutoLoginHash, 'Stores the token as a hash.');
930
    }
931
932
    public function testValidateAutoLoginToken()
933
    {
934
        $enc = new PasswordEncryptor_Blowfish();
0 ignored issues
show
Unused Code introduced by
The assignment to $enc is dead and can be removed.
Loading history...
935
936
        $m1 = new Member();
937
        $m1->write();
938
        $m1Token = $m1->generateAutologinTokenAndStoreHash();
939
940
        $m2 = new Member();
941
        $m2->write();
942
        $m2->generateAutologinTokenAndStoreHash();
943
944
        $this->assertTrue($m1->validateAutoLoginToken($m1Token), 'Passes token validity test against matching member.');
945
        $this->assertFalse($m2->validateAutoLoginToken($m1Token), 'Fails token validity test against other member.');
946
    }
947
948
    public function testRememberMeHashGeneration()
949
    {
950
        /** @var Member $m1 */
951
        $m1 = $this->objFromFixture(Member::class, 'grouplessmember');
952
953
        Injector::inst()->get(IdentityStore::class)->logIn($m1, true);
954
955
        $hashes = RememberLoginHash::get()->filter('MemberID', $m1->ID);
956
        $this->assertEquals($hashes->count(), 1);
957
        /** @var RememberLoginHash $firstHash */
958
        $firstHash = $hashes->first();
959
        $this->assertNotNull($firstHash->DeviceID);
960
        $this->assertNotNull($firstHash->Hash);
961
    }
962
963
    public function testRememberMeHashAutologin()
964
    {
965
        /** @var Member $m1 */
966
        $m1 = $this->objFromFixture(Member::class, 'noexpiry');
967
968
        Injector::inst()->get(IdentityStore::class)->logIn($m1, true);
969
970
        /** @var RememberLoginHash $firstHash */
971
        $firstHash = RememberLoginHash::get()->filter('MemberID', $m1->ID)->first();
972
        $this->assertNotNull($firstHash);
973
974
        // re-generates the hash so we can get the token
975
        $firstHash->Hash = $firstHash->getNewHash($m1);
976
        $token = $firstHash->getToken();
977
        $firstHash->write();
978
979
        $response = $this->get(
980
            'Security/login',
981
            $this->session(),
982
            null,
983
            array(
984
                'alc_enc' => $m1->ID . ':' . $token,
985
                'alc_device' => $firstHash->DeviceID
986
            )
987
        );
988
        $message = Convert::raw2xml(
989
            _t(
990
                'SilverStripe\\Security\\Member.LOGGEDINAS',
991
                "You're logged in as {name}.",
992
                array('name' => $m1->FirstName)
993
            )
994
        );
995
        $this->assertContains($message, $response->getBody());
996
997
        $this->logOut();
998
999
        // A wrong token or a wrong device ID should not let us autologin
1000
        $response = $this->get(
1001
            'Security/login',
1002
            $this->session(),
1003
            null,
1004
            array(
1005
                'alc_enc' => $m1->ID . ':asdfasd' . str_rot13($token),
1006
                'alc_device' => $firstHash->DeviceID
1007
            )
1008
        );
1009
        $this->assertNotContains($message, $response->getBody());
1010
1011
        $response = $this->get(
1012
            'Security/login',
1013
            $this->session(),
1014
            null,
1015
            array(
1016
                'alc_enc' => $m1->ID . ':' . $token,
1017
                'alc_device' => str_rot13($firstHash->DeviceID)
1018
            )
1019
        );
1020
        $this->assertNotContains($message, $response->getBody());
1021
1022
        // Re-logging (ie 'alc_enc' has expired), and not checking the "Remember Me" option
1023
        // should remove all previous hashes for this device
1024
        $response = $this->post(
1025
            'Security/login/default/LoginForm',
1026
            array(
1027
                'Email' => $m1->Email,
1028
                'Password' => '1nitialPassword',
1029
                'action_doLogin' => 'action_doLogin'
1030
            ),
1031
            null,
1032
            $this->session(),
1033
            null,
1034
            array(
1035
                'alc_device' => $firstHash->DeviceID
1036
            )
1037
        );
1038
        $this->assertContains($message, $response->getBody());
1039
        $this->assertEquals(RememberLoginHash::get()->filter('MemberID', $m1->ID)->count(), 0);
1040
    }
1041
1042
    public function testExpiredRememberMeHashAutologin()
1043
    {
1044
        /** @var Member $m1 */
1045
        $m1 = $this->objFromFixture(Member::class, 'noexpiry');
1046
        Injector::inst()->get(IdentityStore::class)->logIn($m1, true);
1047
        /** @var RememberLoginHash $firstHash */
1048
        $firstHash = RememberLoginHash::get()->filter('MemberID', $m1->ID)->first();
1049
        $this->assertNotNull($firstHash);
1050
1051
        // re-generates the hash so we can get the token
1052
        $firstHash->Hash = $firstHash->getNewHash($m1);
1053
        $token = $firstHash->getToken();
1054
        $firstHash->ExpiryDate = '2000-01-01 00:00:00';
1055
        $firstHash->write();
1056
1057
        DBDatetime::set_mock_now('1999-12-31 23:59:59');
1058
1059
        $response = $this->get(
1060
            'Security/login',
1061
            $this->session(),
1062
            null,
1063
            array(
1064
                'alc_enc' => $m1->ID . ':' . $token,
1065
                'alc_device' => $firstHash->DeviceID
1066
            )
1067
        );
1068
        $message = Convert::raw2xml(
1069
            _t(
1070
                'SilverStripe\\Security\\Member.LOGGEDINAS',
1071
                "You're logged in as {name}.",
1072
                array('name' => $m1->FirstName)
1073
            )
1074
        );
1075
        $this->assertContains($message, $response->getBody());
1076
1077
        $this->logOut();
1078
1079
        // re-generates the hash so we can get the token
1080
        $firstHash->Hash = $firstHash->getNewHash($m1);
1081
        $token = $firstHash->getToken();
1082
        $firstHash->ExpiryDate = '2000-01-01 00:00:00';
1083
        $firstHash->write();
1084
1085
        DBDatetime::set_mock_now('2000-01-01 00:00:01');
1086
1087
        $response = $this->get(
1088
            'Security/login',
1089
            $this->session(),
1090
            null,
1091
            array(
1092
                'alc_enc' => $m1->ID . ':' . $token,
1093
                'alc_device' => $firstHash->DeviceID
1094
            )
1095
        );
1096
        $this->assertNotContains($message, $response->getBody());
1097
        $this->logOut();
1098
        DBDatetime::clear_mock_now();
1099
    }
1100
1101
    public function testRememberMeMultipleDevices()
1102
    {
1103
        /** @var Member $m1 */
1104
        $m1 = $this->objFromFixture(Member::class, 'noexpiry');
1105
1106
        // First device
1107
        Injector::inst()->get(IdentityStore::class)->logIn($m1, true);
1108
        Cookie::set('alc_device', null);
1109
        // Second device
1110
        Injector::inst()->get(IdentityStore::class)->logIn($m1, true);
1111
1112
        // Hash of first device
1113
        /** @var RememberLoginHash $firstHash */
1114
        $firstHash = RememberLoginHash::get()->filter('MemberID', $m1->ID)->first();
1115
        $this->assertNotNull($firstHash);
1116
1117
        // Hash of second device
1118
        /** @var RememberLoginHash $secondHash */
1119
        $secondHash = RememberLoginHash::get()->filter('MemberID', $m1->ID)->last();
1120
        $this->assertNotNull($secondHash);
1121
1122
        // DeviceIDs are different
1123
        $this->assertNotEquals($firstHash->DeviceID, $secondHash->DeviceID);
1124
1125
        // re-generates the hashes so we can get the tokens
1126
        $firstHash->Hash = $firstHash->getNewHash($m1);
1127
        $firstToken = $firstHash->getToken();
1128
        $firstHash->write();
1129
1130
        $secondHash->Hash = $secondHash->getNewHash($m1);
1131
        $secondToken = $secondHash->getToken();
1132
        $secondHash->write();
1133
1134
        // Accessing the login page should show the user's name straight away
1135
        $response = $this->get(
1136
            'Security/login',
1137
            $this->session(),
1138
            null,
1139
            array(
1140
                'alc_enc' => $m1->ID . ':' . $firstToken,
1141
                'alc_device' => $firstHash->DeviceID
1142
            )
1143
        );
1144
        $message = Convert::raw2xml(
1145
            _t(
1146
                'SilverStripe\\Security\\Member.LOGGEDINAS',
1147
                "You're logged in as {name}.",
1148
                array('name' => $m1->FirstName)
1149
            )
1150
        );
1151
        $this->assertContains($message, $response->getBody());
1152
1153
        // Test that removing session but not cookie keeps user
1154
        /** @var SessionAuthenticationHandler $sessionHandler */
1155
        $sessionHandler = Injector::inst()->get(SessionAuthenticationHandler::class);
1156
        $sessionHandler->logOut();
1157
        Security::setCurrentUser(null);
1158
1159
        // Accessing the login page from the second device
1160
        $response = $this->get(
1161
            'Security/login',
1162
            $this->session(),
1163
            null,
1164
            array(
1165
                'alc_enc' => $m1->ID . ':' . $secondToken,
1166
                'alc_device' => $secondHash->DeviceID
1167
            )
1168
        );
1169
        $this->assertContains($message, $response->getBody());
1170
1171
        // Logging out from the second device - only one device being logged out
1172
        RememberLoginHash::config()->update('logout_across_devices', false);
1173
        $this->get(
1174
            'Security/logout',
1175
            $this->session(),
1176
            null,
1177
            array(
1178
                'alc_enc' => $m1->ID . ':' . $secondToken,
1179
                'alc_device' => $secondHash->DeviceID
1180
            )
1181
        );
1182
        $this->assertEquals(
1183
            RememberLoginHash::get()->filter(array('MemberID'=>$m1->ID, 'DeviceID'=>$firstHash->DeviceID))->count(),
1184
            1
1185
        );
1186
1187
        // Logging out from any device when all login hashes should be removed
1188
        RememberLoginHash::config()->update('logout_across_devices', true);
1189
        Injector::inst()->get(IdentityStore::class)->logIn($m1, true);
1190
        $this->get('Security/logout', $this->session());
1191
        $this->assertEquals(
1192
            RememberLoginHash::get()->filter('MemberID', $m1->ID)->count(),
1193
            0
1194
        );
1195
    }
1196
1197
    public function testCanDelete()
1198
    {
1199
        $admin1 = $this->objFromFixture(Member::class, 'admin');
1200
        $admin2 = $this->objFromFixture(Member::class, 'other-admin');
1201
        $member1 = $this->objFromFixture(Member::class, 'grouplessmember');
1202
        $member2 = $this->objFromFixture(Member::class, 'noformatmember');
1203
1204
        $this->assertTrue(
1205
            $admin1->canDelete($admin2),
1206
            'Admins can delete other admins'
1207
        );
1208
        $this->assertTrue(
1209
            $member1->canDelete($admin2),
1210
            'Admins can delete non-admins'
1211
        );
1212
        $this->assertFalse(
1213
            $admin1->canDelete($admin1),
1214
            'Admins can not delete themselves'
1215
        );
1216
        $this->assertFalse(
1217
            $member1->canDelete($member2),
1218
            'Non-admins can not delete other non-admins'
1219
        );
1220
        $this->assertFalse(
1221
            $member1->canDelete($member1),
1222
            'Non-admins can not delete themselves'
1223
        );
1224
    }
1225
1226
    public function testFailedLoginCount()
1227
    {
1228
        $maxFailedLoginsAllowed = 3;
1229
        //set up the config variables to enable login lockouts
1230
        Member::config()->update('lock_out_after_incorrect_logins', $maxFailedLoginsAllowed);
1231
1232
        /** @var Member $member */
1233
        $member = $this->objFromFixture(Member::class, 'test');
1234
        $failedLoginCount = $member->FailedLoginCount;
1235
1236
        for ($i = 1; $i < $maxFailedLoginsAllowed; ++$i) {
1237
            $member->registerFailedLogin();
1238
1239
            $this->assertEquals(
1240
                ++$failedLoginCount,
1241
                $member->FailedLoginCount,
1242
                'Failed to increment $member->FailedLoginCount'
1243
            );
1244
1245
            $this->assertTrue(
1246
                $member->canLogin(),
1247
                "Member has been locked out too early"
1248
            );
1249
        }
1250
    }
1251
1252
    public function testMemberValidator()
1253
    {
1254
        // clear custom requirements for this test
1255
        Member_Validator::config()->update('customRequired', null);
1256
        /** @var Member $memberA */
1257
        $memberA = $this->objFromFixture(Member::class, 'admin');
1258
        /** @var Member $memberB */
1259
        $memberB = $this->objFromFixture(Member::class, 'test');
1260
1261
        // create a blank form
1262
        $form = new MemberTest\ValidatorForm();
1263
1264
        $validator = new Member_Validator();
1265
        $validator->setForm($form);
1266
1267
        // Simulate creation of a new member via form, but use an existing member identifier
1268
        $fail = $validator->php(
1269
            array(
1270
            'FirstName' => 'Test',
1271
            'Email' => $memberA->Email
1272
            )
1273
        );
1274
1275
        $this->assertFalse(
1276
            $fail,
1277
            'Member_Validator must fail when trying to create new Member with existing Email.'
1278
        );
1279
1280
        // populate the form with values from another member
1281
        $form->loadDataFrom($memberB);
1282
1283
        // Assign the validator to an existing member
1284
        // (this is basically the same as passing the member ID with the form data)
1285
        $validator->setForMember($memberB);
1286
1287
        // Simulate update of a member via form and use an existing member Email
1288
        $fail = $validator->php(
1289
            array(
1290
            'FirstName' => 'Test',
1291
            'Email' => $memberA->Email
1292
            )
1293
        );
1294
1295
        // Simulate update to a new Email address
1296
        $pass1 = $validator->php(
1297
            array(
1298
            'FirstName' => 'Test',
1299
            'Email' => '[email protected]'
1300
            )
1301
        );
1302
1303
        // Pass in the same Email address that the member already has. Ensure that case is valid
1304
        $pass2 = $validator->php(
1305
            array(
1306
            'FirstName' => 'Test',
1307
            'Surname' => 'User',
1308
            'Email' => $memberB->Email
1309
            )
1310
        );
1311
1312
        $this->assertFalse(
1313
            $fail,
1314
            'Member_Validator must fail when trying to update existing member with existing Email.'
1315
        );
1316
1317
        $this->assertTrue(
1318
            $pass1,
1319
            'Member_Validator must pass when Email is updated to a value that\'s not in use.'
1320
        );
1321
1322
        $this->assertTrue(
1323
            $pass2,
1324
            'Member_Validator must pass when Member updates his own Email to the already existing value.'
1325
        );
1326
    }
1327
1328
    public function testMemberValidatorWithExtensions()
1329
    {
1330
        // clear custom requirements for this test
1331
        Member_Validator::config()->update('customRequired', null);
1332
1333
        // create a blank form
1334
        $form = new MemberTest\ValidatorForm();
1335
1336
        // Test extensions
1337
        Member_Validator::add_extension(MemberTest\SurnameMustMatchFirstNameExtension::class);
1338
        $validator = new Member_Validator();
1339
        $validator->setForm($form);
1340
1341
        // This test should fail, since the extension enforces FirstName == Surname
1342
        $fail = $validator->php(
1343
            array(
1344
            'FirstName' => 'Test',
1345
            'Surname' => 'User',
1346
            'Email' => '[email protected]'
1347
            )
1348
        );
1349
1350
        $pass = $validator->php(
1351
            array(
1352
            'FirstName' => 'Test',
1353
            'Surname' => 'Test',
1354
            'Email' => '[email protected]'
1355
            )
1356
        );
1357
1358
        $this->assertFalse(
1359
            $fail,
1360
            'Member_Validator must fail because of added extension.'
1361
        );
1362
1363
        $this->assertTrue(
1364
            $pass,
1365
            'Member_Validator must succeed, since it meets all requirements.'
1366
        );
1367
1368
        // Add another extension that always fails. This ensures that all extensions are considered in the validation
1369
        Member_Validator::add_extension(MemberTest\AlwaysFailExtension::class);
1370
        $validator = new Member_Validator();
1371
        $validator->setForm($form);
1372
1373
        // Even though the data is valid, This test should still fail, since one extension always returns false
1374
        $fail = $validator->php(
1375
            array(
1376
            'FirstName' => 'Test',
1377
            'Surname' => 'Test',
1378
            'Email' => '[email protected]'
1379
            )
1380
        );
1381
1382
        $this->assertFalse(
1383
            $fail,
1384
            'Member_Validator must fail because of added extensions.'
1385
        );
1386
1387
        // Remove added extensions
1388
        Member_Validator::remove_extension(MemberTest\AlwaysFailExtension::class);
1389
        Member_Validator::remove_extension(MemberTest\SurnameMustMatchFirstNameExtension::class);
1390
    }
1391
1392
    public function testCustomMemberValidator()
1393
    {
1394
        // clear custom requirements for this test
1395
        Member_Validator::config()->update('customRequired', null);
1396
1397
        $member = $this->objFromFixture(Member::class, 'admin');
1398
1399
        $form = new MemberTest\ValidatorForm();
1400
        $form->loadDataFrom($member);
1401
1402
        $validator = new Member_Validator();
1403
        $validator->setForm($form);
1404
1405
        $pass = $validator->php(
1406
            array(
1407
            'FirstName' => 'Borris',
1408
            'Email' => '[email protected]'
1409
            )
1410
        );
1411
1412
        $fail = $validator->php(
1413
            array(
1414
            'Email' => '[email protected]',
1415
            'Surname' => ''
1416
            )
1417
        );
1418
1419
        $this->assertTrue($pass, 'Validator requires a FirstName and Email');
1420
        $this->assertFalse($fail, 'Missing FirstName');
1421
1422
        $ext = new MemberTest\ValidatorExtension();
1423
        $ext->updateValidator($validator);
1424
1425
        $pass = $validator->php(
1426
            array(
1427
            'FirstName' => 'Borris',
1428
            'Email' => '[email protected]'
1429
            )
1430
        );
1431
1432
        $fail = $validator->php(
1433
            array(
1434
            'Email' => '[email protected]'
1435
            )
1436
        );
1437
1438
        $this->assertFalse($pass, 'Missing surname');
1439
        $this->assertFalse($fail, 'Missing surname value');
1440
1441
        $fail = $validator->php(
1442
            array(
1443
            'Email' => '[email protected]',
1444
            'Surname' => 'Silverman'
1445
            )
1446
        );
1447
1448
        $this->assertTrue($fail, 'Passes with email and surname now (no firstname)');
1449
    }
1450
1451
    public function testCurrentUser()
1452
    {
1453
        $this->assertNull(Security::getCurrentUser());
1454
1455
        $adminMember = $this->objFromFixture(Member::class, 'admin');
1456
        $this->logInAs($adminMember);
1457
1458
        $userFromSession = Security::getCurrentUser();
1459
        $this->assertEquals($adminMember->ID, $userFromSession->ID);
1460
    }
1461
1462
    /**
1463
     * @covers \SilverStripe\Security\Member::actAs()
1464
     */
1465
    public function testActAsUserPermissions()
1466
    {
1467
        $this->assertNull(Security::getCurrentUser());
1468
1469
        /** @var Member $adminMember */
1470
        $adminMember = $this->objFromFixture(Member::class, 'admin');
1471
1472
        // Check acting as admin when not logged in
1473
        $checkAdmin = Member::actAs($adminMember, function () {
1474
            return Permission::check('ADMIN');
1475
        });
1476
        $this->assertTrue($checkAdmin);
1477
1478
        // Check nesting
1479
        $checkAdmin = Member::actAs($adminMember, function () {
1480
            return Member::actAs(null, function () {
1481
                return Permission::check('ADMIN');
1482
            });
1483
        });
1484
        $this->assertFalse($checkAdmin);
1485
1486
        // Check logging in as non-admin user
1487
        $this->logInWithPermission('TEST_PERMISSION');
1488
1489
        $hasPerm = Member::actAs(null, function () {
1490
            return Permission::check('TEST_PERMISSION');
1491
        });
1492
        $this->assertFalse($hasPerm);
1493
1494
        // Check permissions can be promoted
1495
        $checkAdmin = Member::actAs($adminMember, function () {
1496
            return Permission::check('ADMIN');
1497
        });
1498
        $this->assertTrue($checkAdmin);
1499
    }
1500
1501
    /**
1502
     * @covers \SilverStripe\Security\Member::actAs()
1503
     */
1504
    public function testActAsUser()
1505
    {
1506
        $this->assertNull(Security::getCurrentUser());
1507
1508
        /** @var Member $adminMember */
1509
        $adminMember = $this->objFromFixture(Member::class, 'admin');
1510
        $member = Member::actAs($adminMember, function () {
1511
            return Security::getCurrentUser();
1512
        });
1513
        $this->assertEquals($adminMember->ID, $member->ID);
1514
1515
        // Check nesting
1516
        $member = Member::actAs($adminMember, function () {
1517
            return Member::actAs(null, function () {
1518
                return Security::getCurrentUser();
1519
            });
1520
        });
1521
        $this->assertEmpty($member);
1522
    }
1523
1524
    public function testChangePasswordWithExtensionsThatModifyValidationResult()
1525
    {
1526
        // Default behaviour
1527
        /** @var Member $member */
1528
        $member = $this->objFromFixture(Member::class, 'admin');
1529
        $result = $member->changePassword('my-secret-new-password');
1530
        $this->assertInstanceOf(ValidationResult::class, $result);
1531
        $this->assertTrue($result->isValid());
1532
1533
        // With an extension added
1534
        Member::add_extension(MemberTest\ExtendedChangePasswordExtension::class);
1535
        $member = $this->objFromFixture(Member::class, 'admin');
1536
        $result = $member->changePassword('my-second-secret-password');
0 ignored issues
show
Bug introduced by
The method changePassword() does not exist on SilverStripe\ORM\DataObject. 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

1536
        /** @scrutinizer ignore-call */ 
1537
        $result = $member->changePassword('my-second-secret-password');
Loading history...
1537
        $this->assertInstanceOf(ValidationResult::class, $result);
1538
        $this->assertFalse($result->isValid());
1539
    }
1540
}
1541