NEW Allow admins to require password reset for members

This came from silverstripe/silverstripe-security-extensions
This commit is contained in:
Guy Sartorelli 2023-01-27 11:38:10 +13:00
parent 0fee1aa584
commit 8ddedb038e
No known key found for this signature in database
GPG Key ID: F313E3B9504D496A
3 changed files with 221 additions and 2 deletions

View File

@ -13,6 +13,7 @@ use SilverStripe\Core\Config\Config;
use SilverStripe\Core\Convert;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\Dev\TestMailer;
use SilverStripe\Forms\CheckboxField;
use SilverStripe\Forms\ConfirmedPasswordField;
use SilverStripe\Forms\DropdownField;
use SilverStripe\Forms\FieldList;
@ -398,7 +399,40 @@ class Member extends DataObject
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) {
return false;
@ -1363,6 +1397,19 @@ class Member extends DataObject
if ($permissionsTab) {
$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();

View File

@ -7,6 +7,9 @@ use SilverStripe\Core\Config\Config;
use SilverStripe\Core\Convert;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\Dev\FunctionalTest;
use SilverStripe\Forms\CheckboxField;
use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\Form;
use SilverStripe\Forms\ListboxField;
use SilverStripe\i18n\i18n;
use SilverStripe\ORM\DataObject;
@ -402,6 +405,161 @@ class MemberTest extends FunctionalTest
$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()
{
/** @var Member $staffmember */
@ -869,7 +1027,7 @@ class MemberTest extends FunctionalTest
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');
$this->assertEquals(17, $members->count(), 'There are 16 members in the mock plus a fake admin');
}
/**

View File

@ -57,6 +57,20 @@
Surname: User
Email: noexpiry@silverstripe.com
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:
Email: staffmember@test.com
Groups: '=>SilverStripe\Security\Group.staffgroup'