Merge pull request #7026 from Firesphere/move_default_admin

Move default admin
This commit is contained in:
Damian Mooyman 2017-06-15 18:12:51 +12:00 committed by GitHub
commit 22e084f288
22 changed files with 617 additions and 343 deletions

View File

@ -1324,7 +1324,17 @@ After (`mysite/_config/config.yml`):
* `MODULES_PATH` removed * `MODULES_PATH` removed
* `MODULES_DIR` removed * `MODULES_DIR` removed
* `SS_HOST` removed. Use `SS_BASE_URL` instead. * `SS_HOST` removed. Use `SS_BASE_URL` instead.
* `Member::canLogIn()` now returns boolean. Use `Member::validateCanLogin()` to get a `ValidationResult`
* `Security` methods deprecated:
* `has_default_admin` use `DefaultAdminService::hasDefaultAdmin()` instead
* `check_default_admin` use `DefaultAdminService::isDefaultAdminCredentials()` instead
* `default_admin_username` use `DefaultAdminService::getDefaultAdminUsername()` instead
* `default_admin_password` use `DefaultAdminService::getDefaultAdminPassword()` instead
* `setDefaultAdmin` use `DefaultAdminService::setDefaultAdmin()` instead
* `clearDefaultAdmin` use `DefaultAdminService::clearDefaultAdmin()` instead
* `findAnAdministrator` use `DefaultAdminService::findOrCreateDefaultAdmin()` instead
* `Member` methods deprecated:
* `checkPassword`. Use Authenticator::checkPassword() instead
#### <a name="overview-general-removed"></a>General and Core Removed API #### <a name="overview-general-removed"></a>General and Core Removed API

4
sh.exe.stackdump Normal file
View File

@ -0,0 +1,4 @@
Stack trace:
Frame Function Args
0081BF88 6106D69F (00000000, 01010000, 00000000, 00000190)
End of stack trace

View File

@ -15,6 +15,7 @@ use SilverStripe\Dev\Install\DatabaseConfigurationHelper;
use SilverStripe\ORM\DatabaseAdmin; use SilverStripe\ORM\DatabaseAdmin;
use SilverStripe\ORM\DB; use SilverStripe\ORM\DB;
use SilverStripe\Security\Security; use SilverStripe\Security\Security;
use SilverStripe\Security\DefaultAdminService;
/** /**
* SilverStripe CMS Installer * SilverStripe CMS Installer
@ -1515,7 +1516,7 @@ PHP
// Create default administrator user and group in database // Create default administrator user and group in database
// (not using Security::setDefaultAdmin()) // (not using Security::setDefaultAdmin())
$adminMember = Security::findAnAdministrator(); $adminMember = DefaultAdminService::singleton()->findOrCreateDefaultAdmin();
$adminMember->Email = $config['admin']['username']; $adminMember->Email = $config['admin']['username'];
$adminMember->Password = $config['admin']['password']; $adminMember->Password = $config['admin']['password'];
$adminMember->PasswordEncryption = Security::config()->encryption_algorithm; $adminMember->PasswordEncryption = Security::config()->encryption_algorithm;

View File

@ -4,9 +4,8 @@ namespace SilverStripe\Forms;
use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\DataObjectInterface; use SilverStripe\ORM\DataObjectInterface;
use SilverStripe\Security\Member; use SilverStripe\Security\Authenticator;
use SilverStripe\Security\Security; use SilverStripe\Security\Security;
use SilverStripe\View\Requirements;
/** /**
* Two masked input fields, checks for matching passwords. * Two masked input fields, checks for matching passwords.
@ -519,17 +518,20 @@ class ConfirmedPasswordField extends FormField
} }
// With a valid user and password, check the password is correct // With a valid user and password, check the password is correct
$checkResult = $member->checkPassword($this->currentPasswordValue); $authenticators = Security::singleton()->getApplicableAuthenticators(Authenticator::CHECK_PASSWORD);
if (!$checkResult->isValid()) { foreach ($authenticators as $authenticator) {
$validator->validationError( $checkResult = $authenticator->checkPassword($member, $this->currentPasswordValue);
$name, if (!$checkResult->isValid()) {
_t( $validator->validationError(
'SilverStripe\\Forms\\ConfirmedPasswordField.CURRENT_PASSWORD_ERROR', $name,
"The current password you have entered is not correct." _t(
), 'SilverStripe\\Forms\\ConfirmedPasswordField.CURRENT_PASSWORD_ERROR',
"validation" "The current password you have entered is not correct."
); ),
return false; "validation"
);
return false;
}
} }
} }

View File

@ -96,10 +96,11 @@ use stdClass;
* @todo Add instance specific removeExtension() which undos loadExtraStatics() * @todo Add instance specific removeExtension() which undos loadExtraStatics()
* and defineMethods() * and defineMethods()
* *
* @property integer ID ID of the DataObject, 0 if the DataObject doesn't exist in database. * @property int $ID ID of the DataObject, 0 if the DataObject doesn't exist in database.
* @property string ClassName Class name of the DataObject * @property int $OldID ID of object, if deleted
* @property string LastEdited Date and time of DataObject's last modification. * @property string $ClassName Class name of the DataObject
* @property string Created Date and time of DataObject creation. * @property string $LastEdited Date and time of DataObject's last modification.
* @property string $Created Date and time of DataObject creation.
*/ */
class DataObject extends ViewableData implements DataObjectInterface, i18nEntityProvider, Resettable class DataObject extends ViewableData implements DataObjectInterface, i18nEntityProvider, Resettable
{ {

View File

@ -16,13 +16,36 @@ use SilverStripe\Security\MemberAuthenticator\LogoutHandler;
*/ */
interface Authenticator interface Authenticator
{ {
/**
* Can log a user in
*/
const LOGIN = 1; const LOGIN = 1;
/**
* Can log user out
*/
const LOGOUT = 2; const LOGOUT = 2;
/**
* Can change password (check + reset)
*/
const CHANGE_PASSWORD = 4; const CHANGE_PASSWORD = 4;
/**
* Can modify password
*/
const RESET_PASSWORD = 8; const RESET_PASSWORD = 8;
/**
* In-CMS authentication
*/
const CMS_LOGIN = 16; const CMS_LOGIN = 16;
/**
* Can check password is valid without logging the user in or modifying the password
*/
const CHECK_PASSWORD = 32;
/** /**
* Returns the services supported by this authenticator * Returns the services supported by this authenticator
* *
@ -85,5 +108,18 @@ interface Authenticator
* @param ValidationResult $result A validationresult which is either valid or contains the error message(s) * @param ValidationResult $result A validationresult which is either valid or contains the error message(s)
* @return Member The matched member, or null if the authentication fails * @return Member The matched member, or null if the authentication fails
*/ */
public function authenticate($data, &$result = null); public function authenticate($data, ValidationResult &$result = null);
/**
* Check if the passed password matches the stored one (if the member is not locked out).
*
* Note, we don't return early, to prevent differences in timings to give away if a member
* password is invalid.
*
* @param Member $member
* @param string $password
* @param ValidationResult $result
* @return ValidationResult
*/
public function checkPassword(Member $member, $password, ValidationResult &$result = null);
} }

View File

@ -0,0 +1,191 @@
<?php
namespace SilverStripe\Security;
use BadMethodCallException;
use InvalidArgumentException;
use SilverStripe\Core\Config\Configurable;
use SilverStripe\Core\Extensible;
use SilverStripe\Core\Injector\Injectable;
/**
* Provides access to the default admin
*/
class DefaultAdminService
{
use Extensible;
use Configurable;
use Injectable;
/**
* @var bool
*/
protected static $has_default_admin = false;
/**
* @var string
*/
protected static $default_username = null;
/**
* @var string
*/
protected static $default_password = null;
public function __construct()
{
$this->constructExtensions();
}
/**
* Set the default admin credentials
*
* @param string $username
* @param string $password
*/
public static function setDefaultAdmin($username, $password)
{
// don't overwrite if already set
if (static::hasDefaultAdmin()) {
throw new BadMethodCallException(
"Default admin already exists. Use clearDefaultAdmin() first."
);
}
if (empty($username) || empty($password)) {
throw new InvalidArgumentException("Default admin username / password cannot be empty");
}
static::$default_username = $username;
static::$default_password = $password;
static::$has_default_admin = true;
}
/**
* @return string The default admin username
* @throws BadMethodCallException Throws exception if there is no default admin
*/
public static function getDefaultAdminUsername()
{
if (!static::hasDefaultAdmin()) {
throw new BadMethodCallException(
"No default admin configured. Please call hasDefaultAdmin() before getting default admin username"
);
}
return static::$default_username;
}
/**
* @return string The default admin password
* @throws BadMethodCallException Throws exception if there is no default admin
*/
public static function getDefaultAdminPassword()
{
if (!static::hasDefaultAdmin()) {
throw new BadMethodCallException(
"No default admin configured. Please call hasDefaultAdmin() before getting default admin password"
);
}
return static::$default_password;
}
/**
* Check if there is a default admin
*
* @return bool
*/
public static function hasDefaultAdmin()
{
return static::$has_default_admin;
}
/**
* Flush the default admin credentials
*/
public static function clearDefaultAdmin()
{
static::$has_default_admin = false;
static::$default_username = null;
static::$default_password = null;
}
/**
* @return Member|null
*/
public function findOrCreateDefaultAdmin()
{
$this->extend('beforeFindOrCreateDefaultAdmin');
// Check if we have default admins
if (!static::hasDefaultAdmin()) {
return null;
}
// Find or create ADMIN group
Group::singleton()->requireDefaultRecords();
$adminGroup = Permission::get_groups_by_permission('ADMIN')->first();
if (!$adminGroup) {
Group::singleton()->requireDefaultRecords();
$adminGroup = Permission::get_groups_by_permission('ADMIN')->first();
}
// Find member
/** @skipUpgrade */
$admin = Member::get()
->filter('Email', static::getDefaultAdminUsername())
->first();
// If no admin is found, create one
if (!$admin) {
// 'Password' is not set to avoid creating
// persistent logins in the database. See Security::setDefaultAdmin().
// Set 'Email' to identify this as the default admin
$admin = Member::create();
$admin->FirstName = _t(__CLASS__ . '.DefaultAdminFirstname', 'Default Admin');
$admin->Email = static::getDefaultAdminUsername();
$admin->write();
}
// Ensure this user is in the admin group
if (!$admin->inGroup($adminGroup)) {
// Add member to group instead of adding group to member
// This bypasses the privilege escallation code in Member_GroupSet
$adminGroup
->DirectMembers()
->add($admin);
}
$this->extend('afterFindOrCreateDefaultAdmin', $admin);
return $admin;
}
/**
* Check if the user is a default admin.
* Returns false if there is no default admin.
*
* @param string $username
* @return bool
*/
public static function isDefaultAdmin($username)
{
return static::hasDefaultAdmin()
&& $username
&& $username === static::getDefaultAdminUsername();
}
/**
* Check if the user credentials match the default admin.
* Returns false if there is no default admin.
*
* @param string $username
* @param string $password
* @return bool
*/
public static function isDefaultAdminCredentials($username, $password)
{
return static::isDefaultAdmin($username)
&& $password
&& $password === static::getDefaultAdminPassword();
}
}

View File

@ -33,14 +33,14 @@ use SilverStripe\ORM\UnsavedRelationList;
/** /**
* A security group. * A security group.
* *
* @property string Title Name of the group * @property string $Title Name of the group
* @property string Description Description of the group * @property string $Description Description of the group
* @property string Code Group code * @property string $Code Group code
* @property string Locked Boolean indicating whether group is locked in security panel * @property string $Locked Boolean indicating whether group is locked in security panel
* @property int Sort * @property int $Sort
* @property string HtmlEditorConfig * @property string HtmlEditorConfig
* *
* @property int ParentID ID of parent group * @property int $ParentID ID of parent group
* *
* @method Group Parent() Return parent group * @method Group Parent() Return parent group
* @method HasManyList Permissions() List of group permissions * @method HasManyList Permissions() List of group permissions

View File

@ -19,6 +19,8 @@ use SilverStripe\Forms\DropdownField;
use SilverStripe\Forms\FieldList; use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\HTMLEditor\HTMLEditorConfig; use SilverStripe\Forms\HTMLEditor\HTMLEditorConfig;
use SilverStripe\Forms\ListboxField; use SilverStripe\Forms\ListboxField;
use SilverStripe\Forms\Tab;
use SilverStripe\Forms\TabSet;
use SilverStripe\i18n\i18n; use SilverStripe\i18n\i18n;
use SilverStripe\ORM\ArrayList; use SilverStripe\ORM\ArrayList;
use SilverStripe\ORM\DataList; use SilverStripe\ORM\DataList;
@ -259,87 +261,43 @@ class Member extends DataObject
{ {
parent::requireDefaultRecords(); parent::requireDefaultRecords();
// Default groups should've been built by Group->requireDefaultRecords() already // Default groups should've been built by Group->requireDefaultRecords() already
static::default_admin(); $service = DefaultAdminService::singleton();
$service->findOrCreateDefaultAdmin();
} }
/** /**
* Get the default admin record if it exists, or creates it otherwise if enabled * Get the default admin record if it exists, or creates it otherwise if enabled
* *
* @deprecated 4.0.0...5.0.0 Use DefaultAdminService::findOrCreateDefaultAdmin() instead
* @return Member * @return Member
*/ */
public static function default_admin() public static function default_admin()
{ {
// Check if set Deprecation::notice('5.0', 'Use DefaultAdminService::findOrCreateDefaultAdmin() instead');
if (!Security::has_default_admin()) { return DefaultAdminService::singleton()->findOrCreateDefaultAdmin();
return null;
}
// Find or create ADMIN group
Group::singleton()->requireDefaultRecords();
$adminGroup = Permission::get_groups_by_permission('ADMIN')->first();
// Find member
/** @skipUpgrade */
$admin = static::get()
->filter('Email', Security::default_admin_username())
->first();
if (!$admin) {
// 'Password' is not set to avoid creating
// persistent logins in the database. See Security::setDefaultAdmin().
// Set 'Email' to identify this as the default admin
$admin = Member::create();
$admin->FirstName = _t(__CLASS__ . '.DefaultAdminFirstname', 'Default Admin');
$admin->Email = Security::default_admin_username();
$admin->write();
}
// Ensure this user is in the admin group
if (!$admin->inGroup($adminGroup)) {
// Add member to group instead of adding group to member
// This bypasses the privilege escallation code in Member_GroupSet
$adminGroup
->DirectMembers()
->add($admin);
}
return $admin;
} }
/** /**
* Check if the passed password matches the stored one (if the member is not locked out). * Check if the passed password matches the stored one (if the member is not locked out).
* *
* @param string $password * @deprecated 4.0.0...5.0.0 Use Authenticator::checkPassword() instead
*
* @param string $password
* @return ValidationResult * @return ValidationResult
*/ */
public function checkPassword($password) public function checkPassword($password)
{ {
$result = $this->canLogIn(); Deprecation::notice('5.0', 'Use Authenticator::checkPassword() instead');
// Short-circuit the result upon failure, no further checks needed. // With a valid user and password, check the password is correct
if (!$result->isValid()) { $result = ValidationResult::create();
return $result; $authenticators = Security::singleton()->getApplicableAuthenticators(Authenticator::CHECK_PASSWORD);
foreach ($authenticators as $authenticator) {
$authenticator->checkPassword($this, $password, $result);
if (!$result->isValid()) {
break;
}
} }
// Allow default admin to login as self
if ($this->isDefaultAdmin() && Security::check_default_admin($this->Email, $password)) {
return $result;
}
// Check a password is set on this member
if (empty($this->Password) && $this->exists()) {
$result->addError(_t(__CLASS__ . '.NoPassword', 'There is no password on this member.'));
return $result;
}
$e = PasswordEncryptor::create_for_algorithm($this->PasswordEncryption);
if (!$e->check($this->Password, $password, $this->Salt, $this)) {
$result->addError(_t(
__CLASS__ . '.ERRORWRONGCRED',
'The provided details don\'t seem to be correct. Please try again.'
));
}
return $result; return $result;
} }
@ -350,8 +308,17 @@ class Member extends DataObject
*/ */
public function isDefaultAdmin() public function isDefaultAdmin()
{ {
return Security::has_default_admin() return DefaultAdminService::isDefaultAdmin($this->Email);
&& $this->Email === Security::default_admin_username(); }
/**
* Check if this user can login
*
* @return bool
*/
public function canLogin()
{
return $this->validateCanLogin()->isValid();
} }
/** /**
@ -360,12 +327,12 @@ class Member extends DataObject
* *
* You can hook into this with a "canLogIn" method on an attached extension. * You can hook into this with a "canLogIn" method on an attached extension.
* *
* @param ValidationResult $result Optional result to add errors to
* @return ValidationResult * @return ValidationResult
*/ */
public function canLogIn() public function validateCanLogin(ValidationResult &$result = null)
{ {
$result = ValidationResult::create(); $result = $result ?: ValidationResult::create();
if ($this->isLockedOut()) { if ($this->isLockedOut()) {
$result->addError( $result->addError(
_t( _t(
@ -394,7 +361,9 @@ class Member extends DataObject
return false; return false;
} }
return DBDatetime::now()->getTimestamp() < $this->dbObject('LockedOutUntil')->getTimestamp(); /** @var DBDatetime $lockedOutUntil */
$lockedOutUntil = $this->dbObject('LockedOutUntil');
return DBDatetime::now()->getTimestamp() < $lockedOutUntil->getTimestamp();
} }
/** /**
@ -1418,8 +1387,12 @@ class Member extends DataObject
public function getCMSFields() public function getCMSFields()
{ {
$this->beforeUpdateCMSFields(function (FieldList $fields) { $this->beforeUpdateCMSFields(function (FieldList $fields) {
/** @var TabSet $rootTabSet */
$rootTabSet = $fields->fieldByName("Root");
/** @var Tab $mainTab */
$mainTab = $rootTabSet->fieldByName("Main");
/** @var FieldList $mainFields */ /** @var FieldList $mainFields */
$mainFields = $fields->fieldByName("Root")->fieldByName("Main")->getChildren(); $mainFields = $mainTab->getChildren();
// Build change password field // Build change password field
$mainFields->replaceField('Password', $this->getMemberPasswordField()); $mainFields->replaceField('Password', $this->getMemberPasswordField());
@ -1479,7 +1452,7 @@ class Member extends DataObject
} }
} }
$permissionsTab = $fields->fieldByName("Root")->fieldByName('Permissions'); $permissionsTab = $rootTabSet->fieldByName('Permissions');
if ($permissionsTab) { if ($permissionsTab) {
$permissionsTab->addExtraClass('readonly'); $permissionsTab->addExtraClass('readonly');
} }

View File

@ -41,7 +41,7 @@ class CMSLoginHandler extends LoginHandler
protected function redirectToChangePassword() protected function redirectToChangePassword()
{ {
// Since this form is loaded via an iframe, this redirect must be performed via javascript // Since this form is loaded via an iframe, this redirect must be performed via javascript
$changePasswordForm = ChangePasswordForm::create($this->form->getController(), 'ChangePasswordForm'); $changePasswordForm = ChangePasswordForm::create($this, 'ChangePasswordForm');
$changePasswordForm->sessionMessage( $changePasswordForm->sessionMessage(
_t('SilverStripe\\Security\\Member.PASSWORDEXPIRED', 'Your password has expired. Please choose a new one.'), _t('SilverStripe\\Security\\Member.PASSWORDEXPIRED', 'Your password has expired. Please choose a new one.'),
'good' 'good'

View File

@ -6,6 +6,9 @@ use SilverStripe\ORM\ValidationResult;
use SilverStripe\Security\Authenticator as BaseAuthenticator; use SilverStripe\Security\Authenticator as BaseAuthenticator;
use SilverStripe\Security\Member; use SilverStripe\Security\Member;
/**
* Provides authentication for the user within the CMS
*/
class CMSMemberAuthenticator extends MemberAuthenticator class CMSMemberAuthenticator extends MemberAuthenticator
{ {
@ -20,14 +23,14 @@ class CMSMemberAuthenticator extends MemberAuthenticator
* @param Member|null $member * @param Member|null $member
* @return Member * @return Member
*/ */
protected function authenticateMember($data, &$result = null, $member = null) protected function authenticateMember($data, ValidationResult &$result = null, Member $member = null)
{ {
// Attempt to identify by temporary ID // Attempt to identify by temporary ID
if (!empty($data['tempid'])) { if (!empty($data['tempid'])) {
// Find user by tempid, in case they are re-validating an existing session // Find user by tempid, in case they are re-validating an existing session
$member = Member::member_from_tempid($data['tempid']); $member = Member::member_from_tempid($data['tempid']);
if ($member) { if ($member) {
$data['email'] = $member->Email; $data['Email'] = $member->Email;
} }
} }

View File

@ -200,11 +200,8 @@ class ChangePasswordHandler extends RequestHandler
{ {
$member = Security::getCurrentUser(); $member = Security::getCurrentUser();
// The user was logged in, check the current password // The user was logged in, check the current password
if ($member && ( $oldPassword = isset($data['OldPassword']) ? $data['OldPassword'] : null;
empty($data['OldPassword']) || if ($member && !$this->checkPassword($member, $oldPassword)) {
!$member->checkPassword($data['OldPassword'])->isValid()
)
) {
$form->sessionMessage( $form->sessionMessage(
_t( _t(
'SilverStripe\\Security\\Member.ERRORPASSWORDNOTMATCH', 'SilverStripe\\Security\\Member.ERRORPASSWORDNOTMATCH',
@ -274,8 +271,10 @@ class ChangePasswordHandler extends RequestHandler
$member->AutoLoginExpired = DBDatetime::create()->now(); $member->AutoLoginExpired = DBDatetime::create()->now();
$member->write(); $member->write();
if ($member->canLogIn()->isValid()) { if ($member->canLogIn()) {
Injector::inst()->get(IdentityStore::class)->logIn($member, false, $this->getRequest()); /** @var IdentityStore $identityStore */
$identityStore = Injector::inst()->get(IdentityStore::class);
$identityStore->logIn($member, false, $this->getRequest());
} }
// TODO Add confirmation message to login redirect // TODO Add confirmation message to login redirect
@ -305,4 +304,26 @@ class ChangePasswordHandler extends RequestHandler
return $this->redirect($url); return $this->redirect($url);
} }
/**
* Check if password is ok
*
* @param Member $member
* @param string $password
* @return bool
*/
protected function checkPassword($member, $password)
{
if (empty($password)) {
return false;
}
// With a valid user and password, check the password is correct
$authenticators = Security::singleton()->getApplicableAuthenticators(Authenticator::CHECK_PASSWORD);
foreach ($authenticators as $authenticator) {
if (!$authenticator->checkPassword($member, $password)->isValid()) {
return false;
}
}
return true;
}
} }

View File

@ -113,6 +113,7 @@ class LoginHandler extends RequestHandler
$this->extend('beforeLogin'); $this->extend('beforeLogin');
// Successful login // Successful login
/** @var ValidationResult $result */
if ($member = $this->checkLogin($data, $result)) { if ($member = $this->checkLogin($data, $result)) {
$this->performLogin($member, $data, $form->getRequestHandler()->getRequest()); $this->performLogin($member, $data, $form->getRequestHandler()->getRequest());
// Allow operations on the member after successful login // Allow operations on the member after successful login
@ -209,7 +210,7 @@ class LoginHandler extends RequestHandler
* @return Member Returns the member object on successful authentication * @return Member Returns the member object on successful authentication
* or NULL on failure. * or NULL on failure.
*/ */
public function checkLogin($data, &$result) public function checkLogin($data, ValidationResult &$result = null)
{ {
$member = $this->authenticator->authenticate($data, $result); $member = $this->authenticator->authenticate($data, $result);
if ($member instanceof Member) { if ($member instanceof Member) {

View File

@ -5,11 +5,14 @@ namespace SilverStripe\Security\MemberAuthenticator;
use InvalidArgumentException; use InvalidArgumentException;
use SilverStripe\Control\Controller; use SilverStripe\Control\Controller;
use SilverStripe\Control\Session; use SilverStripe\Control\Session;
use SilverStripe\Core\Extensible;
use SilverStripe\ORM\ValidationResult; use SilverStripe\ORM\ValidationResult;
use SilverStripe\Security\Authenticator; use SilverStripe\Security\Authenticator;
use SilverStripe\Security\LoginAttempt; use SilverStripe\Security\LoginAttempt;
use SilverStripe\Security\Member; use SilverStripe\Security\Member;
use SilverStripe\Security\PasswordEncryptor;
use SilverStripe\Security\Security; use SilverStripe\Security\Security;
use SilverStripe\Security\DefaultAdminService;
/** /**
* Authenticator for the default "member" method * Authenticator for the default "member" method
@ -19,12 +22,13 @@ use SilverStripe\Security\Security;
*/ */
class MemberAuthenticator implements Authenticator class MemberAuthenticator implements Authenticator
{ {
use Extensible;
public function supportedServices() public function supportedServices()
{ {
// Bitwise-OR of all the supported services in this Authenticator, to make a bitmask // Bitwise-OR of all the supported services in this Authenticator, to make a bitmask
return Authenticator::LOGIN | Authenticator::LOGOUT | Authenticator::CHANGE_PASSWORD return Authenticator::LOGIN | Authenticator::LOGOUT | Authenticator::CHANGE_PASSWORD
| Authenticator::RESET_PASSWORD; | Authenticator::RESET_PASSWORD | Authenticator::CHECK_PASSWORD;
} }
/** /**
@ -32,7 +36,7 @@ class MemberAuthenticator implements Authenticator
* @param null|ValidationResult $result * @param null|ValidationResult $result
* @return null|Member * @return null|Member
*/ */
public function authenticate($data, &$result = null) public function authenticate($data, ValidationResult &$result = null)
{ {
// Find authenticated member // Find authenticated member
$member = $this->authenticateMember($data, $result); $member = $this->authenticateMember($data, $result);
@ -52,45 +56,46 @@ class MemberAuthenticator implements Authenticator
* *
* @param array $data Form submitted data * @param array $data Form submitted data
* @param ValidationResult $result * @param ValidationResult $result
* @param Member|null This third parameter is used in the CMSAuthenticator(s) * @param Member $member This third parameter is used in the CMSAuthenticator(s)
* @return Member Found member, regardless of successful login * @return Member Found member, regardless of successful login
*/ */
protected function authenticateMember($data, &$result = null, $member = null) protected function authenticateMember($data, ValidationResult &$result = null, Member $member = null)
{ {
// Default success to false
$email = !empty($data['Email']) ? $data['Email'] : null; $email = !empty($data['Email']) ? $data['Email'] : null;
$result = new ValidationResult(); $result = $result ?: ValidationResult::create();
// Check default login (see Security::setDefaultAdmin()) // Check default login (see Security::setDefaultAdmin())
$asDefaultAdmin = $email === Security::default_admin_username(); $asDefaultAdmin = DefaultAdminService::isDefaultAdmin($email);
if ($asDefaultAdmin) { if ($asDefaultAdmin) {
// If logging is as default admin, ensure record is setup correctly // If logging is as default admin, ensure record is setup correctly
$member = Member::default_admin(); $member = DefaultAdminService::singleton()->findOrCreateDefaultAdmin();
$success = Security::check_default_admin($email, $data['Password']); $member->validateCanLogin($result);
$result = $member->canLogIn(); if ($result->isValid()) {
//protect against failed login // Check if default admin credentials are correct
if ($success && $result->isValid()) { if (DefaultAdminService::isDefaultAdminCredentials($email, $data['Password'])) {
return $member; return $member;
} else { } else {
$result->addError(_t( $result->addError(_t(
'SilverStripe\\Security\\Member.ERRORWRONGCRED', 'SilverStripe\\Security\\Member.ERRORWRONGCRED',
"The provided details don't seem to be correct. Please try again." "The provided details don't seem to be correct. Please try again."
)); ));
}
} }
} }
// Attempt to identify user by email // Attempt to identify user by email
if (!$member && $email) { if (!$member && $email) {
// Find user by email // Find user by email
$identifierField = Member::config()->get('unique_identifier_field');
/** @var Member $member */ /** @var Member $member */
$member = Member::get() $member = Member::get()
->filter([Member::config()->get('unique_identifier_field') => $email]) ->filter([$identifierField => $email])
->first(); ->first();
} }
// Validate against member if possible // Validate against member if possible
if ($member && !$asDefaultAdmin) { if ($member && !$asDefaultAdmin) {
$result = $member->checkPassword($data['Password']); $this->checkPassword($member, $data['Password'], $result);
} }
// Emit failure to member and form (if available) // Emit failure to member and form (if available)
@ -98,22 +103,61 @@ class MemberAuthenticator implements Authenticator
if ($member) { if ($member) {
$member->registerFailedLogin(); $member->registerFailedLogin();
} }
} elseif ($member) {
$member->registerSuccessfulLogin();
} else { } else {
if ($member) { // A non-existing member occurred. This will make the result "valid" so let's invalidate
$member->registerSuccessfulLogin(); $result->addError(_t(
} else { 'SilverStripe\\Security\\Member.ERRORWRONGCRED',
// A non-existing member occurred. This will make the result "valid" so let's invalidate "The provided details don't seem to be correct. Please try again."
$result->addError(_t( ));
'SilverStripe\\Security\\Member.ERRORWRONGCRED', return null;
"The provided details don't seem to be correct. Please try again."
));
$member = null;
}
} }
return $member; return $member;
} }
/**
* Check if the passed password matches the stored one (if the member is not locked out).
*
* Note, we don't return early, to prevent differences in timings to give away if a member
* password is invalid.
*
* @param Member $member
* @param string $password
* @param ValidationResult $result
* @return ValidationResult
*/
public function checkPassword(Member $member, $password, ValidationResult &$result = null)
{
// Check if allowed to login
$result = $member->validateCanLogin($result);
if (!$result->isValid()) {
return $result;
}
// Allow default admin to login as self
if (DefaultAdminService::isDefaultAdminCredentials($member->Email, $password)) {
return $result;
}
// Check a password is set on this member
if (empty($member->Password) && $member->exists()) {
$result->addError(_t(__CLASS__ . '.NoPassword', 'There is no password on this member.'));
}
$encryptor = PasswordEncryptor::create_for_algorithm($member->PasswordEncryption);
if (!$encryptor->check($member->Password, $password, $member->Salt, $member)) {
$result->addError(_t(
__CLASS__ . '.ERRORWRONGCRED',
'The provided details don\'t seem to be correct. Please try again.'
));
}
return $result;
}
/** /**
* Log login attempt * Log login attempt
* TODO We could handle this with an extension * TODO We could handle this with an extension
@ -142,7 +186,7 @@ class MemberAuthenticator implements Authenticator
$attempt->Status = 'Success'; $attempt->Status = 'Success';
// Audit logging hook // Audit logging hook
$member->extend('authenticated'); $member->extend('authenticationSucceeded');
} else { } else {
// Failed login - we're trying to see if a user exists with this email (disregarding wrong passwords) // Failed login - we're trying to see if a user exists with this email (disregarding wrong passwords)
$attempt->Status = 'Failure'; $attempt->Status = 'Failure';

View File

@ -77,8 +77,7 @@ class MemberLoginForm extends BaseLoginForm
$actions = null, $actions = null,
$checkCurrentUser = true $checkCurrentUser = true
) { ) {
$this->setController($controller);
$this->controller = $controller;
$this->authenticator_class = $authenticatorClass; $this->authenticator_class = $authenticatorClass;
$customCSS = project() . '/css/member_login.css'; $customCSS = project() . '/css/member_login.css';
@ -125,13 +124,14 @@ class MemberLoginForm extends BaseLoginForm
*/ */
protected function getFormFields() protected function getFormFields()
{ {
if ($this->controller->request->getVar('BackURL')) { $request = $this->getController()->getRequest();
$backURL = $this->controller->request->getVar('BackURL'); if ($request->getVar('BackURL')) {
$backURL = $request->getVar('BackURL');
} else { } else {
$backURL = Session::get('BackURL'); $backURL = Session::get('BackURL');
} }
$label = Member::singleton()->fieldLabel(Member::config()->unique_identifier_field); $label = Member::singleton()->fieldLabel(Member::config()->get('unique_identifier_field'));
$fields = FieldList::create( $fields = FieldList::create(
HiddenField::create("AuthenticationMethod", null, $this->authenticator_class, $this), HiddenField::create("AuthenticationMethod", null, $this->authenticator_class, $this),
// Regardless of what the unique identifer field is (usually 'Email'), it will be held in the // Regardless of what the unique identifer field is (usually 'Email'), it will be held in the

View File

@ -7,10 +7,10 @@ use SilverStripe\ORM\DataObject;
/** /**
* Keep track of users' previous passwords, so that we can check that new passwords aren't changed back to old ones. * Keep track of users' previous passwords, so that we can check that new passwords aren't changed back to old ones.
* *
* @property string Password * @property string $Password
* @property string Salt * @property string $Salt
* @property string PasswordEncryption * @property string $PasswordEncryption
* @property int MemberID ID of the Member * @property int $MemberID ID of the Member
* @method Member Member() Owner of the password * @method Member Member() Owner of the password
*/ */
class MemberPassword extends DataObject class MemberPassword extends DataObject
@ -21,9 +21,9 @@ class MemberPassword extends DataObject
'PasswordEncryption' => 'Varchar(50)', 'PasswordEncryption' => 'Varchar(50)',
); );
private static $has_one = array( private static $has_one = [
'Member' => 'SilverStripe\\Security\\Member' 'Member' => Member::class,
); ];
private static $table_name = "MemberPassword"; private static $table_name = "MemberPassword";
@ -47,12 +47,12 @@ class MemberPassword extends DataObject
* Check if the given password is the same as the one stored in this record. * Check if the given password is the same as the one stored in this record.
* See {@link Member->checkPassword()}. * See {@link Member->checkPassword()}.
* *
* @param String $password Cleartext password * @param string $password Cleartext password
* @return Boolean * @return bool
*/ */
public function checkPassword($password) public function checkPassword($password)
{ {
$e = PasswordEncryptor::create_for_algorithm($this->PasswordEncryption); $encryptor = PasswordEncryptor::create_for_algorithm($this->PasswordEncryption);
return $e->check($this->Password, $password, $this->Salt, $this->Member()); return $encryptor->check($this->Password, $password, $this->Salt, $this->Member());
} }
} }

View File

@ -25,10 +25,10 @@ use SilverStripe\ORM\DB;
use SilverStripe\ORM\FieldType\DBField; use SilverStripe\ORM\FieldType\DBField;
use SilverStripe\ORM\FieldType\DBHTMLText; use SilverStripe\ORM\FieldType\DBHTMLText;
use SilverStripe\ORM\ValidationResult; use SilverStripe\ORM\ValidationResult;
use SilverStripe\Security\DefaultAdminService;
use SilverStripe\View\ArrayData; use SilverStripe\View\ArrayData;
use SilverStripe\View\SSViewer; use SilverStripe\View\SSViewer;
use SilverStripe\View\TemplateGlobalProvider; use SilverStripe\View\TemplateGlobalProvider;
use Subsite;
/** /**
* Implements a basic security model * Implements a basic security model
@ -47,22 +47,6 @@ class Security extends Controller implements TemplateGlobalProvider
'ping', 'ping',
); );
/**
* Default user name. {@link setDefaultAdmin()}
*
* @var string
* @see setDefaultAdmin()
*/
protected static $default_username;
/**
* Default password. {@link setDefaultAdmin()}
*
* @var string
* @see setDefaultAdmin()
*/
protected static $default_password;
/** /**
* If set to TRUE to prevent sharing of the session across several sites * If set to TRUE to prevent sharing of the session across several sites
* in the domain. * in the domain.
@ -286,7 +270,7 @@ class Security extends Controller implements TemplateGlobalProvider
*/ */
public function getApplicableAuthenticators($service = Authenticator::LOGIN) public function getApplicableAuthenticators($service = Authenticator::LOGIN)
{ {
$authenticators = $this->authenticators; $authenticators = $this->getAuthenticators();
/** @var Authenticator $authenticator */ /** @var Authenticator $authenticator */
foreach ($authenticators as $name => $authenticator) { foreach ($authenticators as $name => $authenticator) {
@ -295,6 +279,10 @@ class Security extends Controller implements TemplateGlobalProvider
} }
} }
if (empty($authenticators)) {
throw new LogicException('No applicable authenticators found');
}
return $authenticators; return $authenticators;
} }
@ -957,57 +945,27 @@ class Security extends Controller implements TemplateGlobalProvider
* purposes outside of any default credentials set through {@link Security::setDefaultAdmin()}. * purposes outside of any default credentials set through {@link Security::setDefaultAdmin()}.
* *
* @return Member * @return Member
*
* @deprecated 4.0.0..5.0.0 Please use DefaultAdminService::findOrCreateDefaultAdmin()
*/ */
public static function findAnAdministrator() public static function findAnAdministrator()
{ {
static::singleton()->extend('beforeFindAdministrator'); Deprecation::notice('5.0.0', 'Please use DefaultAdminService::findOrCreateDefaultAdmin()');
/** @var Member $member */ $service = DefaultAdminService::singleton();
$member = null; return $service->findOrCreateDefaultAdmin();
// find a group with ADMIN permission
$adminGroup = Permission::get_groups_by_permission('ADMIN')->first();
if (!$adminGroup) {
Group::singleton()->requireDefaultRecords();
$adminGroup = Permission::get_groups_by_permission('ADMIN')->first();
}
$member = $adminGroup->Members()->First();
if (!$member) {
Member::singleton()->requireDefaultRecords();
$member = Permission::get_members_by_permission('ADMIN')->first();
}
if (!$member) {
$member = Member::default_admin();
}
if (!$member) {
// Failover to a blank admin
$member = Member::create();
$member->FirstName = _t('SilverStripe\\Security\\Member.DefaultAdminFirstname', 'Default Admin');
$member->write();
// Add member to group instead of adding group to member
// This bypasses the privilege escallation code in Member_GroupSet
$adminGroup
->DirectMembers()
->add($member);
}
static::singleton()->extend('afterFindAdministrator');
return $member;
} }
/** /**
* Flush the default admin credentials * Flush the default admin credentials
*
* @deprecated 4.0.0..5.0.0 Please use DefaultAdminService::clearDefaultAdmin()
*/ */
public static function clear_default_admin() public static function clear_default_admin()
{ {
self::$default_username = null; Deprecation::notice('5.0.0', 'Please use DefaultAdminService::clearDefaultAdmin()');
self::$default_password = null;
DefaultAdminService::clearDefaultAdmin();
} }
@ -1022,17 +980,14 @@ class Security extends Controller implements TemplateGlobalProvider
* @param string $username The user name * @param string $username The user name
* @param string $password The password (in cleartext) * @param string $password The password (in cleartext)
* @return bool True if successfully set * @return bool True if successfully set
*
* @deprecated 4.0.0..5.0.0 Please use DefaultAdminService::setDefaultAdmin($username, $password)
*/ */
public static function setDefaultAdmin($username, $password) public static function setDefaultAdmin($username, $password)
{ {
// don't overwrite if already set Deprecation::notice('5.0.0', 'Please use DefaultAdminService::setDefaultAdmin($username, $password)');
if (self::$default_username || self::$default_password) {
return false;
}
self::$default_username = $username;
self::$default_password = $password;
DefaultAdminService::setDefaultAdmin($username, $password);
return true; return true;
} }
@ -1043,42 +998,53 @@ class Security extends Controller implements TemplateGlobalProvider
* @param string $username * @param string $username
* @param string $password * @param string $password
* @return bool * @return bool
*
* @deprecated 4.0.0..5.0.0 Use DefaultAdminService::isDefaultAdminCredentials() instead
*/ */
public static function check_default_admin($username, $password) public static function check_default_admin($username, $password)
{ {
return ( Deprecation::notice('5.0.0', 'Please use DefaultAdminService::isDefaultAdminCredentials($username, $password)');
self::$default_username === $username
&& self::$default_password === $password /** @var DefaultAdminService $service */
&& self::has_default_admin() return DefaultAdminService::isDefaultAdminCredentials($username, $password);
);
} }
/** /**
* Check that the default admin account has been set. * Check that the default admin account has been set.
*
* @deprecated 4.0.0..5.0.0 Use DefaultAdminService::hasDefaultAdmin() instead
*/ */
public static function has_default_admin() public static function has_default_admin()
{ {
return !empty(self::$default_username) && !empty(self::$default_password); Deprecation::notice('5.0.0', 'Please use DefaultAdminService::hasDefaultAdmin()');
return DefaultAdminService::hasDefaultAdmin();
} }
/** /**
* Get default admin username * Get default admin username
* *
* @deprecated 4.0.0..5.0.0 Use DefaultAdminService::getDefaultAdminUsername()
* @return string * @return string
*/ */
public static function default_admin_username() public static function default_admin_username()
{ {
return self::$default_username; Deprecation::notice('5.0.0', 'Please use DefaultAdminService::getDefaultAdminUsername()');
return DefaultAdminService::getDefaultAdminUsername();
} }
/** /**
* Get default admin password * Get default admin password
* *
* @deprecated 4.0.0..5.0.0 Use DefaultAdminService::getDefaultAdminPassword()
* @return string * @return string
*/ */
public static function default_admin_password() public static function default_admin_password()
{ {
return self::$default_password; Deprecation::notice('5.0.0', 'Please use DefaultAdminService::getDefaultAdminPassword()');
return DefaultAdminService::getDefaultAdminPassword();
} }
/** /**
@ -1115,16 +1081,16 @@ class Security extends Controller implements TemplateGlobalProvider
$algorithm = self::config()->get('password_encryption_algorithm'); $algorithm = self::config()->get('password_encryption_algorithm');
} }
$e = PasswordEncryptor::create_for_algorithm($algorithm); $encryptor = PasswordEncryptor::create_for_algorithm($algorithm);
// New salts will only need to be generated if the password is hashed for the first time // New salts will only need to be generated if the password is hashed for the first time
$salt = ($salt) ? $salt : $e->salt($password); $salt = ($salt) ? $salt : $encryptor->salt($password);
return array( return array(
'password' => $e->encrypt($password, $salt, $member), 'password' => $encryptor->encrypt($password, $salt, $member),
'salt' => $salt, 'salt' => $salt,
'algorithm' => $algorithm, 'algorithm' => $algorithm,
'encryptor' => $e 'encryptor' => $encryptor
); );
} }

View File

@ -46,14 +46,14 @@
* - SS_SEND_ALL_EMAILS_FROM: If you set this define, all emails will be send from this address. * - SS_SEND_ALL_EMAILS_FROM: If you set this define, all emails will be send from this address.
*/ */
use Monolog\Logger;
use Monolog\Handler\StreamHandler; use Monolog\Handler\StreamHandler;
use Monolog\Logger;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use SilverStripe\Control\Email\Email; use SilverStripe\Control\Email\Email;
use SilverStripe\Core\Injector\Injector; use SilverStripe\Core\Injector\Injector;
use SilverStripe\Dev\Install\DatabaseAdapterRegistry; use SilverStripe\Dev\Install\DatabaseAdapterRegistry;
use SilverStripe\Security\BasicAuth; use SilverStripe\Security\BasicAuth;
use SilverStripe\Security\Security; use SilverStripe\Security\DefaultAdminService;
global $database; global $database;
@ -131,7 +131,7 @@ if ($defaultAdminUser = getenv('SS_DEFAULT_ADMIN_USERNAME')) {
E_USER_ERROR E_USER_ERROR
); );
} else { } else {
Security::setDefaultAdmin($defaultAdminUser, $defaultAdminPass); DefaultAdminService::setDefaultAdmin($defaultAdminUser, $defaultAdminPass);
} }
} }
if ($useBasicAuth = getenv('SS_USE_BASIC_AUTH')) { if ($useBasicAuth = getenv('SS_USE_BASIC_AUTH')) {

View File

@ -5,6 +5,7 @@ namespace SilverStripe\Security\Tests;
use SilverStripe\Core\Injector\Injector; use SilverStripe\Core\Injector\Injector;
use SilverStripe\ORM\DataModel; use SilverStripe\ORM\DataModel;
use SilverStripe\ORM\FieldType\DBDatetime; use SilverStripe\ORM\FieldType\DBDatetime;
use SilverStripe\ORM\ValidationResult;
use SilverStripe\Security\Authenticator; use SilverStripe\Security\Authenticator;
use SilverStripe\Security\MemberAuthenticator\CMSMemberAuthenticator; use SilverStripe\Security\MemberAuthenticator\CMSMemberAuthenticator;
use SilverStripe\Security\MemberAuthenticator\CMSMemberLoginForm; use SilverStripe\Security\MemberAuthenticator\CMSMemberLoginForm;
@ -16,6 +17,7 @@ use SilverStripe\Security\IdentityStore;
use SilverStripe\Core\Config\Config; use SilverStripe\Core\Config\Config;
use SilverStripe\Dev\SapphireTest; use SilverStripe\Dev\SapphireTest;
use SilverStripe\Control\HTTPRequest; use SilverStripe\Control\HTTPRequest;
use SilverStripe\Security\DefaultAdminService;
class MemberAuthenticatorTest extends SapphireTest class MemberAuthenticatorTest extends SapphireTest
{ {
@ -29,29 +31,35 @@ class MemberAuthenticatorTest extends SapphireTest
{ {
parent::setUp(); parent::setUp();
$this->defaultUsername = Security::default_admin_username(); if (DefaultAdminService::hasDefaultAdmin()) {
$this->defaultPassword = Security::default_admin_password(); $this->defaultUsername = DefaultAdminService::getDefaultAdminUsername();
Security::clear_default_admin(); $this->defaultPassword = DefaultAdminService::getDefaultAdminPassword();
Security::setDefaultAdmin('admin', 'password'); DefaultAdminService::clearDefaultAdmin();
} else {
$this->defaultUsername = null;
$this->defaultPassword = null;
}
DefaultAdminService::clearDefaultAdmin();
DefaultAdminService::setDefaultAdmin('admin', 'password');
} }
protected function tearDown() protected function tearDown()
{ {
Security::setDefaultAdmin($this->defaultUsername, $this->defaultPassword); DefaultAdminService::clearDefaultAdmin();
if ($this->defaultUsername) {
DefaultAdminService::setDefaultAdmin($this->defaultUsername, $this->defaultPassword);
}
parent::tearDown(); parent::tearDown();
} }
public function testCustomIdentifierField() public function testCustomIdentifierField()
{ {
Member::config()->set('unique_identifier_field', 'Username');
$origField = Member::config()->unique_identifier_field; $label = Member::singleton()
Member::config()->unique_identifier_field = 'Username'; ->fieldLabel(Member::config()->get('unique_identifier_field'));
$label=singleton(Member::class)->fieldLabel(Member::config()->unique_identifier_field);
$this->assertEquals($label, 'Username'); $this->assertEquals($label, 'Username');
Member::config()->unique_identifier_field = $origField;
} }
public function testGenerateLoginForm() public function testGenerateLoginForm()
@ -106,6 +114,7 @@ class MemberAuthenticatorTest extends SapphireTest
$this->assertNotEmpty($tempID); $this->assertNotEmpty($tempID);
// Test correct login // Test correct login
/** @var ValidationResult $message */
$result = $authenticator->authenticate( $result = $authenticator->authenticate(
array( array(
'tempid' => $tempID, 'tempid' => $tempID,
@ -143,6 +152,7 @@ class MemberAuthenticatorTest extends SapphireTest
$authenticator = new MemberAuthenticator(); $authenticator = new MemberAuthenticator();
// Test correct login // Test correct login
/** @var ValidationResult $message */
$result = $authenticator->authenticate( $result = $authenticator->authenticate(
array( array(
'Email' => 'admin', 'Email' => 'admin',
@ -151,7 +161,7 @@ class MemberAuthenticatorTest extends SapphireTest
$message $message
); );
$this->assertNotEmpty($result); $this->assertNotEmpty($result);
$this->assertEquals($result->Email, Security::default_admin_username()); $this->assertEquals($result->Email, DefaultAdminService::getDefaultAdminUsername());
$this->assertTrue($message->isValid()); $this->assertTrue($message->isValid());
// Test incorrect login // Test incorrect login
@ -174,8 +184,8 @@ class MemberAuthenticatorTest extends SapphireTest
{ {
$authenticator = new MemberAuthenticator(); $authenticator = new MemberAuthenticator();
Config::inst()->update(Member::class, 'lock_out_after_incorrect_logins', 1); Config::modify()->set(Member::class, 'lock_out_after_incorrect_logins', 1);
Config::inst()->update(Member::class, 'lock_out_delay_mins', 10); Config::modify()->set(Member::class, 'lock_out_delay_mins', 10);
DBDatetime::set_mock_now('2016-04-18 00:00:00'); DBDatetime::set_mock_now('2016-04-18 00:00:00');
// Test correct login // Test correct login
@ -186,7 +196,9 @@ class MemberAuthenticatorTest extends SapphireTest
] ]
); );
$this->assertFalse(Member::default_admin()->canLogin()->isValid()); $defaultAdmin = DefaultAdminService::singleton()->findOrCreateDefaultAdmin();
$this->assertEquals('2016-04-18 00:10:00', Member::default_admin()->LockedOutUntil); $this->assertNotNull($defaultAdmin);
$this->assertFalse($defaultAdmin->canLogin());
$this->assertEquals('2016-04-18 00:10:00', $defaultAdmin->LockedOutUntil);
} }
} }

View File

@ -2,26 +2,27 @@
namespace SilverStripe\Security\Tests; namespace SilverStripe\Security\Tests;
use SilverStripe\Control\Cookie;
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\Control\Cookie;
use SilverStripe\i18n\i18n; use SilverStripe\i18n\i18n;
use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\DB; use SilverStripe\ORM\DB;
use SilverStripe\ORM\FieldType\DBDatetime; use SilverStripe\ORM\FieldType\DBDatetime;
use SilverStripe\Security\Member; use SilverStripe\ORM\ValidationException;
use SilverStripe\Security\MemberAuthenticator\SessionAuthenticationHandler;
use SilverStripe\Security\Security;
use SilverStripe\Security\MemberPassword;
use SilverStripe\Security\Group; use SilverStripe\Security\Group;
use SilverStripe\Security\Permission;
use SilverStripe\Security\IdentityStore; use SilverStripe\Security\IdentityStore;
use SilverStripe\Security\PasswordEncryptor_Blowfish; use SilverStripe\Security\Member;
use SilverStripe\Security\RememberLoginHash;
use SilverStripe\Security\Member_Validator; use SilverStripe\Security\Member_Validator;
use SilverStripe\Security\MemberAuthenticator\MemberAuthenticator;
use SilverStripe\Security\MemberAuthenticator\SessionAuthenticationHandler;
use SilverStripe\Security\MemberPassword;
use SilverStripe\Security\PasswordEncryptor_Blowfish;
use SilverStripe\Security\Permission;
use SilverStripe\Security\RememberLoginHash;
use SilverStripe\Security\Security;
use SilverStripe\Security\Tests\MemberTest\FieldsExtension; use SilverStripe\Security\Tests\MemberTest\FieldsExtension;
use SilverStripe\Control\HTTPRequest;
class MemberTest extends FunctionalTest class MemberTest extends FunctionalTest
{ {
@ -50,24 +51,13 @@ class MemberTest extends FunctionalTest
{ {
parent::setUp(); parent::setUp();
$this->orig['Member_unique_identifier_field'] = Member::config()->unique_identifier_field; Member::config()->set('unique_identifier_field', 'Email');
Member::config()->unique_identifier_field = 'Email';
Member::set_password_validator(null); Member::set_password_validator(null);
} }
protected function tearDown()
{
Member::config()->unique_identifier_field = $this->orig['Member_unique_identifier_field'];
parent::tearDown();
}
/**
* @expectedException \SilverStripe\ORM\ValidationException
*/
public function testWriteDoesntMergeNewRecordWithExistingMember() public function testWriteDoesntMergeNewRecordWithExistingMember()
{ {
$this->expectException(ValidationException::class);
$m1 = new Member(); $m1 = new Member();
$m1->Email = 'member@test.com'; $m1->Email = 'member@test.com';
$m1->write(); $m1->write();
@ -101,7 +91,7 @@ class MemberTest extends FunctionalTest
$memberWithPassword->write(); $memberWithPassword->write();
$this->assertEquals( $this->assertEquals(
$memberWithPassword->PasswordEncryption, $memberWithPassword->PasswordEncryption,
Security::config()->password_encryption_algorithm, Security::config()->get('password_encryption_algorithm'),
'Password encryption is set for new member records on first write (with setting "Password")' 'Password encryption is set for new member records on first write (with setting "Password")'
); );
@ -120,8 +110,7 @@ class MemberTest extends FunctionalTest
$member->PasswordEncryption = 'sha1_v2.4'; $member->PasswordEncryption = 'sha1_v2.4';
$member->write(); $member->write();
$origAlgo = Security::config()->password_encryption_algorithm; Security::config()->set('password_encryption_algorithm', 'none');
Security::config()->password_encryption_algorithm = 'none';
$member->Password = 'mynewpassword'; $member->Password = 'mynewpassword';
$member->write(); $member->write();
@ -130,10 +119,9 @@ class MemberTest extends FunctionalTest
$member->PasswordEncryption, $member->PasswordEncryption,
'sha1_v2.4' 'sha1_v2.4'
); );
$result = $member->checkPassword('mynewpassword'); $auth = new MemberAuthenticator();
$result = $auth->checkPassword($member, 'mynewpassword');
$this->assertTrue($result->isValid()); $this->assertTrue($result->isValid());
Security::config()->password_encryption_algorithm = $origAlgo;
} }
public function testKeepsEncryptionOnEmptyPasswords() public function testKeepsEncryptionOnEmptyPasswords()
@ -150,16 +138,19 @@ class MemberTest extends FunctionalTest
$member->PasswordEncryption, $member->PasswordEncryption,
'sha1_v2.4' 'sha1_v2.4'
); );
$result = $member->checkPassword(''); $auth = new MemberAuthenticator();
$result = $auth->checkPassword($member, '');
$this->assertTrue($result->isValid()); $this->assertTrue($result->isValid());
} }
public function testSetPassword() public function testSetPassword()
{ {
/** @var Member $member */
$member = $this->objFromFixture(Member::class, 'test'); $member = $this->objFromFixture(Member::class, 'test');
$member->Password = "test1"; $member->Password = "test1";
$member->write(); $member->write();
$result = $member->checkPassword('test1'); $auth = new MemberAuthenticator();
$result = $auth->checkPassword($member, 'test1');
$this->assertTrue($result->isValid()); $this->assertTrue($result->isValid());
} }
@ -168,6 +159,7 @@ class MemberTest extends FunctionalTest
*/ */
public function testPasswordChangeLogging() public function testPasswordChangeLogging()
{ {
/** @var Member $member */
$member = $this->objFromFixture(Member::class, 'test'); $member = $this->objFromFixture(Member::class, 'test');
$this->assertNotNull($member); $this->assertNotNull($member);
$member->Password = "test1"; $member->Password = "test1";
@ -179,7 +171,7 @@ class MemberTest extends FunctionalTest
$member->Password = "test3"; $member->Password = "test3";
$member->write(); $member->write();
$passwords = DataObject::get("SilverStripe\\Security\\MemberPassword", "\"MemberID\" = $member->ID", "\"Created\" DESC, \"ID\" DESC") $passwords = DataObject::get(MemberPassword::class, "\"MemberID\" = $member->ID", "\"Created\" DESC, \"ID\" DESC")
->getIterator(); ->getIterator();
$this->assertNotNull($passwords); $this->assertNotNull($passwords);
$passwords->rewind(); $passwords->rewind();
@ -215,6 +207,7 @@ class MemberTest extends FunctionalTest
$this->clearEmails(); $this->clearEmails();
/** @var Member $member */
$member = $this->objFromFixture(Member::class, 'test'); $member = $this->objFromFixture(Member::class, 'test');
$this->assertNotNull($member); $this->assertNotNull($member);
$valid = $member->changePassword('32asDF##$$%%'); $valid = $member->changePassword('32asDF##$$%%');
@ -236,6 +229,7 @@ class MemberTest extends FunctionalTest
$this->clearEmails(); $this->clearEmails();
$this->autoFollowRedirection = false; $this->autoFollowRedirection = false;
/** @var Member $member */
$member = $this->objFromFixture(Member::class, 'test'); $member = $this->objFromFixture(Member::class, 'test');
$this->assertNotNull($member); $this->assertNotNull($member);
@ -353,8 +347,9 @@ class MemberTest extends FunctionalTest
*/ */
public function testPasswordExpirySetting() public function testPasswordExpirySetting()
{ {
Member::config()->password_expiry_days = 90; Member::config()->set('password_expiry_days', 90);
/** @var Member $member */
$member = $this->objFromFixture(Member::class, 'test'); $member = $this->objFromFixture(Member::class, 'test');
$this->assertNotNull($member); $this->assertNotNull($member);
$valid = $member->changePassword("Xx?1234234"); $valid = $member->changePassword("Xx?1234234");
@ -363,7 +358,7 @@ class MemberTest extends FunctionalTest
$expiryDate = date('Y-m-d', time() + 90*86400); $expiryDate = date('Y-m-d', time() + 90*86400);
$this->assertEquals($expiryDate, $member->PasswordExpiry); $this->assertEquals($expiryDate, $member->PasswordExpiry);
Member::config()->password_expiry_days = null; Member::config()->set('password_expiry_days', null);
$valid = $member->changePassword("Xx?1234235"); $valid = $member->changePassword("Xx?1234235");
$this->assertTrue($valid->isValid()); $this->assertTrue($valid->isValid());
@ -372,6 +367,7 @@ class MemberTest extends FunctionalTest
public function testIsPasswordExpired() public function testIsPasswordExpired()
{ {
/** @var Member $member */
$member = $this->objFromFixture(Member::class, 'test'); $member = $this->objFromFixture(Member::class, 'test');
$this->assertNotNull($member); $this->assertNotNull($member);
$this->assertFalse($member->isPasswordExpired()); $this->assertFalse($member->isPasswordExpired());
@ -394,14 +390,13 @@ class MemberTest extends FunctionalTest
} }
public function testInGroups() public function testInGroups()
{ {
/** @var Member $staffmember */
$staffmember = $this->objFromFixture(Member::class, 'staffmember'); $staffmember = $this->objFromFixture(Member::class, 'staffmember');
$managementmember = $this->objFromFixture(Member::class, 'managementmember'); /** @var Member $ceomember */
$accountingmember = $this->objFromFixture(Member::class, 'accountingmember');
$ceomember = $this->objFromFixture(Member::class, 'ceomember'); $ceomember = $this->objFromFixture(Member::class, 'ceomember');
$staffgroup = $this->objFromFixture(Group::class, 'staffgroup'); $staffgroup = $this->objFromFixture(Group::class, 'staffgroup');
$managementgroup = $this->objFromFixture(Group::class, 'managementgroup'); $managementgroup = $this->objFromFixture(Group::class, 'managementgroup');
$accountinggroup = $this->objFromFixture(Group::class, 'accountinggroup');
$ceogroup = $this->objFromFixture(Group::class, 'ceogroup'); $ceogroup = $this->objFromFixture(Group::class, 'ceogroup');
$this->assertTrue( $this->assertTrue(
@ -420,7 +415,9 @@ class MemberTest extends FunctionalTest
public function testAddToGroupByCode() public function testAddToGroupByCode()
{ {
/** @var Member $grouplessMember */
$grouplessMember = $this->objFromFixture(Member::class, 'grouplessmember'); $grouplessMember = $this->objFromFixture(Member::class, 'grouplessmember');
/** @var Group $memberlessGroup */
$memberlessGroup = $this->objFromFixture(Group::class, 'memberlessgroup'); $memberlessGroup = $this->objFromFixture(Group::class, 'memberlessgroup');
$this->assertFalse($grouplessMember->Groups()->exists()); $this->assertFalse($grouplessMember->Groups()->exists());
@ -434,6 +431,7 @@ class MemberTest extends FunctionalTest
$grouplessMember->addToGroupByCode('somegroupthatwouldneverexist', 'New Group'); $grouplessMember->addToGroupByCode('somegroupthatwouldneverexist', 'New Group');
$this->assertEquals($grouplessMember->Groups()->count(), 2); $this->assertEquals($grouplessMember->Groups()->count(), 2);
/** @var Group $group */
$group = DataObject::get_one( $group = DataObject::get_one(
Group::class, Group::class,
array( array(
@ -447,7 +445,9 @@ class MemberTest extends FunctionalTest
public function testRemoveFromGroupByCode() public function testRemoveFromGroupByCode()
{ {
/** @var Member $grouplessMember */
$grouplessMember = $this->objFromFixture(Member::class, 'grouplessmember'); $grouplessMember = $this->objFromFixture(Member::class, 'grouplessmember');
/** @var Group $memberlessGroup */
$memberlessGroup = $this->objFromFixture(Group::class, 'memberlessgroup'); $memberlessGroup = $this->objFromFixture(Group::class, 'memberlessgroup');
$this->assertFalse($grouplessMember->Groups()->exists()); $this->assertFalse($grouplessMember->Groups()->exists());
@ -461,6 +461,7 @@ class MemberTest extends FunctionalTest
$grouplessMember->addToGroupByCode('somegroupthatwouldneverexist', 'New Group'); $grouplessMember->addToGroupByCode('somegroupthatwouldneverexist', 'New Group');
$this->assertEquals($grouplessMember->Groups()->count(), 2); $this->assertEquals($grouplessMember->Groups()->count(), 2);
/** @var Group $group */
$group = DataObject::get_one(Group::class, "\"Code\" = 'somegroupthatwouldneverexist'"); $group = DataObject::get_one(Group::class, "\"Code\" = 'somegroupthatwouldneverexist'");
$this->assertNotNull($group); $this->assertNotNull($group);
$this->assertEquals($group->Code, 'somegroupthatwouldneverexist'); $this->assertEquals($group->Code, 'somegroupthatwouldneverexist');
@ -476,14 +477,20 @@ class MemberTest extends FunctionalTest
public function testInGroup() public function testInGroup()
{ {
/** @var Member $staffmember */
$staffmember = $this->objFromFixture(Member::class, 'staffmember'); $staffmember = $this->objFromFixture(Member::class, 'staffmember');
/** @var Member $managementmember */
$managementmember = $this->objFromFixture(Member::class, 'managementmember'); $managementmember = $this->objFromFixture(Member::class, 'managementmember');
/** @var Member $accountingmember */
$accountingmember = $this->objFromFixture(Member::class, 'accountingmember'); $accountingmember = $this->objFromFixture(Member::class, 'accountingmember');
/** @var Member $ceomember */
$ceomember = $this->objFromFixture(Member::class, 'ceomember'); $ceomember = $this->objFromFixture(Member::class, 'ceomember');
/** @var Group $staffgroup */
$staffgroup = $this->objFromFixture(Group::class, 'staffgroup'); $staffgroup = $this->objFromFixture(Group::class, 'staffgroup');
/** @var Group $managementgroup */
$managementgroup = $this->objFromFixture(Group::class, 'managementgroup'); $managementgroup = $this->objFromFixture(Group::class, 'managementgroup');
$accountinggroup = $this->objFromFixture(Group::class, 'accountinggroup'); /** @var Group $ceogroup */
$ceogroup = $this->objFromFixture(Group::class, 'ceogroup'); $ceogroup = $this->objFromFixture(Group::class, 'ceogroup');
$this->assertTrue( $this->assertTrue(
@ -614,6 +621,7 @@ class MemberTest extends FunctionalTest
*/ */
public function testName() public function testName()
{ {
/** @var Member $member */
$member = $this->objFromFixture(Member::class, 'test'); $member = $this->objFromFixture(Member::class, 'test');
$member->setName('Test Some User'); $member->setName('Test Some User');
$this->assertEquals('Test Some User', $member->getName()); $this->assertEquals('Test Some User', $member->getName());
@ -645,8 +653,11 @@ class MemberTest extends FunctionalTest
public function testOnChangeGroups() public function testOnChangeGroups()
{ {
/** @var Group $staffGroup */
$staffGroup = $this->objFromFixture(Group::class, 'staffgroup'); $staffGroup = $this->objFromFixture(Group::class, 'staffgroup');
/** @var Member $staffMember */
$staffMember = $this->objFromFixture(Member::class, 'staffmember'); $staffMember = $this->objFromFixture(Member::class, 'staffmember');
/** @var Member $adminMember */
$adminMember = $this->objFromFixture(Member::class, 'admin'); $adminMember = $this->objFromFixture(Member::class, 'admin');
$newAdminGroup = new Group(array('Title' => 'newadmin')); $newAdminGroup = new Group(array('Title' => 'newadmin'));
$newAdminGroup->write(); $newAdminGroup->write();
@ -685,7 +696,9 @@ class MemberTest extends FunctionalTest
*/ */
public function testOnChangeGroupsByAdd() public function testOnChangeGroupsByAdd()
{ {
/** @var Member $staffMember */
$staffMember = $this->objFromFixture(Member::class, 'staffmember'); $staffMember = $this->objFromFixture(Member::class, 'staffmember');
/** @var Member $adminMember */
$adminMember = $this->objFromFixture(Member::class, 'admin'); $adminMember = $this->objFromFixture(Member::class, 'admin');
// Setup new admin group // Setup new admin group
@ -736,6 +749,7 @@ class MemberTest extends FunctionalTest
*/ */
public function testOnChangeGroupsBySetIDList() public function testOnChangeGroupsBySetIDList()
{ {
/** @var Member $staffMember */
$staffMember = $this->objFromFixture(Member::class, 'staffmember'); $staffMember = $this->objFromFixture(Member::class, 'staffmember');
// Setup new admin group // Setup new admin group
@ -865,7 +879,7 @@ class MemberTest extends FunctionalTest
$m2 = new Member(); $m2 = new Member();
$m2->PasswordEncryption = 'blowfish'; $m2->PasswordEncryption = 'blowfish';
$m2->Salt = $enc->salt('456'); $m2->Salt = $enc->salt('456');
$m2Token = $m2->generateAutologinTokenAndStoreHash(); $m2->generateAutologinTokenAndStoreHash();
$this->assertTrue($m1->validateAutoLoginToken($m1Token), 'Passes token validity test against matching member.'); $this->assertTrue($m1->validateAutoLoginToken($m1Token), 'Passes token validity test against matching member.');
$this->assertFalse($m2->validateAutoLoginToken($m1Token), 'Fails token validity test against other member.'); $this->assertFalse($m2->validateAutoLoginToken($m1Token), 'Fails token validity test against other member.');
@ -873,12 +887,14 @@ class MemberTest extends FunctionalTest
public function testRememberMeHashGeneration() public function testRememberMeHashGeneration()
{ {
/** @var Member $m1 */
$m1 = $this->objFromFixture(Member::class, 'grouplessmember'); $m1 = $this->objFromFixture(Member::class, 'grouplessmember');
Injector::inst()->get(IdentityStore::class)->logIn($m1, true); Injector::inst()->get(IdentityStore::class)->logIn($m1, true);
$hashes = RememberLoginHash::get()->filter('MemberID', $m1->ID); $hashes = RememberLoginHash::get()->filter('MemberID', $m1->ID);
$this->assertEquals($hashes->count(), 1); $this->assertEquals($hashes->count(), 1);
/** @var RememberLoginHash $firstHash */
$firstHash = $hashes->first(); $firstHash = $hashes->first();
$this->assertNotNull($firstHash->DeviceID); $this->assertNotNull($firstHash->DeviceID);
$this->assertNotNull($firstHash->Hash); $this->assertNotNull($firstHash->Hash);
@ -893,6 +909,7 @@ class MemberTest extends FunctionalTest
Injector::inst()->get(IdentityStore::class)->logIn($m1, true); Injector::inst()->get(IdentityStore::class)->logIn($m1, true);
/** @var RememberLoginHash $firstHash */
$firstHash = RememberLoginHash::get()->filter('MemberID', $m1->ID)->first(); $firstHash = RememberLoginHash::get()->filter('MemberID', $m1->ID)->first();
$this->assertNotNull($firstHash); $this->assertNotNull($firstHash);
@ -966,11 +983,10 @@ class MemberTest extends FunctionalTest
public function testExpiredRememberMeHashAutologin() public function testExpiredRememberMeHashAutologin()
{ {
/** /** @var Member $m1 */
* @var Member $m1
*/
$m1 = $this->objFromFixture(Member::class, 'noexpiry'); $m1 = $this->objFromFixture(Member::class, 'noexpiry');
Injector::inst()->get(IdentityStore::class)->logIn($m1, true); Injector::inst()->get(IdentityStore::class)->logIn($m1, true);
/** @var RememberLoginHash $firstHash */
$firstHash = RememberLoginHash::get()->filter('MemberID', $m1->ID)->first(); $firstHash = RememberLoginHash::get()->filter('MemberID', $m1->ID)->first();
$this->assertNotNull($firstHash); $this->assertNotNull($firstHash);
@ -1026,6 +1042,7 @@ class MemberTest extends FunctionalTest
public function testRememberMeMultipleDevices() public function testRememberMeMultipleDevices()
{ {
/** @var Member $m1 */
$m1 = $this->objFromFixture(Member::class, 'noexpiry'); $m1 = $this->objFromFixture(Member::class, 'noexpiry');
// First device // First device
@ -1035,10 +1052,12 @@ class MemberTest extends FunctionalTest
Injector::inst()->get(IdentityStore::class)->logIn($m1, true); Injector::inst()->get(IdentityStore::class)->logIn($m1, true);
// Hash of first device // Hash of first device
/** @var RememberLoginHash $firstHash */
$firstHash = RememberLoginHash::get()->filter('MemberID', $m1->ID)->first(); $firstHash = RememberLoginHash::get()->filter('MemberID', $m1->ID)->first();
$this->assertNotNull($firstHash); $this->assertNotNull($firstHash);
// Hash of second device // Hash of second device
/** @var RememberLoginHash $secondHash */
$secondHash = RememberLoginHash::get()->filter('MemberID', $m1->ID)->last(); $secondHash = RememberLoginHash::get()->filter('MemberID', $m1->ID)->last();
$this->assertNotNull($secondHash); $this->assertNotNull($secondHash);
@ -1093,7 +1112,7 @@ class MemberTest extends FunctionalTest
// Logging out from the second device - only one device being logged out // Logging out from the second device - only one device being logged out
RememberLoginHash::config()->update('logout_across_devices', false); RememberLoginHash::config()->update('logout_across_devices', false);
$response = $this->get( $this->get(
'Security/logout', 'Security/logout',
$this->session(), $this->session(),
null, null,
@ -1110,7 +1129,7 @@ class MemberTest extends FunctionalTest
// Logging out from any device when all login hashes should be removed // Logging out from any device when all login hashes should be removed
RememberLoginHash::config()->update('logout_across_devices', true); RememberLoginHash::config()->update('logout_across_devices', true);
Injector::inst()->get(IdentityStore::class)->logIn($m1, true); Injector::inst()->get(IdentityStore::class)->logIn($m1, true);
$response = $this->get('Security/logout', $this->session()); $this->get('Security/logout', $this->session());
$this->assertEquals( $this->assertEquals(
RememberLoginHash::get()->filter('MemberID', $m1->ID)->count(), RememberLoginHash::get()->filter('MemberID', $m1->ID)->count(),
0 0
@ -1152,6 +1171,7 @@ class MemberTest extends FunctionalTest
//set up the config variables to enable login lockouts //set up the config variables to enable login lockouts
Member::config()->update('lock_out_after_incorrect_logins', $maxFailedLoginsAllowed); Member::config()->update('lock_out_after_incorrect_logins', $maxFailedLoginsAllowed);
/** @var Member $member */
$member = $this->objFromFixture(Member::class, 'test'); $member = $this->objFromFixture(Member::class, 'test');
$failedLoginCount = $member->FailedLoginCount; $failedLoginCount = $member->FailedLoginCount;
@ -1165,7 +1185,7 @@ class MemberTest extends FunctionalTest
); );
$this->assertTrue( $this->assertTrue(
$member->canLogin()->isValid(), $member->canLogin(),
"Member has been locked out too early" "Member has been locked out too early"
); );
} }
@ -1175,7 +1195,9 @@ class MemberTest extends FunctionalTest
{ {
// clear custom requirements for this test // clear custom requirements for this test
Member_Validator::config()->update('customRequired', null); Member_Validator::config()->update('customRequired', null);
/** @var Member $memberA */
$memberA = $this->objFromFixture(Member::class, 'admin'); $memberA = $this->objFromFixture(Member::class, 'admin');
/** @var Member $memberB */
$memberB = $this->objFromFixture(Member::class, 'test'); $memberB = $this->objFromFixture(Member::class, 'test');
// create a blank form // create a blank form

View File

@ -2,17 +2,17 @@
namespace SilverStripe\Security\Tests; namespace SilverStripe\Security\Tests;
use SilverStripe\Security\Security;
use SilverStripe\Security\Permission;
use SilverStripe\Security\Member;
use SilverStripe\Dev\SapphireTest; use SilverStripe\Dev\SapphireTest;
use SilverStripe\Security\Member;
use SilverStripe\Security\Permission;
use SilverStripe\Security\DefaultAdminService;
class SecurityDefaultAdminTest extends SapphireTest class SecurityDefaultAdminTest extends SapphireTest
{ {
protected $usesDatabase = true; protected $usesDatabase = true;
protected $defaultUsername = null; protected $defaultUsername = null;
protected $defaultPassword = null; protected $defaultPassword = null;
protected function setUp() protected function setUp()
@ -26,33 +26,41 @@ class SecurityDefaultAdminTest extends SapphireTest
} }
self::empty_temp_db(); self::empty_temp_db();
$this->defaultUsername = Security::default_admin_username(); if (DefaultAdminService::hasDefaultAdmin()) {
$this->defaultPassword = Security::default_admin_password(); $this->defaultUsername = DefaultAdminService::getDefaultAdminUsername();
Security::clear_default_admin(); $this->defaultPassword = DefaultAdminService::getDefaultAdminPassword();
Security::setDefaultAdmin('admin', 'password'); DefaultAdminService::clearDefaultAdmin();
} else {
$this->defaultUsername = null;
$this->defaultPassword = null;
}
DefaultAdminService::setDefaultAdmin('admin', 'password');
Permission::reset(); Permission::reset();
} }
protected function tearDown() protected function tearDown()
{ {
Security::setDefaultAdmin($this->defaultUsername, $this->defaultPassword); DefaultAdminService::clearDefaultAdmin();
if ($this->defaultUsername) {
DefaultAdminService::setDefaultAdmin($this->defaultUsername, $this->defaultPassword);
}
Permission::reset(); Permission::reset();
parent::tearDown(); parent::tearDown();
} }
public function testCheckDefaultAdmin() public function testCheckDefaultAdmin()
{ {
$this->assertTrue(Security::has_default_admin()); $this->assertTrue(DefaultAdminService::hasDefaultAdmin());
$this->assertTrue( $this->assertTrue(
Security::check_default_admin('admin', 'password'), DefaultAdminService::isDefaultAdminCredentials('admin', 'password'),
'Succeeds with correct username and password' 'Succeeds with correct username and password'
); );
$this->assertFalse( $this->assertFalse(
Security::check_default_admin('wronguser', 'password'), DefaultAdminService::isDefaultAdminCredentials('wronguser', 'password'),
'Fails with incorrect username' 'Fails with incorrect username'
); );
$this->assertFalse( $this->assertFalse(
Security::check_default_admin('admin', 'wrongpassword'), DefaultAdminService::isDefaultAdminCredentials('admin', 'wrongpassword'),
'Fails with incorrect password' 'Fails with incorrect password'
); );
} }
@ -62,29 +70,34 @@ class SecurityDefaultAdminTest extends SapphireTest
$adminMembers = Permission::get_members_by_permission('ADMIN'); $adminMembers = Permission::get_members_by_permission('ADMIN');
$this->assertEquals(0, $adminMembers->count()); $this->assertEquals(0, $adminMembers->count());
$admin = Security::findAnAdministrator(); $admin = DefaultAdminService::singleton()->findOrCreateDefaultAdmin();
$this->assertInstanceOf(Member::class, $admin); $this->assertInstanceOf(Member::class, $admin);
$this->assertTrue(Permission::checkMember($admin, 'ADMIN')); $this->assertTrue(Permission::checkMember($admin, 'ADMIN'));
$this->assertEquals($admin->Email, Security::default_admin_username()); $this->assertEquals($admin->Email, DefaultAdminService::getDefaultAdminUsername());
$this->assertTrue(DefaultAdminService::isDefaultAdmin($admin->Email));
$this->assertNull($admin->Password); $this->assertNull($admin->Password);
} }
public function testFindAnAdministratorWithoutDefaultAdmin() public function testFindAnAdministratorWithoutDefaultAdmin()
{ {
// Clear default admin // Clear default admin
Security::clear_default_admin(); $service = DefaultAdminService::singleton();
DefaultAdminService::clearDefaultAdmin();
$adminMembers = Permission::get_members_by_permission('ADMIN'); $adminMembers = Permission::get_members_by_permission('ADMIN');
$this->assertEquals(0, $adminMembers->count()); $this->assertEquals(0, $adminMembers->count());
$admin = Security::findAnAdministrator(); $admin = $service->findOrCreateDefaultAdmin();
$this->assertNull($admin);
$this->assertInstanceOf(Member::class, $admin); // When clearing the admin, it will not re-instate it anymore
DefaultAdminService::setDefaultAdmin('admin', 'password');
$admin = $service->findOrCreateDefaultAdmin();
$this->assertTrue(Permission::checkMember($admin, 'ADMIN')); $this->assertTrue(Permission::checkMember($admin, 'ADMIN'));
// User should be blank // User should have Email but no Password
$this->assertEmpty($admin->Email); $this->assertEquals('admin', $admin->Email);
$this->assertEmpty($admin->Password); $this->assertEmpty($admin->Password);
} }
@ -93,11 +106,11 @@ class SecurityDefaultAdminTest extends SapphireTest
$adminMembers = Permission::get_members_by_permission('ADMIN'); $adminMembers = Permission::get_members_by_permission('ADMIN');
$this->assertEquals(0, $adminMembers->count()); $this->assertEquals(0, $adminMembers->count());
$admin = Member::default_admin(); $admin = DefaultAdminService::singleton()->findOrCreateDefaultAdmin();
$this->assertInstanceOf(Member::class, $admin); $this->assertInstanceOf(Member::class, $admin);
$this->assertTrue(Permission::checkMember($admin, 'ADMIN')); $this->assertTrue(Permission::checkMember($admin, 'ADMIN'));
$this->assertEquals($admin->Email, Security::default_admin_username()); $this->assertEquals($admin->Email, DefaultAdminService::getDefaultAdminUsername());
$this->assertTrue(DefaultAdminService::isDefaultAdmin($admin->Email));
$this->assertNull($admin->Password); $this->assertNull($admin->Password);
} }
} }

View File

@ -31,14 +31,6 @@ class SecurityTest extends FunctionalTest
protected $autoFollowRedirection = false; protected $autoFollowRedirection = false;
protected $priorAuthenticators = array();
protected $priorDefaultAuthenticator = null;
protected $priorUniqueIdentifierField = null;
protected $priorRememberUsername = null;
protected static $extra_controllers = [ protected static $extra_controllers = [
SecurityTest\NullController::class, SecurityTest\NullController::class,
SecurityTest\SecuredController::class, SecurityTest\SecuredController::class,
@ -50,9 +42,6 @@ class SecurityTest extends FunctionalTest
Config::modify()->set(MemberAuthenticator::class, 'authenticators', []); Config::modify()->set(MemberAuthenticator::class, 'authenticators', []);
Config::modify()->set(MemberAuthenticator::class, 'default_authenticator', MemberAuthenticator::class); Config::modify()->set(MemberAuthenticator::class, 'default_authenticator', MemberAuthenticator::class);
// And that the unique identified field is 'Email'
$this->priorUniqueIdentifierField = Member::config()->unique_identifier_field;
$this->priorRememberUsername = Security::config()->remember_username;
/** /**
* @skipUpgrade * @skipUpgrade
*/ */
@ -63,21 +52,6 @@ class SecurityTest extends FunctionalTest
Config::modify()->merge('SilverStripe\\Control\\Director', 'alternate_base_url', '/'); Config::modify()->merge('SilverStripe\\Control\\Director', 'alternate_base_url', '/');
} }
protected function tearDown()
{
// Restore selected authenticator
// MemberAuthenticator might not actually be present
// Config::modify()->set(Authenticator::class, 'authenticators', $this->priorAuthenticators);
// Config::modify()->set(Authenticator::class, 'default_authenticator', $this->priorDefaultAuthenticator);
// Restore unique identifier field
Member::config()->unique_identifier_field = $this->priorUniqueIdentifierField;
Security::config()->remember_username = $this->priorRememberUsername;
parent::tearDown();
}
public function testAccessingAuthenticatedPageRedirectsToLoginForm() public function testAccessingAuthenticatedPageRedirectsToLoginForm()
{ {
$this->autoFollowRedirection = false; $this->autoFollowRedirection = false;