silverstripe-framework/tests/security/MemberTest.php
Damian Mooyman 5c9044a007 API Enforce default_cast for all field usages
API Introduce HTMLFragment as casting helper for HTMLText with shortcodes disabled
API Introduce DBField::CDATA for XML file value encoding
API RSSFeed now casts from the underlying model rather than by override
API Introduce CustomMethods::getExtraMethodConfig() to allow metadata to be queried
BUG Remove _call hack from VirtualPage
API Remove FormField::$dontEscape
API Introduce HTMLReadonlyField for non-editable readonly HTML
API FormField::Field() now returns string in many cases rather than DBField instance.
API Remove redundant *_val methods from ViewableData
API ViewableData::obj() no longer has a $forceReturnObject parameter as it always returns an object
BUG  Fix issue with ViewableData caching incorrect field values after being modified.
API Remove deprecated DB class methods
API Enforce plain text left/right formfield titles
2016-07-13 17:15:45 +12:00

1414 lines
46 KiB
PHP

<?php
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\DB;
use SilverStripe\ORM\FieldType\DBDatetime;
use SilverStripe\ORM\DataExtension;
use SilverStripe\Security\Member;
use SilverStripe\Security\Security;
use SilverStripe\Security\MemberPassword;
use SilverStripe\Security\Group;
use SilverStripe\Security\Permission;
use SilverStripe\Security\PasswordEncryptor_Blowfish;
use SilverStripe\Security\RememberLoginHash;
use SilverStripe\Security\Member_Validator;
use SilverStripe\Security\PasswordValidator;
/**
* @package framework
* @subpackage tests
*/
class MemberTest extends FunctionalTest {
protected static $fixture_file = 'MemberTest.yml';
protected $orig = array();
protected $local = null;
protected $illegalExtensions = array(
'SilverStripe\\Security\\Member' => array(
// TODO Coupling with modules, this should be resolved by automatically
// removing all applied extensions before a unit test
'ForumRole',
'OpenIDAuthenticatedRole'
)
);
public function __construct() {
parent::__construct();
//Setting the locale has to happen in the constructor (using the setUp and tearDown methods doesn't work)
//This is because the test relies on the yaml file being interpreted according to a particular date format
//and this setup occurs before the setUp method is run
$this->local = i18n::default_locale();
i18n::set_default_locale('en_US');
}
public function __destruct() {
i18n::set_default_locale($this->local);
}
public function setUp() {
parent::setUp();
$this->orig['Member_unique_identifier_field'] = Member::config()->unique_identifier_field;
Member::config()->unique_identifier_field = 'Email';
Member::set_password_validator(null);
}
public function tearDown() {
Member::config()->unique_identifier_field = $this->orig['Member_unique_identifier_field'];
parent::tearDown();
}
/**
* @expectedException SilverStripe\ORM\ValidationException
*/
public function testWriteDoesntMergeNewRecordWithExistingMember() {
$m1 = new Member();
$m1->Email = 'member@test.com';
$m1->write();
$m2 = new Member();
$m2->Email = 'member@test.com';
$m2->write();
}
/**
* @expectedException SilverStripe\ORM\ValidationException
*/
public function testWriteDoesntMergeExistingMemberOnIdentifierChange() {
$m1 = new Member();
$m1->Email = 'member@test.com';
$m1->write();
$m2 = new Member();
$m2->Email = 'member_new@test.com';
$m2->write();
$m2->Email = 'member@test.com';
$m2->write();
}
public function testDefaultPasswordEncryptionOnMember() {
$memberWithPassword = new Member();
$memberWithPassword->Password = 'mypassword';
$memberWithPassword->write();
$this->assertEquals(
$memberWithPassword->PasswordEncryption,
Security::config()->password_encryption_algorithm,
'Password encryption is set for new member records on first write (with setting "Password")'
);
$memberNoPassword = new Member();
$memberNoPassword->write();
$this->assertNull(
$memberNoPassword->PasswordEncryption,
'Password encryption is not set for new member records on first write, when not setting a "Password")'
);
}
public function testDefaultPasswordEncryptionDoesntChangeExistingMembers() {
$member = new Member();
$member->Password = 'mypassword';
$member->PasswordEncryption = 'sha1_v2.4';
$member->write();
$origAlgo = Security::config()->password_encryption_algorithm;
Security::config()->password_encryption_algorithm = 'none';
$member->Password = 'mynewpassword';
$member->write();
$this->assertEquals(
$member->PasswordEncryption,
'sha1_v2.4'
);
$result = $member->checkPassword('mynewpassword');
$this->assertTrue($result->valid());
Security::config()->password_encryption_algorithm = $origAlgo;
}
public function testKeepsEncryptionOnEmptyPasswords() {
$member = new Member();
$member->Password = 'mypassword';
$member->PasswordEncryption = 'sha1_v2.4';
$member->write();
$member->Password = '';
$member->write();
$this->assertEquals(
$member->PasswordEncryption,
'sha1_v2.4'
);
$result = $member->checkPassword('');
$this->assertTrue($result->valid());
}
public function testSetPassword() {
$member = $this->objFromFixture('SilverStripe\\Security\\Member', 'test');
$member->Password = "test1";
$member->write();
$result = $member->checkPassword('test1');
$this->assertTrue($result->valid());
}
/**
* Test that password changes are logged properly
*/
public function testPasswordChangeLogging() {
$member = $this->objFromFixture('SilverStripe\\Security\\Member', 'test');
$this->assertNotNull($member);
$member->Password = "test1";
$member->write();
$member->Password = "test2";
$member->write();
$member->Password = "test3";
$member->write();
$passwords = DataObject::get("SilverStripe\\Security\\MemberPassword", "\"MemberID\" = $member->ID", "\"Created\" DESC, \"ID\" DESC")
->getIterator();
$this->assertNotNull($passwords);
$passwords->rewind();
$this->assertTrue($passwords->current()->checkPassword('test3'), "Password test3 not found in MemberRecord");
$passwords->next();
$this->assertTrue($passwords->current()->checkPassword('test2'), "Password test2 not found in MemberRecord");
$passwords->next();
$this->assertTrue($passwords->current()->checkPassword('test1'), "Password test1 not found in MemberRecord");
$passwords->next();
$this->assertInstanceOf('SilverStripe\\ORM\\DataObject', $passwords->current());
$this->assertTrue($passwords->current()->checkPassword('1nitialPassword'),
"Password 1nitialPassword not found in MemberRecord");
//check we don't retain orphaned records when a member is deleted
$member->delete();
$passwords = MemberPassword::get()->filter('MemberID', $member->OldID);
$this->assertCount(0, $passwords);
}
/**
* Test that changed passwords will send an email
*/
public function testChangedPasswordEmaling() {
Config::inst()->update('SilverStripe\\Security\\Member', 'notify_password_change', true);
$this->clearEmails();
$member = $this->objFromFixture('SilverStripe\\Security\\Member', 'test');
$this->assertNotNull($member);
$valid = $member->changePassword('32asDF##$$%%');
$this->assertTrue($valid->valid());
$this->assertEmailSent('testuser@example.com', null, 'Your password has been changed',
'/testuser@example\.com/');
}
/**
* Test that triggering "forgotPassword" sends an Email with a reset link
*/
public function testForgotPasswordEmaling() {
$this->clearEmails();
$this->autoFollowRedirection = false;
$member = $this->objFromFixture('SilverStripe\\Security\\Member', 'test');
$this->assertNotNull($member);
// Initiate a password-reset
$response = $this->post('Security/LostPasswordForm', array('Email' => $member->Email));
$this->assertEquals($response->getStatusCode(), 302);
// We should get redirected to Security/passwordsent
$this->assertContains('Security/passwordsent/testuser@example.com',
urldecode($response->getHeader('Location')));
// Check existance of reset link
$this->assertEmailSent("testuser@example.com", null, 'Your password reset link',
'/Security\/changepassword\?m='.$member->ID.'&amp;t=[^"]+/');
}
/**
* Test that passwords validate against NZ e-government guidelines
* - don't allow the use of the last 6 passwords
* - require at least 3 of lowercase, uppercase, digits and punctuation
* - at least 7 characters long
*/
public function testValidatePassword() {
$member = $this->objFromFixture('SilverStripe\\Security\\Member', 'test');
$this->assertNotNull($member);
Member::set_password_validator(new MemberTest_PasswordValidator());
// BAD PASSWORDS
$valid = $member->changePassword('shorty');
$this->assertFalse($valid->valid());
$this->assertContains("TOO_SHORT", $valid->codeList());
$valid = $member->changePassword('longone');
$this->assertNotContains("TOO_SHORT", $valid->codeList());
$this->assertContains("LOW_CHARACTER_STRENGTH", $valid->codeList());
$this->assertFalse($valid->valid());
$valid = $member->changePassword('w1thNumb3rs');
$this->assertNotContains("LOW_CHARACTER_STRENGTH", $valid->codeList());
$this->assertTrue($valid->valid());
// Clear out the MemberPassword table to ensure that the system functions properly in that situation
DB::query("DELETE FROM \"MemberPassword\"");
// GOOD PASSWORDS
$valid = $member->changePassword('withSym###Ls');
$this->assertNotContains("LOW_CHARACTER_STRENGTH", $valid->codeList());
$this->assertTrue($valid->valid());
$valid = $member->changePassword('withSym###Ls2');
$this->assertTrue($valid->valid());
$valid = $member->changePassword('withSym###Ls3');
$this->assertTrue($valid->valid());
$valid = $member->changePassword('withSym###Ls4');
$this->assertTrue($valid->valid());
$valid = $member->changePassword('withSym###Ls5');
$this->assertTrue($valid->valid());
$valid = $member->changePassword('withSym###Ls6');
$this->assertTrue($valid->valid());
$valid = $member->changePassword('withSym###Ls7');
$this->assertTrue($valid->valid());
// CAN'T USE PASSWORDS 2-7, but I can use pasword 1
$valid = $member->changePassword('withSym###Ls2');
$this->assertFalse($valid->valid());
$this->assertContains("PREVIOUS_PASSWORD", $valid->codeList());
$valid = $member->changePassword('withSym###Ls5');
$this->assertFalse($valid->valid());
$this->assertContains("PREVIOUS_PASSWORD", $valid->codeList());
$valid = $member->changePassword('withSym###Ls7');
$this->assertFalse($valid->valid());
$this->assertContains("PREVIOUS_PASSWORD", $valid->codeList());
$valid = $member->changePassword('withSym###Ls');
$this->assertTrue($valid->valid());
// HAVING DONE THAT, PASSWORD 2 is now available from the list
$valid = $member->changePassword('withSym###Ls2');
$this->assertTrue($valid->valid());
$valid = $member->changePassword('withSym###Ls3');
$this->assertTrue($valid->valid());
$valid = $member->changePassword('withSym###Ls4');
$this->assertTrue($valid->valid());
Member::set_password_validator(null);
}
/**
* Test that the PasswordExpiry date is set when passwords are changed
*/
public function testPasswordExpirySetting() {
Member::config()->password_expiry_days = 90;
$member = $this->objFromFixture('SilverStripe\\Security\\Member', 'test');
$this->assertNotNull($member);
$valid = $member->changePassword("Xx?1234234");
$this->assertTrue($valid->valid());
$expiryDate = date('Y-m-d', time() + 90*86400);
$this->assertEquals($expiryDate, $member->PasswordExpiry);
Member::config()->password_expiry_days = null;
$valid = $member->changePassword("Xx?1234235");
$this->assertTrue($valid->valid());
$this->assertNull($member->PasswordExpiry);
}
public function testIsPasswordExpired() {
$member = $this->objFromFixture('SilverStripe\\Security\\Member', 'test');
$this->assertNotNull($member);
$this->assertFalse($member->isPasswordExpired());
$member = $this->objFromFixture('SilverStripe\\Security\\Member', 'noexpiry');
$member->PasswordExpiry = null;
$this->assertFalse($member->isPasswordExpired());
$member = $this->objFromFixture('SilverStripe\\Security\\Member', 'expiredpassword');
$this->assertTrue($member->isPasswordExpired());
// Check the boundary conditions
// If PasswordExpiry == today, then it's expired
$member->PasswordExpiry = date('Y-m-d');
$this->assertTrue($member->isPasswordExpired());
// If PasswordExpiry == tomorrow, then it's not
$member->PasswordExpiry = date('Y-m-d', time() + 86400);
$this->assertFalse($member->isPasswordExpired());
}
public function testMemberWithNoDateFormatFallsbackToGlobalLocaleDefaultFormat() {
Config::inst()->update('i18n', 'date_format', 'yyyy-MM-dd');
Config::inst()->update('i18n', 'time_format', 'H:mm');
$member = $this->objFromFixture('SilverStripe\\Security\\Member', 'noformatmember');
$this->assertEquals('yyyy-MM-dd', $member->DateFormat);
$this->assertEquals('H:mm', $member->TimeFormat);
}
public function testInGroups() {
$staffmember = $this->objFromFixture('SilverStripe\\Security\\Member', 'staffmember');
$managementmember = $this->objFromFixture('SilverStripe\\Security\\Member', 'managementmember');
$accountingmember = $this->objFromFixture('SilverStripe\\Security\\Member', 'accountingmember');
$ceomember = $this->objFromFixture('SilverStripe\\Security\\Member', 'ceomember');
$staffgroup = $this->objFromFixture('SilverStripe\\Security\\Group', 'staffgroup');
$managementgroup = $this->objFromFixture('SilverStripe\\Security\\Group', 'managementgroup');
$accountinggroup = $this->objFromFixture('SilverStripe\\Security\\Group', 'accountinggroup');
$ceogroup = $this->objFromFixture('SilverStripe\\Security\\Group', 'ceogroup');
$this->assertTrue(
$staffmember->inGroups(array($staffgroup, $managementgroup)),
'inGroups() succeeds if a membership is detected on one of many passed groups'
);
$this->assertFalse(
$staffmember->inGroups(array($ceogroup, $managementgroup)),
'inGroups() fails if a membership is detected on none of the passed groups'
);
$this->assertFalse(
$ceomember->inGroups(array($staffgroup, $managementgroup), true),
'inGroups() fails if no direct membership is detected on any of the passed groups (in strict mode)'
);
}
public function testAddToGroupByCode() {
$grouplessMember = $this->objFromFixture('SilverStripe\\Security\\Member', 'grouplessmember');
$memberlessGroup = $this->objFromFixture('SilverStripe\\Security\\Group','memberlessgroup');
$this->assertFalse($grouplessMember->Groups()->exists());
$this->assertFalse($memberlessGroup->Members()->exists());
$grouplessMember->addToGroupByCode('memberless');
$this->assertEquals($memberlessGroup->Members()->Count(), 1);
$this->assertEquals($grouplessMember->Groups()->Count(), 1);
$grouplessMember->addToGroupByCode('somegroupthatwouldneverexist', 'New Group');
$this->assertEquals($grouplessMember->Groups()->Count(), 2);
$group = DataObject::get_one('SilverStripe\\Security\\Group', array(
'"Group"."Code"' => 'somegroupthatwouldneverexist'
));
$this->assertNotNull($group);
$this->assertEquals($group->Code, 'somegroupthatwouldneverexist');
$this->assertEquals($group->Title, 'New Group');
}
public function testRemoveFromGroupByCode() {
$grouplessMember = $this->objFromFixture('SilverStripe\\Security\\Member', 'grouplessmember');
$memberlessGroup = $this->objFromFixture('SilverStripe\\Security\\Group','memberlessgroup');
$this->assertFalse($grouplessMember->Groups()->exists());
$this->assertFalse($memberlessGroup->Members()->exists());
$grouplessMember->addToGroupByCode('memberless');
$this->assertEquals($memberlessGroup->Members()->Count(), 1);
$this->assertEquals($grouplessMember->Groups()->Count(), 1);
$grouplessMember->addToGroupByCode('somegroupthatwouldneverexist', 'New Group');
$this->assertEquals($grouplessMember->Groups()->Count(), 2);
$group = DataObject::get_one('SilverStripe\\Security\\Group', "\"Code\" = 'somegroupthatwouldneverexist'");
$this->assertNotNull($group);
$this->assertEquals($group->Code, 'somegroupthatwouldneverexist');
$this->assertEquals($group->Title, 'New Group');
$grouplessMember->removeFromGroupByCode('memberless');
$this->assertEquals($memberlessGroup->Members()->Count(), 0);
$this->assertEquals($grouplessMember->Groups()->Count(), 1);
$grouplessMember->removeFromGroupByCode('somegroupthatwouldneverexist');
$this->assertEquals($grouplessMember->Groups()->Count(), 0);
}
public function testInGroup() {
$staffmember = $this->objFromFixture('SilverStripe\\Security\\Member', 'staffmember');
$managementmember = $this->objFromFixture('SilverStripe\\Security\\Member', 'managementmember');
$accountingmember = $this->objFromFixture('SilverStripe\\Security\\Member', 'accountingmember');
$ceomember = $this->objFromFixture('SilverStripe\\Security\\Member', 'ceomember');
$staffgroup = $this->objFromFixture('SilverStripe\\Security\\Group', 'staffgroup');
$managementgroup = $this->objFromFixture('SilverStripe\\Security\\Group', 'managementgroup');
$accountinggroup = $this->objFromFixture('SilverStripe\\Security\\Group', 'accountinggroup');
$ceogroup = $this->objFromFixture('SilverStripe\\Security\\Group', 'ceogroup');
$this->assertTrue(
$staffmember->inGroup($staffgroup),
'Direct group membership is detected'
);
$this->assertTrue(
$managementmember->inGroup($staffgroup),
'Users of child group are members of a direct parent group (if not in strict mode)'
);
$this->assertTrue(
$accountingmember->inGroup($staffgroup),
'Users of child group are members of a direct parent group (if not in strict mode)'
);
$this->assertTrue(
$ceomember->inGroup($staffgroup),
'Users of indirect grandchild group are members of a parent group (if not in strict mode)'
);
$this->assertTrue(
$ceomember->inGroup($ceogroup, true),
'Direct group membership is dected (if in strict mode)'
);
$this->assertFalse(
$ceomember->inGroup($staffgroup, true),
'Users of child group are not members of a direct parent group (if in strict mode)'
);
$this->assertFalse(
$staffmember->inGroup($managementgroup),
'Users of parent group are not members of a direct child group'
);
$this->assertFalse(
$staffmember->inGroup($ceogroup),
'Users of parent group are not members of an indirect grandchild group'
);
$this->assertFalse(
$accountingmember->inGroup($managementgroup),
'Users of group are not members of any siblings'
);
$this->assertFalse(
$staffmember->inGroup('does-not-exist'),
'Non-existant group returns false'
);
}
/**
* Tests that the user is able to view their own record, and in turn, they can
* edit and delete their own record too.
*/
public function testCanManipulateOwnRecord() {
$extensions = $this->removeExtensions(Object::get_extensions('SilverStripe\\Security\\Member'));
$member = $this->objFromFixture('SilverStripe\\Security\\Member', 'test');
$member2 = $this->objFromFixture('SilverStripe\\Security\\Member', 'staffmember');
$this->session()->inst_set('loggedInAs', null);
/* Not logged in, you can't view, delete or edit the record */
$this->assertFalse($member->canView());
$this->assertFalse($member->canDelete());
$this->assertFalse($member->canEdit());
/* Logged in users can edit their own record */
$this->session()->inst_set('loggedInAs', $member->ID);
$this->assertTrue($member->canView());
$this->assertFalse($member->canDelete());
$this->assertTrue($member->canEdit());
/* Other uses cannot view, delete or edit others records */
$this->session()->inst_set('loggedInAs', $member2->ID);
$this->assertFalse($member->canView());
$this->assertFalse($member->canDelete());
$this->assertFalse($member->canEdit());
$this->addExtensions($extensions);
$this->session()->inst_set('loggedInAs', null);
}
public function testAuthorisedMembersCanManipulateOthersRecords() {
$extensions = $this->removeExtensions(Object::get_extensions('SilverStripe\\Security\\Member'));
$member = $this->objFromFixture('SilverStripe\\Security\\Member', 'test');
$member2 = $this->objFromFixture('SilverStripe\\Security\\Member', 'staffmember');
/* Group members with SecurityAdmin permissions can manipulate other records */
$this->session()->inst_set('loggedInAs', $member->ID);
$this->assertTrue($member2->canView());
$this->assertTrue($member2->canDelete());
$this->assertTrue($member2->canEdit());
$this->addExtensions($extensions);
$this->session()->inst_set('loggedInAs', null);
}
public function testExtendedCan() {
$extensions = $this->removeExtensions(Object::get_extensions('SilverStripe\\Security\\Member'));
$member = $this->objFromFixture('SilverStripe\\Security\\Member', 'test');
/* Normal behaviour is that you can't view a member unless canView() on an extension returns true */
$this->assertFalse($member->canView());
$this->assertFalse($member->canDelete());
$this->assertFalse($member->canEdit());
/* Apply a extension that allows viewing in any case (most likely the case for member profiles) */
Member::add_extension('MemberTest_ViewingAllowedExtension');
$member2 = $this->objFromFixture('SilverStripe\\Security\\Member', 'staffmember');
$this->assertTrue($member2->canView());
$this->assertFalse($member2->canDelete());
$this->assertFalse($member2->canEdit());
/* Apply a extension that denies viewing of the Member */
Member::remove_extension('MemberTest_ViewingAllowedExtension');
Member::add_extension('MemberTest_ViewingDeniedExtension');
$member3 = $this->objFromFixture('SilverStripe\\Security\\Member', 'managementmember');
$this->assertFalse($member3->canView());
$this->assertFalse($member3->canDelete());
$this->assertFalse($member3->canEdit());
/* Apply a extension that allows viewing and editing but denies deletion */
Member::remove_extension('MemberTest_ViewingDeniedExtension');
Member::add_extension('MemberTest_EditingAllowedDeletingDeniedExtension');
$member4 = $this->objFromFixture('SilverStripe\\Security\\Member', 'accountingmember');
$this->assertTrue($member4->canView());
$this->assertFalse($member4->canDelete());
$this->assertTrue($member4->canEdit());
Member::remove_extension('MemberTest_EditingAllowedDeletingDeniedExtension');
$this->addExtensions($extensions);
}
/**
* Tests for {@link Member::getName()} and {@link Member::setName()}
*/
public function testName() {
$member = $this->objFromFixture('SilverStripe\\Security\\Member', 'test');
$member->setName('Test Some User');
$this->assertEquals('Test Some User', $member->getName());
$member->setName('Test');
$this->assertEquals('Test', $member->getName());
$member->FirstName = 'Test';
$member->Surname = '';
$this->assertEquals('Test', $member->getName());
}
public function testMembersWithSecurityAdminAccessCantEditAdminsUnlessTheyreAdminsThemselves() {
$adminMember = $this->objFromFixture('SilverStripe\\Security\\Member', 'admin');
$otherAdminMember = $this->objFromFixture('SilverStripe\\Security\\Member', 'other-admin');
$securityAdminMember = $this->objFromFixture('SilverStripe\\Security\\Member', 'test');
$ceoMember = $this->objFromFixture('SilverStripe\\Security\\Member', 'ceomember');
// Careful: Don't read as english language.
// More precisely this should read canBeEditedBy()
$this->assertTrue($adminMember->canEdit($adminMember), 'Admins can edit themselves');
$this->assertTrue($otherAdminMember->canEdit($adminMember), 'Admins can edit other admins');
$this->assertTrue($securityAdminMember->canEdit($adminMember), 'Admins can edit other members');
$this->assertTrue($securityAdminMember->canEdit($securityAdminMember), 'Security-Admins can edit themselves');
$this->assertFalse($adminMember->canEdit($securityAdminMember), 'Security-Admins can not edit other admins');
$this->assertTrue($ceoMember->canEdit($securityAdminMember), 'Security-Admins can edit other members');
}
public function testOnChangeGroups() {
$staffGroup = $this->objFromFixture('SilverStripe\\Security\\Group', 'staffgroup');
$staffMember = $this->objFromFixture('SilverStripe\\Security\\Member', 'staffmember');
$adminMember = $this->objFromFixture('SilverStripe\\Security\\Member', 'admin');
$newAdminGroup = new Group(array('Title' => 'newadmin'));
$newAdminGroup->write();
Permission::grant($newAdminGroup->ID, 'ADMIN');
$newOtherGroup = new Group(array('Title' => 'othergroup'));
$newOtherGroup->write();
$this->assertTrue(
$staffMember->onChangeGroups(array($staffGroup->ID)),
'Adding existing non-admin group relation is allowed for non-admin members'
);
$this->assertTrue(
$staffMember->onChangeGroups(array($newOtherGroup->ID)),
'Adding new non-admin group relation is allowed for non-admin members'
);
$this->assertFalse(
$staffMember->onChangeGroups(array($newAdminGroup->ID)),
'Adding new admin group relation is not allowed for non-admin members'
);
$this->session()->inst_set('loggedInAs', $adminMember->ID);
$this->assertTrue(
$staffMember->onChangeGroups(array($newAdminGroup->ID)),
'Adding new admin group relation is allowed for normal users, when granter is logged in as admin'
);
$this->session()->inst_set('loggedInAs', null);
$this->assertTrue(
$adminMember->onChangeGroups(array($newAdminGroup->ID)),
'Adding new admin group relation is allowed for admin members'
);
}
/**
* Test Member_GroupSet::add
*/
public function testOnChangeGroupsByAdd() {
$staffMember = $this->objFromFixture('SilverStripe\\Security\\Member', 'staffmember');
$adminMember = $this->objFromFixture('SilverStripe\\Security\\Member', 'admin');
// Setup new admin group
$newAdminGroup = new Group(array('Title' => 'newadmin'));
$newAdminGroup->write();
Permission::grant($newAdminGroup->ID, 'ADMIN');
// Setup non-admin group
$newOtherGroup = new Group(array('Title' => 'othergroup'));
$newOtherGroup->write();
// Test staff can be added to other group
$this->assertFalse($staffMember->inGroup($newOtherGroup));
$staffMember->Groups()->add($newOtherGroup);
$this->assertTrue(
$staffMember->inGroup($newOtherGroup),
'Adding new non-admin group relation is allowed for non-admin members'
);
// Test staff member can't be added to admin groups
$this->assertFalse($staffMember->inGroup($newAdminGroup));
$staffMember->Groups()->add($newAdminGroup);
$this->assertFalse(
$staffMember->inGroup($newAdminGroup),
'Adding new admin group relation is not allowed for non-admin members'
);
// Test staff member can be added to admin group by admins
$this->logInAs($adminMember);
$staffMember->Groups()->add($newAdminGroup);
$this->assertTrue(
$staffMember->inGroup($newAdminGroup),
'Adding new admin group relation is allowed for normal users, when granter is logged in as admin'
);
// Test staff member can be added if they are already admin
$this->session()->inst_set('loggedInAs', null);
$this->assertFalse($adminMember->inGroup($newAdminGroup));
$adminMember->Groups()->add($newAdminGroup);
$this->assertTrue(
$adminMember->inGroup($newAdminGroup),
'Adding new admin group relation is allowed for admin members'
);
}
/**
* Test Member_GroupSet::add
*/
public function testOnChangeGroupsBySetIDList() {
$staffMember = $this->objFromFixture('SilverStripe\\Security\\Member', 'staffmember');
// Setup new admin group
$newAdminGroup = new Group(array('Title' => 'newadmin'));
$newAdminGroup->write();
Permission::grant($newAdminGroup->ID, 'ADMIN');
// Test staff member can't be added to admin groups
$this->assertFalse($staffMember->inGroup($newAdminGroup));
$staffMember->Groups()->setByIDList(array($newAdminGroup->ID));
$this->assertFalse(
$staffMember->inGroup($newAdminGroup),
'Adding new admin group relation is not allowed for non-admin members'
);
}
/**
* Test that extensions using updateCMSFields() are applied correctly
*/
public function testUpdateCMSFields() {
Member::add_extension('MemberTest_FieldsExtension');
$member = singleton('SilverStripe\\Security\\Member');
$fields = $member->getCMSFields();
$this->assertNotNull($fields->dataFieldByName('Email'), 'Scaffolded fields are retained');
$this->assertNull($fields->dataFieldByName('Salt'), 'Field modifications run correctly');
$this->assertNotNull($fields->dataFieldByName('TestMemberField'), 'Extension is applied correctly');
Member::remove_extension('MemberTest_FieldsExtension');
}
/**
* Test that all members are returned
*/
public function testMap_in_groupsReturnsAll() {
$members = Member::map_in_groups();
$this->assertEquals(13, $members->count(), 'There are 12 members in the mock plus a fake admin');
}
/**
* Test that only admin members are returned
*/
public function testMap_in_groupsReturnsAdmins() {
$adminID = $this->objFromFixture('SilverStripe\\Security\\Group', 'admingroup')->ID;
$members = Member::map_in_groups($adminID)->toArray();
$admin = $this->objFromFixture('SilverStripe\\Security\\Member', 'admin');
$otherAdmin = $this->objFromFixture('SilverStripe\\Security\\Member', 'other-admin');
$this->assertTrue(in_array($admin->getTitle(), $members),
$admin->getTitle().' should be in the returned list.');
$this->assertTrue(in_array($otherAdmin->getTitle(), $members),
$otherAdmin->getTitle().' should be in the returned list.');
$this->assertEquals(2, count($members), 'There should be 2 members from the admin group');
}
/**
* Add the given array of member extensions as class names.
* This is useful for re-adding extensions after being removed
* in a test case to produce an unbiased test.
*
* @param array $extensions
* @return array The added extensions
*/
protected function addExtensions($extensions) {
if($extensions) foreach($extensions as $extension) {
Member::add_extension($extension);
}
return $extensions;
}
/**
* Remove given extensions from Member. This is useful for
* removing extensions that could produce a biased
* test result, as some extensions applied by project
* code or modules can do this.
*
* @param array $extensions
* @return array The removed extensions
*/
protected function removeExtensions($extensions) {
if($extensions) foreach($extensions as $extension) {
Member::remove_extension($extension);
}
return $extensions;
}
public function testGenerateAutologinTokenAndStoreHash() {
$enc = new PasswordEncryptor_Blowfish();
$m = new Member();
$m->PasswordEncryption = 'blowfish';
$m->Salt = $enc->salt('123');
$token = $m->generateAutologinTokenAndStoreHash();
$this->assertEquals($m->encryptWithUserSettings($token), $m->AutoLoginHash, 'Stores the token as ahash.');
}
public function testValidateAutoLoginToken() {
$enc = new PasswordEncryptor_Blowfish();
$m1 = new Member();
$m1->PasswordEncryption = 'blowfish';
$m1->Salt = $enc->salt('123');
$m1Token = $m1->generateAutologinTokenAndStoreHash();
$m2 = new Member();
$m2->PasswordEncryption = 'blowfish';
$m2->Salt = $enc->salt('456');
$m2Token = $m2->generateAutologinTokenAndStoreHash();
$this->assertTrue($m1->validateAutoLoginToken($m1Token), 'Passes token validity test against matching member.');
$this->assertFalse($m2->validateAutoLoginToken($m1Token), 'Fails token validity test against other member.');
}
public function testRememberMeHashGeneration() {
$m1 = $this->objFromFixture('SilverStripe\\Security\\Member', 'grouplessmember');
$m1->login(true);
$hashes = RememberLoginHash::get()->filter('MemberID', $m1->ID);
$this->assertEquals($hashes->Count(), 1);
$firstHash = $hashes->First();
$this->assertNotNull($firstHash->DeviceID);
$this->assertNotNull($firstHash->Hash);
}
public function testRememberMeHashAutologin() {
/** @var Member $m1 */
$m1 = $this->objFromFixture('SilverStripe\\Security\\Member', 'noexpiry');
$m1->logIn(true);
$firstHash = RememberLoginHash::get()->filter('MemberID', $m1->ID)->First();
$this->assertNotNull($firstHash);
// re-generates the hash so we can get the token
$firstHash->Hash = $firstHash->getNewHash($m1);
$token = $firstHash->getToken();
$firstHash->write();
$response = $this->get(
'Security/login',
$this->session(),
null,
array(
'alc_enc' => $m1->ID.':'.$token,
'alc_device' => $firstHash->DeviceID
)
);
$message = _t(
'Member.LOGGEDINAS',
"You're logged in as {name}.",
array('name' => $m1->FirstName)
);
$this->assertContains($message, $response->getBody());
$this->session()->inst_set('loggedInAs', null);
// A wrong token or a wrong device ID should not let us autologin
$response = $this->get(
'Security/login',
$this->session(),
null,
array(
'alc_enc' => $m1->ID.':'.str_rot13($token),
'alc_device' => $firstHash->DeviceID
)
);
$this->assertNotContains($message, $response->getBody());
$response = $this->get(
'Security/login',
$this->session(),
null,
array(
'alc_enc' => $m1->ID.':'.$token,
'alc_device' => str_rot13($firstHash->DeviceID)
)
);
$this->assertNotContains($message, $response->getBody());
// Re-logging (ie 'alc_enc' has expired), and not checking the "Remember Me" option
// should remove all previous hashes for this device
$response = $this->post(
'Security/LoginForm',
array(
'Email' => $m1->Email,
'Password' => '1nitialPassword',
'AuthenticationMethod' => 'SilverStripe\\Security\\MemberAuthenticator',
'action_dologin' => 'action_dologin'
),
null,
$this->session(),
null,
array(
'alc_device' => $firstHash->DeviceID
)
);
$this->assertContains($message, $response->getBody());
$this->assertEquals(RememberLoginHash::get()->filter('MemberID', $m1->ID)->Count(), 0);
}
public function testExpiredRememberMeHashAutologin() {
$m1 = $this->objFromFixture('SilverStripe\\Security\\Member', 'noexpiry');
$m1->login(true);
$firstHash = RememberLoginHash::get()->filter('MemberID', $m1->ID)->First();
$this->assertNotNull($firstHash);
// re-generates the hash so we can get the token
$firstHash->Hash = $firstHash->getNewHash($m1);
$token = $firstHash->getToken();
$firstHash->ExpiryDate = '2000-01-01 00:00:00';
$firstHash->write();
DBDateTime::set_mock_now('1999-12-31 23:59:59');
$response = $this->get(
'Security/login',
$this->session(),
null,
array(
'alc_enc' => $m1->ID.':'.$token,
'alc_device' => $firstHash->DeviceID
)
);
$message = _t(
'Member.LOGGEDINAS',
"You're logged in as {name}.",
array('name' => $m1->FirstName)
);
$this->assertContains($message, $response->getBody());
$this->session()->inst_set('loggedInAs', null);
// re-generates the hash so we can get the token
$firstHash->Hash = $firstHash->getNewHash($m1);
$token = $firstHash->getToken();
$firstHash->ExpiryDate = '2000-01-01 00:00:00';
$firstHash->write();
DBDateTime::set_mock_now('2000-01-01 00:00:01');
$response = $this->get(
'Security/login',
$this->session(),
null,
array(
'alc_enc' => $m1->ID.':'.$token,
'alc_device' => $firstHash->DeviceID
)
);
$this->assertNotContains($message, $response->getBody());
$this->session()->inst_set('loggedInAs', null);
DBDatetime::clear_mock_now();
}
public function testRememberMeMultipleDevices() {
$m1 = $this->objFromFixture('SilverStripe\\Security\\Member', 'noexpiry');
// First device
$m1->login(true);
Cookie::set('alc_device', null);
// Second device
$m1->login(true);
// Hash of first device
$firstHash = RememberLoginHash::get()->filter('MemberID', $m1->ID)->First();
$this->assertNotNull($firstHash);
// Hash of second device
$secondHash = RememberLoginHash::get()->filter('MemberID', $m1->ID)->Last();
$this->assertNotNull($secondHash);
// DeviceIDs are different
$this->assertNotEquals($firstHash->DeviceID, $secondHash->DeviceID);
// re-generates the hashes so we can get the tokens
$firstHash->Hash = $firstHash->getNewHash($m1);
$firstToken = $firstHash->getToken();
$firstHash->write();
$secondHash->Hash = $secondHash->getNewHash($m1);
$secondToken = $secondHash->getToken();
$secondHash->write();
// Accessing the login page should show the user's name straight away
$response = $this->get(
'Security/login',
$this->session(),
null,
array(
'alc_enc' => $m1->ID.':'.$firstToken,
'alc_device' => $firstHash->DeviceID
)
);
$message = _t(
'Member.LOGGEDINAS',
"You're logged in as {name}.",
array('name' => $m1->FirstName)
);
$this->assertContains($message, $response->getBody());
$this->session()->inst_set('loggedInAs', null);
// Accessing the login page from the second device
$response = $this->get(
'Security/login',
$this->session(),
null,
array(
'alc_enc' => $m1->ID.':'.$secondToken,
'alc_device' => $secondHash->DeviceID
)
);
$this->assertContains($message, $response->getBody());
$logout_across_devices = Config::inst()->get('SilverStripe\\Security\\RememberLoginHash', 'logout_across_devices');
// Logging out from the second device - only one device being logged out
Config::inst()->update('SilverStripe\\Security\\RememberLoginHash', 'logout_across_devices', false);
$response = $this->get(
'Security/logout',
$this->session(),
null,
array(
'alc_enc' => $m1->ID.':'.$secondToken,
'alc_device' => $secondHash->DeviceID
)
);
$this->assertEquals(
RememberLoginHash::get()->filter(array('MemberID'=>$m1->ID, 'DeviceID'=>$firstHash->DeviceID))->Count(),
1
);
// Logging out from any device when all login hashes should be removed
Config::inst()->update('SilverStripe\\Security\\RememberLoginHash', 'logout_across_devices', true);
$m1->login(true);
$response = $this->get('Security/logout', $this->session());
$this->assertEquals(
RememberLoginHash::get()->filter('MemberID', $m1->ID)->Count(),
0
);
Config::inst()->update('SilverStripe\\Security\\RememberLoginHash', 'logout_across_devices', $logout_across_devices);
}
public function testCanDelete() {
$admin1 = $this->objFromFixture('SilverStripe\\Security\\Member', 'admin');
$admin2 = $this->objFromFixture('SilverStripe\\Security\\Member', 'other-admin');
$member1 = $this->objFromFixture('SilverStripe\\Security\\Member', 'grouplessmember');
$member2 = $this->objFromFixture('SilverStripe\\Security\\Member', 'noformatmember');
$this->assertTrue(
$admin1->canDelete($admin2),
'Admins can delete other admins'
);
$this->assertTrue(
$member1->canDelete($admin2),
'Admins can delete non-admins'
);
$this->assertFalse(
$admin1->canDelete($admin1),
'Admins can not delete themselves'
);
$this->assertFalse(
$member1->canDelete($member2),
'Non-admins can not delete other non-admins'
);
$this->assertFalse(
$member1->canDelete($member1),
'Non-admins can not delete themselves'
);
}
public function testFailedLoginCount() {
$maxFailedLoginsAllowed = 3;
//set up the config variables to enable login lockouts
Config::nest();
Config::inst()->update('SilverStripe\\Security\\Member', 'lock_out_after_incorrect_logins', $maxFailedLoginsAllowed);
$member = $this->objFromFixture('SilverStripe\\Security\\Member', 'test');
$failedLoginCount = $member->FailedLoginCount;
for ($i = 1; $i < $maxFailedLoginsAllowed; ++$i) {
$member->registerFailedLogin();
$this->assertEquals(
++$failedLoginCount,
$member->FailedLoginCount,
'Failed to increment $member->FailedLoginCount'
);
$this->assertFalse(
$member->isLockedOut(),
"Member has been locked out too early"
);
}
}
public function testMemberValidator()
{
// clear custom requirements for this test
Config::inst()->update('SilverStripe\\Security\\Member_Validator', 'customRequired', null);
$memberA = $this->objFromFixture('SilverStripe\\Security\\Member', 'admin');
$memberB = $this->objFromFixture('SilverStripe\\Security\\Member', 'test');
// create a blank form
$form = new MemberTest_ValidatorForm();
$validator = new Member_Validator();
$validator->setForm($form);
// Simulate creation of a new member via form, but use an existing member identifier
$fail = $validator->php(array(
'FirstName' => 'Test',
'Email' => $memberA->Email
));
$this->assertFalse(
$fail,
'Member_Validator must fail when trying to create new Member with existing Email.'
);
// populate the form with values from another member
$form->loadDataFrom($memberB);
// Assign the validator to an existing member
// (this is basically the same as passing the member ID with the form data)
$validator->setForMember($memberB);
// Simulate update of a member via form and use an existing member Email
$fail = $validator->php(array(
'FirstName' => 'Test',
'Email' => $memberA->Email
));
// Simulate update to a new Email address
$pass1 = $validator->php(array(
'FirstName' => 'Test',
'Email' => 'membervalidatortest@testing.com'
));
// Pass in the same Email address that the member already has. Ensure that case is valid
$pass2 = $validator->php(array(
'FirstName' => 'Test',
'Surname' => 'User',
'Email' => $memberB->Email
));
$this->assertFalse(
$fail,
'Member_Validator must fail when trying to update existing member with existing Email.'
);
$this->assertTrue(
$pass1,
'Member_Validator must pass when Email is updated to a value that\'s not in use.'
);
$this->assertTrue(
$pass2,
'Member_Validator must pass when Member updates his own Email to the already existing value.'
);
}
public function testMemberValidatorWithExtensions()
{
// clear custom requirements for this test
Config::inst()->update('SilverStripe\\Security\\Member_Validator', 'customRequired', null);
// create a blank form
$form = new MemberTest_ValidatorForm();
// Test extensions
Member_Validator::add_extension('MemberTest_MemberValidator_SurnameMustMatchFirstNameExtension');
$validator = new Member_Validator();
$validator->setForm($form);
// This test should fail, since the extension enforces FirstName == Surname
$fail = $validator->php(array(
'FirstName' => 'Test',
'Surname' => 'User',
'Email' => 'test-member-validator-extension@testing.com'
));
$pass = $validator->php(array(
'FirstName' => 'Test',
'Surname' => 'Test',
'Email' => 'test-member-validator-extension@testing.com'
));
$this->assertFalse(
$fail,
'Member_Validator must fail because of added extension.'
);
$this->assertTrue(
$pass,
'Member_Validator must succeed, since it meets all requirements.'
);
// Add another extension that always fails. This ensures that all extensions are considered in the validation
Member_Validator::add_extension('MemberTest_MemberValidator_AlwaysFailsExtension');
$validator = new Member_Validator();
$validator->setForm($form);
// Even though the data is valid, This test should still fail, since one extension always returns false
$fail = $validator->php(array(
'FirstName' => 'Test',
'Surname' => 'Test',
'Email' => 'test-member-validator-extension@testing.com'
));
$this->assertFalse(
$fail,
'Member_Validator must fail because of added extensions.'
);
// Remove added extensions
Member_Validator::remove_extension('MemberTest_MemberValidator_AlwaysFailsExtension');
Member_Validator::remove_extension('MemberTest_MemberValidator_SurnameMustMatchFirstNameExtension');
}
public function testCustomMemberValidator()
{
// clear custom requirements for this test
Config::inst()->update('SilverStripe\\Security\\Member_Validator', 'customRequired', null);
$member = $this->objFromFixture('SilverStripe\\Security\\Member', 'admin');
$form = new MemberTest_ValidatorForm();
$form->loadDataFrom($member);
$validator = new Member_Validator();
$validator->setForm($form);
$pass = $validator->php(array(
'FirstName' => 'Borris',
'Email' => 'borris@silverstripe.com'
));
$fail = $validator->php(array(
'Email' => 'borris@silverstripe.com',
'Surname' => ''
));
$this->assertTrue($pass, 'Validator requires a FirstName and Email');
$this->assertFalse($fail, 'Missing FirstName');
$ext = new MemberTest_ValidatorExtension();
$ext->updateValidator($validator);
$pass = $validator->php(array(
'FirstName' => 'Borris',
'Email' => 'borris@silverstripe.com'
));
$fail = $validator->php(array(
'Email' => 'borris@silverstripe.com'
));
$this->assertFalse($pass, 'Missing surname');
$this->assertFalse($fail, 'Missing surname value');
$fail = $validator->php(array(
'Email' => 'borris@silverstripe.com',
'Surname' => 'Silverman'
));
$this->assertTrue($fail, 'Passes with email and surname now (no firstname)');
}
}
/**
* @package framework
* @subpackage tests
*/
class MemberTest_ValidatorForm extends Form implements TestOnly {
public function __construct() {
parent::__construct(Controller::curr(), __CLASS__, new FieldList(
new TextField('Email'),
new TextField('Surname'),
new TextField('ID'),
new TextField('FirstName')
), new FieldList(
new FormAction('someAction')
));
}
}
/**
* @package framework
* @subpackage tests
*/
class MemberTest_ValidatorExtension extends DataExtension implements TestOnly {
public function updateValidator(&$validator) {
$validator->addRequiredField('Surname');
$validator->removeRequiredField('FirstName');
}
}
/**
* Extension that adds additional validation criteria
* @package framework
* @subpackage tests
*/
class MemberTest_MemberValidator_SurnameMustMatchFirstNameExtension extends DataExtension implements TestOnly
{
public function updatePHP($data, $form) {
return $data['FirstName'] == $data['Surname'];
}
}
/**
* Extension that adds additional validation criteria
* @package framework
* @subpackage tests
*/
class MemberTest_MemberValidator_AlwaysFailsExtension extends DataExtension implements TestOnly
{
public function updatePHP($data, $form) {
return false;
}
}
/**
* @package framework
* @subpackage tests
*/
class MemberTest_ViewingAllowedExtension extends DataExtension implements TestOnly {
public function canView($member = null) {
return true;
}
}
/**
* @package framework
* @subpackage tests
*/
class MemberTest_ViewingDeniedExtension extends DataExtension implements TestOnly {
public function canView($member = null) {
return false;
}
}
/**
* @package framework
* @subpackage tests
*/
class MemberTest_FieldsExtension extends DataExtension implements TestOnly {
public function updateCMSFields(FieldList $fields) {
$fields->addFieldToTab('Root.Main', new TextField('TestMemberField', 'Test'));
}
}
/**
* @package framework
* @subpackage tests
*/
class MemberTest_EditingAllowedDeletingDeniedExtension extends DataExtension implements TestOnly {
public function canView($member = null) {
return true;
}
public function canEdit($member = null) {
return true;
}
public function canDelete($member = null) {
return false;
}
}
/**
* @package framework
* @subpackage tests
*/
class MemberTest_PasswordValidator extends PasswordValidator {
public function __construct() {
parent::__construct();
$this->minLength(7);
$this->checkHistoricalPasswords(6);
$this->characterStrength(3, array('lowercase','uppercase','digits','punctuation'));
}
}