mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 14:05:37 +02:00
NEW Allow admins to require password reset for members
This came from silverstripe/silverstripe-security-extensions
This commit is contained in:
parent
0fee1aa584
commit
8ddedb038e
@ -13,6 +13,7 @@ use SilverStripe\Core\Config\Config;
|
|||||||
use SilverStripe\Core\Convert;
|
use SilverStripe\Core\Convert;
|
||||||
use SilverStripe\Core\Injector\Injector;
|
use SilverStripe\Core\Injector\Injector;
|
||||||
use SilverStripe\Dev\TestMailer;
|
use SilverStripe\Dev\TestMailer;
|
||||||
|
use SilverStripe\Forms\CheckboxField;
|
||||||
use SilverStripe\Forms\ConfirmedPasswordField;
|
use SilverStripe\Forms\ConfirmedPasswordField;
|
||||||
use SilverStripe\Forms\DropdownField;
|
use SilverStripe\Forms\DropdownField;
|
||||||
use SilverStripe\Forms\FieldList;
|
use SilverStripe\Forms\FieldList;
|
||||||
@ -398,7 +399,40 @@ class Member extends DataObject
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function isPasswordExpired()
|
/**
|
||||||
|
* Used to get the value for the reset password on next login checkbox
|
||||||
|
*/
|
||||||
|
public function getRequiresPasswordChangeOnNextLogin(): bool
|
||||||
|
{
|
||||||
|
return $this->isPasswordExpired();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set password expiry to "now" to require a change of password next log in
|
||||||
|
*
|
||||||
|
* @param int|null $dataValue 1 is checked, 0/null is not checked {@see CheckboxField::dataValue}
|
||||||
|
*/
|
||||||
|
public function saveRequiresPasswordChangeOnNextLogin(?int $dataValue): static
|
||||||
|
{
|
||||||
|
if (!$this->canEdit()) {
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
$currentValue = $this->PasswordExpiry;
|
||||||
|
$currentDate = $this->dbObject('PasswordExpiry');
|
||||||
|
|
||||||
|
if ($dataValue && (!$currentValue || $currentDate->inFuture())) {
|
||||||
|
// Only alter future expiries - this way an admin could see how long ago a password expired still
|
||||||
|
$this->PasswordExpiry = DBDatetime::now()->Rfc2822();
|
||||||
|
} elseif (!$dataValue && $this->isPasswordExpired()) {
|
||||||
|
// Only unset if the expiry date is in the past
|
||||||
|
$this->PasswordExpiry = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isPasswordExpired(): bool
|
||||||
{
|
{
|
||||||
if (!$this->PasswordExpiry) {
|
if (!$this->PasswordExpiry) {
|
||||||
return false;
|
return false;
|
||||||
@ -1363,6 +1397,19 @@ class Member extends DataObject
|
|||||||
if ($permissionsTab) {
|
if ($permissionsTab) {
|
||||||
$permissionsTab->addExtraClass('readonly');
|
$permissionsTab->addExtraClass('readonly');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$currentUser = Security::getCurrentUser();
|
||||||
|
// We can allow an admin to require a user to change their password. But:
|
||||||
|
// - Don't show a read only field if the user cannot edit this record
|
||||||
|
// - Don't show if a user views their own profile (just let them reset their own password)
|
||||||
|
if ($currentUser && ($currentUser->ID !== $this->ID) && $this->canEdit()) {
|
||||||
|
$requireNewPassword = CheckboxField::create(
|
||||||
|
'RequiresPasswordChangeOnNextLogin',
|
||||||
|
_t(__CLASS__ . '.RequiresPasswordChangeOnNextLogin', 'Requires password change on next log in')
|
||||||
|
);
|
||||||
|
$fields->insertAfter('Password', $requireNewPassword);
|
||||||
|
$fields->dataFieldByName('Password')->addExtraClass('form-field--no-divider mb-0 pb-0');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return parent::getCMSFields();
|
return parent::getCMSFields();
|
||||||
|
@ -7,6 +7,9 @@ use SilverStripe\Core\Config\Config;
|
|||||||
use SilverStripe\Core\Convert;
|
use SilverStripe\Core\Convert;
|
||||||
use SilverStripe\Core\Injector\Injector;
|
use SilverStripe\Core\Injector\Injector;
|
||||||
use SilverStripe\Dev\FunctionalTest;
|
use SilverStripe\Dev\FunctionalTest;
|
||||||
|
use SilverStripe\Forms\CheckboxField;
|
||||||
|
use SilverStripe\Forms\FieldList;
|
||||||
|
use SilverStripe\Forms\Form;
|
||||||
use SilverStripe\Forms\ListboxField;
|
use SilverStripe\Forms\ListboxField;
|
||||||
use SilverStripe\i18n\i18n;
|
use SilverStripe\i18n\i18n;
|
||||||
use SilverStripe\ORM\DataObject;
|
use SilverStripe\ORM\DataObject;
|
||||||
@ -402,6 +405,161 @@ class MemberTest extends FunctionalTest
|
|||||||
$this->assertFalse($member->isPasswordExpired());
|
$this->assertFalse($member->isPasswordExpired());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testAdminCanRequirePasswordChangeOnNextLogIn()
|
||||||
|
{
|
||||||
|
/** @var Member&MemberExtension $targetMember */
|
||||||
|
$targetMember = $this->objFromFixture(Member::class, 'someone');
|
||||||
|
$this->logInWithPermission('ADMIN');
|
||||||
|
$field = $targetMember->getCMSFields()->dataFieldByName('RequiresPasswordChangeOnNextLogin');
|
||||||
|
$this->assertNotNull($field);
|
||||||
|
$this->assertInstanceOf(CheckboxField::class, $field, 'The field should be an instance of ' . CheckboxField::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testUserCannotRequireTheirOwnPasswordChangeOnNextLogIn()
|
||||||
|
{
|
||||||
|
/** @var Member&MemberExtension $targetMember */
|
||||||
|
$targetMember = $this->objFromFixture(Member::class, 'someone');
|
||||||
|
$this->logInAs($targetMember);
|
||||||
|
$field = $targetMember->getCMSFields()->dataFieldByName('RequiresPasswordChangeOnNextLogin');
|
||||||
|
$this->assertNull($field);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testUserCannotRequireOthersToPasswordChangeOnNextLogIn()
|
||||||
|
{
|
||||||
|
/** @var Member&MemberExtension $targetMember */
|
||||||
|
$targetMember = $this->objFromFixture(Member::class, 'anyone');
|
||||||
|
$this->logInAs('someone');
|
||||||
|
$field = $targetMember->getCMSFields()->dataFieldByName('RequiresPasswordChangeOnNextLogin');
|
||||||
|
$this->assertNull($field);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testCheckingRequiresPasswordChangeOnNextLoginWillSetPasswordExpiryToNow()
|
||||||
|
{
|
||||||
|
$mockDate = '2019-03-02 00:00:00';
|
||||||
|
DBDateTime::set_mock_now($mockDate);
|
||||||
|
|
||||||
|
/** @var Member&MemberExtension $targetMember */
|
||||||
|
$targetMember = $this->objFromFixture(Member::class, 'someone');
|
||||||
|
|
||||||
|
$this->assertNull($targetMember->PasswordExpiry);
|
||||||
|
|
||||||
|
$this->logInWithPermission('ADMIN');
|
||||||
|
$fields = $targetMember->getCMSFields();
|
||||||
|
$form = new Form(null, 'SomeForm', $fields, new FieldList());
|
||||||
|
$field = $fields->dataFieldByName('RequiresPasswordChangeOnNextLogin');
|
||||||
|
$field->setValue(1);
|
||||||
|
$form->saveInto($targetMember);
|
||||||
|
|
||||||
|
$this->assertEquals($mockDate, $targetMember->PasswordExpiry);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testCheckingPasswordChangeUpdatesFutureExpiriesToNow()
|
||||||
|
{
|
||||||
|
$mockDate = '2019-03-02 00:00:00';
|
||||||
|
DBDateTime::set_mock_now($mockDate);
|
||||||
|
|
||||||
|
/** @var Member&MemberExtension $targetMember */
|
||||||
|
$targetMember = $this->objFromFixture(Member::class, 'willexpire');
|
||||||
|
|
||||||
|
$this->assertTrue($targetMember->dbObject('PasswordExpiry')->inFuture());
|
||||||
|
|
||||||
|
$this->logInWithPermission('ADMIN');
|
||||||
|
$fields = $targetMember->getCMSFields();
|
||||||
|
$form = new Form(null, 'SomeForm', $fields, new FieldList());
|
||||||
|
$field = $fields->dataFieldByName('RequiresPasswordChangeOnNextLogin');
|
||||||
|
$field->setValue(1);
|
||||||
|
$form->saveInto($targetMember);
|
||||||
|
|
||||||
|
$this->assertEquals($mockDate, $targetMember->PasswordExpiry);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testCheckingPasswordChangeDoesNotAlterPastDates()
|
||||||
|
{
|
||||||
|
$mockDate = '2019-03-02 00:00:00';
|
||||||
|
DBDateTime::set_mock_now($mockDate);
|
||||||
|
|
||||||
|
/** @var Member&MemberExtension $targetMember */
|
||||||
|
$targetMember = $this->objFromFixture(Member::class, 'expired');
|
||||||
|
$originalValue = $targetMember->PasswordExpiry;
|
||||||
|
|
||||||
|
$this->assertTrue($targetMember->dbObject('PasswordExpiry')->inPast());
|
||||||
|
|
||||||
|
$this->logInWithPermission('ADMIN');
|
||||||
|
$fields = $targetMember->getCMSFields();
|
||||||
|
$form = new Form(null, 'SomeForm', $fields, new FieldList());
|
||||||
|
$field = $fields->dataFieldByName('RequiresPasswordChangeOnNextLogin');
|
||||||
|
$field->setValue(1);
|
||||||
|
$form->saveInto($targetMember);
|
||||||
|
|
||||||
|
$this->assertEquals($originalValue, $targetMember->PasswordExpiry);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSavingUncheckedPasswordChangeNullsPastDates()
|
||||||
|
{
|
||||||
|
$mockDate = '2019-03-02 00:00:00';
|
||||||
|
DBDateTime::set_mock_now($mockDate);
|
||||||
|
|
||||||
|
/** @var Member&MemberExtension $targetMember */
|
||||||
|
$targetMember = $this->objFromFixture(Member::class, 'expired');
|
||||||
|
|
||||||
|
$this->logInWithPermission('ADMIN');
|
||||||
|
$fields = $targetMember->getCMSFields();
|
||||||
|
$form = new Form(null, 'SomeForm', $fields, new FieldList());
|
||||||
|
$field = $fields->dataFieldByName('RequiresPasswordChangeOnNextLogin');
|
||||||
|
$field->setValue(0);
|
||||||
|
$form->saveInto($targetMember);
|
||||||
|
|
||||||
|
$this->assertNull($targetMember->PasswordExpiry);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSavingUncheckedPasswordChangeDoesNotAlterFutureDates()
|
||||||
|
{
|
||||||
|
$mockDate = '2019-03-02 00:00:00';
|
||||||
|
DBDateTime::set_mock_now($mockDate);
|
||||||
|
|
||||||
|
/** @var Member&MemberExtension $targetMember */
|
||||||
|
$targetMember = $this->objFromFixture(Member::class, 'willexpire');
|
||||||
|
$originalValue = $targetMember->PasswordExpiry;
|
||||||
|
|
||||||
|
$this->logInWithPermission('ADMIN');
|
||||||
|
$fields = $targetMember->getCMSFields();
|
||||||
|
$form = new Form(null, 'SomeForm', $fields, new FieldList());
|
||||||
|
$field = $fields->dataFieldByName('RequiresPasswordChangeOnNextLogin');
|
||||||
|
$field->setValue(0);
|
||||||
|
$form->saveInto($targetMember);
|
||||||
|
|
||||||
|
$this->assertNotNull($targetMember->PasswordExpiry);
|
||||||
|
$this->assertEquals($originalValue, $targetMember->PasswordExpiry);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSavingChangePasswordOnNextLoginIsNotPossibleIfTheCurrentMemberCannotEditTheMemberBeingSaved()
|
||||||
|
{
|
||||||
|
/** @var Member&MemberExtension $targetMember */
|
||||||
|
$targetMember = $this->objFromFixture(Member::class, 'expired');
|
||||||
|
$originalValue = $targetMember->PasswordExpiry;
|
||||||
|
|
||||||
|
$this->logInAs('someone');
|
||||||
|
$fields = $targetMember->saveRequiresPasswordChangeOnNextLogin(0);
|
||||||
|
|
||||||
|
$this->assertEquals($originalValue, $targetMember->PasswordExpiry);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetRequiresPasswordChangeOnNextLogin()
|
||||||
|
{
|
||||||
|
$this->assertTrue(
|
||||||
|
$this->objFromFixture(Member::class, 'expired')->getRequiresPasswordChangeOnNextLogin(),
|
||||||
|
'PasswordExpiry date in the past should require a change'
|
||||||
|
);
|
||||||
|
$this->assertFalse(
|
||||||
|
$this->objFromFixture(Member::class, 'willexpire')->getRequiresPasswordChangeOnNextLogin(),
|
||||||
|
'PasswordExpiry date in the past should NOT require a change'
|
||||||
|
);
|
||||||
|
$this->assertFalse(
|
||||||
|
$this->objFromFixture(Member::class, 'someone')->getRequiresPasswordChangeOnNextLogin(),
|
||||||
|
'PasswordExpiry is NULL should NOT require a change'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
public function testInGroups()
|
public function testInGroups()
|
||||||
{
|
{
|
||||||
/** @var Member $staffmember */
|
/** @var Member $staffmember */
|
||||||
@ -869,7 +1027,7 @@ class MemberTest extends FunctionalTest
|
|||||||
public function testMap_in_groupsReturnsAll()
|
public function testMap_in_groupsReturnsAll()
|
||||||
{
|
{
|
||||||
$members = Member::map_in_groups();
|
$members = Member::map_in_groups();
|
||||||
$this->assertEquals(13, $members->count(), 'There are 12 members in the mock plus a fake admin');
|
$this->assertEquals(17, $members->count(), 'There are 16 members in the mock plus a fake admin');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -57,6 +57,20 @@
|
|||||||
Surname: User
|
Surname: User
|
||||||
Email: noexpiry@silverstripe.com
|
Email: noexpiry@silverstripe.com
|
||||||
Password: 1nitialPassword
|
Password: 1nitialPassword
|
||||||
|
someone:
|
||||||
|
FirstName: 'Someone'
|
||||||
|
Email: 'someone@example.com'
|
||||||
|
anyone:
|
||||||
|
FirstName: 'Anyone'
|
||||||
|
Email: 'anyone@example.com'
|
||||||
|
expired:
|
||||||
|
Firstname: 'Expired'
|
||||||
|
Email: 'expired@example.com'
|
||||||
|
PasswordExpiry: '2018-01-01'
|
||||||
|
willexpire:
|
||||||
|
Firstname: 'William'
|
||||||
|
Email: 'william@example.com'
|
||||||
|
PasswordExpiry: '3018-01-01'
|
||||||
staffmember:
|
staffmember:
|
||||||
Email: staffmember@test.com
|
Email: staffmember@test.com
|
||||||
Groups: '=>SilverStripe\Security\Group.staffgroup'
|
Groups: '=>SilverStripe\Security\Group.staffgroup'
|
||||||
|
Loading…
Reference in New Issue
Block a user