Merge pull request #6829 from sminnee/authenticator-refactor

Refactor Authenticators
This commit is contained in:
Damian Mooyman 2017-06-09 16:46:56 +12:00 committed by GitHub
commit c7f7233c4d
69 changed files with 2991 additions and 1862 deletions

View File

@ -12,12 +12,6 @@ use SilverStripe\View\Parsers\ShortcodeParser;
* Here you can make different settings for the Framework module (the core * Here you can make different settings for the Framework module (the core
* module). * module).
* *
* For example you can register the authentication methods you wish to use
* on your site, e.g. to register the OpenID authentication method type
*
* <code>
* Authenticator::register_authenticator('OpenIDAuthenticator');
* </code>
*/ */
ShortcodeParser::get('default') ShortcodeParser::get('default')

View File

@ -1,4 +1,35 @@
SilverStripe\Security\MemberLoginForm: ---
required_fields: Name: coreauthentication
- Email ---
- Password SilverStripe\Core\Injector\Injector:
SilverStripe\Security\MemberAuthenticator\SessionAuthenticationHandler:
properties:
SessionVariable: loggedInAs
SilverStripe\Security\MemberAuthenticator\CookieAuthenticationHandler:
properties:
TokenCookieName: alc_enc
DeviceCookieName: alc_device
CascadeInTo: %$SilverStripe\Security\MemberAuthenticator\SessionAuthenticationHandler
SilverStripe\Security\AuthenticationHandler:
class: SilverStripe\Security\RequestAuthenticationHandler
properties:
Handlers:
session: %$SilverStripe\Security\MemberAuthenticator\SessionAuthenticationHandler
alc: %$SilverStripe\Security\MemberAuthenticator\CookieAuthenticationHandler
---
Name: coresecurity
---
SilverStripe\Core\Injector\Injector:
SilverStripe\Security\AuthenticationRequestFilter:
properties:
AuthenticationHandler: %$SilverStripe\Security\AuthenticationHandler
SilverStripe\Control\RequestProcessor:
properties:
filters:
- %$SilverStripe\Security\AuthenticationRequestFilter
SilverStripe\Security\Security:
properties:
Authenticators:
default: %$SilverStripe\Security\MemberAuthenticator\MemberAuthenticator
cms: %$SilverStripe\Security\MemberAuthenticator\CMSMemberAuthenticator
SilverStripe\Security\IdentityStore: %$SilverStripe\Security\AuthenticationHandler

View File

@ -30,7 +30,7 @@ Example: Disallow creation of new players if the currently logged-in player is n
public function onBeforeWrite() { public function onBeforeWrite() {
// check on first write action, aka "database row creation" (ID-property is not set) // check on first write action, aka "database row creation" (ID-property is not set)
if(!$this->isInDb()) { if(!$this->isInDb()) {
$currentPlayer = Member::currentUser(); $currentPlayer = Security::getCurrentUser();
if(!$currentPlayer->IsTeamManager()) { if(!$currentPlayer->IsTeamManager()) {
user_error('Player-creation not allowed', E_USER_ERROR); user_error('Player-creation not allowed', E_USER_ERROR);

View File

@ -9,7 +9,7 @@ checks. Often it makes sense to centralize those checks on the model, regardless
The API provides four methods for this purpose: `canEdit()`, `canCreate()`, `canView()` and `canDelete()`. The API provides four methods for this purpose: `canEdit()`, `canCreate()`, `canView()` and `canDelete()`.
Since they're PHP methods, they can contain arbitrary logic matching your own requirements. They can optionally receive Since they're PHP methods, they can contain arbitrary logic matching your own requirements. They can optionally receive
a `$member` argument, and default to the currently logged in member (through `Member::currentUser()`). a `$member` argument, and default to the currently logged in member (through `Security::getCurrentUser()`).
<div class="notice" markdown="1"> <div class="notice" markdown="1">
By default, all `DataObject` subclasses can only be edited, created and viewed by users with the 'ADMIN' permission By default, all `DataObject` subclasses can only be edited, created and viewed by users with the 'ADMIN' permission

View File

@ -40,7 +40,7 @@ includes [api:Controller], [api:FormField] and [api:DataObject] instances.
```php ```php
$controller->renderWith(array('MyController', 'MyBaseController')); $controller->renderWith(array('MyController', 'MyBaseController'));
Member::currentUser()->renderWith('Member_Profile'); Security::getCurrentUser()->renderWith('Member_Profile');
``` ```
`renderWith` can be used to override the default template process. For instance, to provide an ajax version of a `renderWith` can be used to override the default template process. For instance, to provide an ajax version of a

View File

@ -109,7 +109,7 @@ we added a `SayHi` method which is unique to our extension.
**mysite/code/Page.php** **mysite/code/Page.php**
:::php :::php
$member = Member::currentUser(); $member = Security::getCurrentUser();
echo $member->SayHi; echo $member->SayHi;
// "Hi Sam" // "Hi Sam"
@ -220,7 +220,7 @@ To see what extensions are currently enabled on an object, use [api:Object::getE
:::php :::php
$member = Member::currentUser(); $member = Security::getCurrentUser();
print_r($member->getExtensionInstances()); print_r($member->getExtensionInstances());

View File

@ -24,12 +24,12 @@ next method for testing if you just need to test.
} }
**Member::currentUser()** **Security::getCurrentUser()**
Returns the full *Member* Object for the current user, returns *null* if user is not logged in. Returns the full *Member* Object for the current user, returns *null* if user is not logged in.
:::php :::php
if( $member = Member::currentUser() ) { if( $member = Security::getCurrentUser() ) {
// Work with $member // Work with $member
} else { } else {
// Do non-member stuff // Do non-member stuff

View File

@ -60,7 +60,7 @@ The PHP Logic..
$email = SilverStripe\Control\Email\Email::create() $email = SilverStripe\Control\Email\Email::create()
->setHTMLTemplate('Email\\MyCustomEmail') ->setHTMLTemplate('Email\\MyCustomEmail')
->setData(array( ->setData(array(
'Member' => Member::currentUser(), 'Member' => Security::getCurrentUser(),
'Link'=> $link, 'Link'=> $link,
)) ))
->setFrom($from) ->setFrom($from)

View File

@ -9,6 +9,7 @@ use SilverStripe\ORM\DataModel;
use SilverStripe\ORM\FieldType\DBHTMLText; use SilverStripe\ORM\FieldType\DBHTMLText;
use SilverStripe\Security\BasicAuth; use SilverStripe\Security\BasicAuth;
use SilverStripe\Security\Member; use SilverStripe\Security\Member;
use SilverStripe\Security\Security;
use SilverStripe\View\SSViewer; use SilverStripe\View\SSViewer;
use SilverStripe\View\TemplateGlobalProvider; use SilverStripe\View\TemplateGlobalProvider;
@ -575,7 +576,7 @@ class Controller extends RequestHandler implements TemplateGlobalProvider
public function can($perm, $member = null) public function can($perm, $member = null)
{ {
if (!$member) { if (!$member) {
$member = Member::currentUser(); $member = Security::getCurrentUser();
} }
if (is_array($perm)) { if (is_array($perm)) {
$perm = array_map(array($this, 'can'), $perm, array_fill(0, count($perm), $member)); $perm = array_map(array($this, 'can'), $perm, array_fill(0, count($perm), $member));

View File

@ -296,6 +296,20 @@ class RequestHandler extends ViewableData
return null; return null;
} }
/**
* @param string $link
* @return string
*/
protected function addBackURLParam($link)
{
$backURL = $this->getBackURL();
if ($backURL) {
return Controller::join_links($link, '?BackURL=' . urlencode($backURL));
}
return $link;
}
/** /**
* Given a request, and an action name, call that action name on this RequestHandler * Given a request, and an action name, call that action name on this RequestHandler
* *

View File

@ -5,8 +5,10 @@ namespace SilverStripe\Dev;
use SilverStripe\Control\Session; use SilverStripe\Control\Session;
use SilverStripe\Control\HTTPResponse; use SilverStripe\Control\HTTPResponse;
use SilverStripe\Core\Config\Config; use SilverStripe\Core\Config\Config;
use SilverStripe\ORM\DataObject;
use SilverStripe\Security\BasicAuth; use SilverStripe\Security\BasicAuth;
use SilverStripe\Security\Member; use SilverStripe\Security\Member;
use SilverStripe\Security\Security;
use SilverStripe\Security\SecurityToken; use SilverStripe\Security\SecurityToken;
use SilverStripe\View\SSViewer; use SilverStripe\View\SSViewer;
use PHPUnit_Framework_AssertionFailedError; use PHPUnit_Framework_AssertionFailedError;
@ -104,6 +106,8 @@ class FunctionalTest extends SapphireTest
// basis. // basis.
BasicAuth::protect_entire_site(false); BasicAuth::protect_entire_site(false);
$this->logOut();
SecurityToken::disable(); SecurityToken::disable();
} }
@ -394,24 +398,6 @@ class FunctionalTest extends SapphireTest
$this->assertTrue($expectedMatches == $actuals, $message); $this->assertTrue($expectedMatches == $actuals, $message);
} }
/**
* Log in as the given member
*
* @param Member|int|string $member The ID, fixture codename, or Member object of the member that you want to log in
*/
public function logInAs($member)
{
if (is_object($member)) {
$memberID = $member->ID;
} elseif (is_numeric($member)) {
$memberID = $member;
} else {
$memberID = $this->idFromFixture('SilverStripe\\Security\\Member', $member);
}
$this->session()->inst_set('loggedInAs', $memberID);
}
/** /**
* Use the draft (stage) site for testing. * Use the draft (stage) site for testing.
* This is helpful if you're not testing publication functionality and don't want "stage management" cluttering * This is helpful if you're not testing publication functionality and don't want "stage management" cluttering

View File

@ -25,6 +25,7 @@ use SilverStripe\Core\Resettable;
use SilverStripe\i18n\i18n; use SilverStripe\i18n\i18n;
use SilverStripe\ORM\DataExtension; use SilverStripe\ORM\DataExtension;
use SilverStripe\ORM\SS_List; use SilverStripe\ORM\SS_List;
use SilverStripe\Security\IdentityStore;
use SilverStripe\Versioned\Versioned; use SilverStripe\Versioned\Versioned;
use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\DataModel; use SilverStripe\ORM\DataModel;
@ -276,7 +277,7 @@ class SapphireTest extends PHPUnit_Framework_TestCase
if (Controller::has_curr()) { if (Controller::has_curr()) {
Controller::curr()->setSession(Session::create(array())); Controller::curr()->setSession(Session::create(array()));
} }
Security::$database_is_ready = null; Security::clear_database_is_ready();
// Set up test routes // Set up test routes
$this->setUpRoutes(); $this->setUpRoutes();
@ -1250,10 +1251,33 @@ class SapphireTest extends PHPUnit_Framework_TestCase
$this->cache_generatedMembers[$permCode] = $member; $this->cache_generatedMembers[$permCode] = $member;
} }
$member->logIn(); $this->logInAs($member);
return $member->ID; return $member->ID;
} }
/**
* Log in as the given member
*
* @param Member|int|string $member The ID, fixture codename, or Member object of the member that you want to log in
*/
public function logInAs($member)
{
if (is_numeric($member)) {
$member = DataObject::get_by_id(Member::class, $member);
} elseif (!is_object($member)) {
$member = $this->objFromFixture(Member::class, $member);
}
Injector::inst()->get(IdentityStore::class)->logIn($member);
}
/**
* Log out the current user
*/
public function logOut()
{
Injector::inst()->get(IdentityStore::class)->logOut();
}
/** /**
* Cache for logInWithPermission() * Cache for logInWithPermission()
*/ */

View File

@ -38,7 +38,7 @@ class TestSession
/** /**
* Necessary to use the mock session * Necessary to use the mock session
* created in {@link session} in the normal controller stack, * created in {@link session} in the normal controller stack,
* e.g. to overwrite Member::currentUser() with custom login data. * e.g. to overwrite Security::getCurrentUser() with custom login data.
* *
* @var Controller * @var Controller
*/ */

View File

@ -5,6 +5,7 @@ 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\Member;
use SilverStripe\Security\Security;
use SilverStripe\View\Requirements; use SilverStripe\View\Requirements;
/** /**
@ -504,7 +505,7 @@ class ConfirmedPasswordField extends FormField
} }
// Check this password is valid for the current user // Check this password is valid for the current user
$member = Member::currentUser(); $member = Security::getCurrentUser();
if (!$member) { if (!$member) {
$validator->validationError( $validator->validationError(
$name, $name,

View File

@ -228,23 +228,23 @@ class FormRequestHandler extends RequestHandler
// First, try a handler method on the controller (has been checked for allowed_actions above already) // First, try a handler method on the controller (has been checked for allowed_actions above already)
$controller = $this->form->getController(); $controller = $this->form->getController();
if ($controller && $controller->hasMethod($funcName)) { if ($controller && $controller->hasMethod($funcName)) {
return $controller->$funcName($vars, $this->form, $request); return $controller->$funcName($vars, $this->form, $request, $this);
} }
// Otherwise, try a handler method on the form request handler. // Otherwise, try a handler method on the form request handler.
if ($this->hasMethod($funcName)) { if ($this->hasMethod($funcName)) {
return $this->$funcName($vars, $this->form, $request); return $this->$funcName($vars, $this->form, $request, $this);
} }
// Otherwise, try a handler method on the form itself // Otherwise, try a handler method on the form itself
if ($this->form->hasMethod($funcName)) { if ($this->form->hasMethod($funcName)) {
return $this->form->$funcName($vars, $this->form, $request); return $this->form->$funcName($vars, $this->form, $request, $this);
} }
// Check for inline actions // Check for inline actions
$field = $this->checkFieldsForAction($this->form->Fields(), $funcName); $field = $this->checkFieldsForAction($this->form->Fields(), $funcName);
if ($field) { if ($field) {
return $field->$funcName($vars, $this->form, $request); return $field->$funcName($vars, $this->form, $request, $this);
} }
} catch (ValidationException $e) { } catch (ValidationException $e) {
// The ValdiationResult contains all the relevant metadata // The ValdiationResult contains all the relevant metadata

View File

@ -10,6 +10,7 @@ use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\FieldType\DBDatetime; use SilverStripe\ORM\FieldType\DBDatetime;
use SilverStripe\ORM\FieldType\DBHTMLText; use SilverStripe\ORM\FieldType\DBHTMLText;
use SilverStripe\Security\Member; use SilverStripe\Security\Member;
use SilverStripe\Security\Security;
use SilverStripe\View\Requirements; use SilverStripe\View\Requirements;
use SilverStripe\View\ArrayData; use SilverStripe\View\ArrayData;
@ -249,7 +250,7 @@ class GridFieldPrintButton implements GridField_HTMLProvider, GridField_ActionPr
"Header" => $header, "Header" => $header,
"ItemRows" => $itemRows, "ItemRows" => $itemRows,
"Datetime" => DBDatetime::now(), "Datetime" => DBDatetime::now(),
"Member" => Member::currentUser(), "Member" => Security::getCurrentUser(),
)); ));
return $ret; return $ret;

View File

@ -490,6 +490,17 @@ abstract class Database
*/ */
abstract public function datetimeDifferenceClause($date1, $date2); abstract public function datetimeDifferenceClause($date1, $date2);
/**
* String operator for concatenation of strings
*
* @return string
*/
public function concatOperator()
{
// @todo Make ' + ' in mssql
return ' || ';
}
/** /**
* Returns true if this database supports collations * Returns true if this database supports collations
* *

View File

@ -24,6 +24,7 @@ use SilverStripe\ORM\FieldType\DBComposite;
use SilverStripe\ORM\FieldType\DBClassName; use SilverStripe\ORM\FieldType\DBClassName;
use SilverStripe\Security\Member; use SilverStripe\Security\Member;
use SilverStripe\Security\Permission; use SilverStripe\Security\Permission;
use SilverStripe\Security\Security;
use SilverStripe\View\ViewableData; use SilverStripe\View\ViewableData;
use LogicException; use LogicException;
use InvalidArgumentException; use InvalidArgumentException;
@ -76,11 +77,11 @@ use stdClass;
* static $api_access = true; * static $api_access = true;
* *
* function canView($member = false) { * function canView($member = false) {
* if(!$member) $member = Member::currentUser(); * if(!$member) $member = Security::getCurrentUser();
* return $member->inGroup('Subscribers'); * return $member->inGroup('Subscribers');
* } * }
* function canEdit($member = false) { * function canEdit($member = false) {
* if(!$member) $member = Member::currentUser(); * if(!$member) $member = Security::getCurrentUser();
* return $member->inGroup('Editors'); * return $member->inGroup('Editors');
* } * }
* *
@ -2498,7 +2499,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
public function can($perm, $member = null, $context = array()) public function can($perm, $member = null, $context = array())
{ {
if (!$member) { if (!$member) {
$member = Member::currentUser(); $member = Security::getCurrentUser();
} }
if ($member && Permission::checkMember($member, "ADMIN")) { if ($member && Permission::checkMember($member, "ADMIN")) {

View File

@ -9,6 +9,7 @@ use SilverStripe\Forms\DateField;
use SilverStripe\i18n\i18n; use SilverStripe\i18n\i18n;
use SilverStripe\ORM\DB; use SilverStripe\ORM\DB;
use SilverStripe\Security\Member; use SilverStripe\Security\Member;
use SilverStripe\Security\Security;
/** /**
* Represents a date field. * Represents a date field.
@ -250,7 +251,7 @@ class DBDate extends DBField
public function FormatFromSettings($member = null) public function FormatFromSettings($member = null)
{ {
if (!$member) { if (!$member) {
$member = Member::currentUser(); $member = Security::getCurrentUser();
} }
// Fall back to nice // Fall back to nice

View File

@ -7,6 +7,7 @@ use SilverStripe\Forms\DatetimeField;
use SilverStripe\i18n\i18n; use SilverStripe\i18n\i18n;
use SilverStripe\ORM\DB; use SilverStripe\ORM\DB;
use SilverStripe\Security\Member; use SilverStripe\Security\Member;
use SilverStripe\Security\Security;
use SilverStripe\View\TemplateGlobalProvider; use SilverStripe\View\TemplateGlobalProvider;
use Exception; use Exception;
use InvalidArgumentException; use InvalidArgumentException;
@ -97,7 +98,7 @@ class DBDatetime extends DBDate implements TemplateGlobalProvider
public function FormatFromSettings($member = null) public function FormatFromSettings($member = null)
{ {
if (!$member) { if (!$member) {
$member = Member::currentUser(); $member = Security::getCurrentUser();
} }
// Fall back to nice // Fall back to nice

View File

@ -8,6 +8,7 @@ use SilverStripe\Forms\TimeField;
use SilverStripe\i18n\i18n; use SilverStripe\i18n\i18n;
use SilverStripe\ORM\DB; use SilverStripe\ORM\DB;
use SilverStripe\Security\Member; use SilverStripe\Security\Member;
use SilverStripe\Security\Security;
/** /**
* Represents a column in the database with the type 'Time'. * Represents a column in the database with the type 'Time'.
@ -153,7 +154,7 @@ class DBTime extends DBField
public function FormatFromSettings($member = null) public function FormatFromSettings($member = null)
{ {
if (!$member) { if (!$member) {
$member = Member::currentUser(); $member = Security::getCurrentUser();
} }
// Fall back to nice // Fall back to nice

View File

@ -0,0 +1,40 @@
<?php
namespace SilverStripe\Security;
use SilverStripe\Control\HTTPRequest;
use SilverStripe\ORM\ValidationException;
/**
* An AuthenticationHandler is responsible for providing an identity (in the form of a Member object) for
* a given HTTPRequest.
*
* It should return the authenticated Member if successful. If a Member cannot be found from the current
* request it should *not* attempt to redirect the visitor to a log-in from or 3rd party handler, as that
* is the responsibiltiy of other systems.
*/
interface AuthenticationHandler extends IdentityStore
{
/**
* Given the current request, authenticate the request for non-session authorization (outside the CMS).
*
* The Member returned from this method will be provided to the Manager for use in the OperationResolver context
* in place of the current CMS member.
*
* Authenticators can be given a priority. In this case, the authenticator with the highest priority will be
* returned first. If not provided, it will default to a low number.
*
* An example for configuring the BasicAuthAuthenticator:
*
* <code>
* SilverStripe\Security\Security:
* authentication_handlers:
* - SilverStripe\Security\BasicAuthentionHandler
* </code>
*
* @param HTTPRequest $request The current HTTP request
* @return Member|null The authenticated Member, or null if this auth mechanism isn't used.
* @throws ValidationException If authentication data exists but does not match a member.
*/
public function authenticateRequest(HTTPRequest $request);
}

View File

@ -0,0 +1,75 @@
<?php
namespace SilverStripe\Security;
use SilverStripe\Control\HTTPRequest;
use SilverStripe\Control\HTTPResponse;
use SilverStripe\Control\HTTPResponse_Exception;
use SilverStripe\Control\RequestFilter;
use SilverStripe\Control\Session;
use SilverStripe\Core\Config\Configurable;
use SilverStripe\ORM\DataModel;
use SilverStripe\ORM\ValidationException;
class AuthenticationRequestFilter implements RequestFilter
{
use Configurable;
/**
* @var AuthenticationHandler
*/
protected $authenticationHandler;
/**
* @return AuthenticationHandler
*/
public function getAuthenticationHandler()
{
return $this->authenticationHandler;
}
/**
* @param AuthenticationHandler $authenticationHandler
* @return $this
*/
public function setAuthenticationHandler(AuthenticationHandler $authenticationHandler)
{
$this->authenticationHandler = $authenticationHandler;
return $this;
}
/**
* Identify the current user from the request
*
* @param HTTPRequest $request
* @param Session $session
* @param DataModel $model
* @return bool|void
* @throws HTTPResponse_Exception
*/
public function preRequest(HTTPRequest $request, Session $session, DataModel $model)
{
try {
$this
->getAuthenticationHandler()
->authenticateRequest($request);
} catch (ValidationException $e) {
throw new HTTPResponse_Exception(
"Bad log-in details: " . $e->getMessage(),
400
);
}
}
/**
* No-op
*
* @param HTTPRequest $request
* @param HTTPResponse $response
* @param DataModel $model
* @return bool|void
*/
public function postRequest(HTTPRequest $request, HTTPResponse $response, DataModel $model)
{
}
}

View File

@ -2,11 +2,9 @@
namespace SilverStripe\Security; namespace SilverStripe\Security;
use SilverStripe\Core\Config\Configurable; use SilverStripe\ORM\ValidationResult;
use SilverStripe\Core\Extensible; use SilverStripe\Security\MemberAuthenticator\LoginHandler;
use SilverStripe\Core\Injector\Injectable; use SilverStripe\Security\MemberAuthenticator\LogoutHandler;
use SilverStripe\Control\Controller;
use SilverStripe\Forms\Form;
/** /**
* Abstract base class for an authentication method * Abstract base class for an authentication method
@ -16,125 +14,76 @@ use SilverStripe\Forms\Form;
* *
* @author Markus Lanthaler <markus@silverstripe.com> * @author Markus Lanthaler <markus@silverstripe.com>
*/ */
abstract class Authenticator interface Authenticator
{ {
use Injectable;
use Configurable;
use Extensible;
public function __construct() const LOGIN = 1;
{ const LOGOUT = 2;
$this->constructExtensions(); const CHANGE_PASSWORD = 4;
} const RESET_PASSWORD = 8;
const CMS_LOGIN = 16;
/** /**
* This variable holds all authenticators that should be used * Returns the services supported by this authenticator
* *
* @var array * The number should be a bitwise-OR of 1 or more of the following constants:
*/ * Authenticator::LOGIN, Authenticator::LOGOUT, Authenticator::CHANGE_PASSWORD,
private static $authenticators = []; * Authenticator::RESET_PASSWORD, or Authenticator::CMS_LOGIN
/**
* Used to influence the order of authenticators on the login-screen
* (default shows first).
* *
* @var string * @return int
*/ */
private static $default_authenticator = MemberAuthenticator::class; public function supportedServices();
/** /**
* Method to authenticate an user * Return RequestHandler to manage the log-in process.
* *
* @param array $RAW_data Raw data to authenticate the user * The default URL of the RequestHandler should return the initial log-in form, any other
* @param Form $form Optional: If passed, better error messages can be * URL may be added for other steps & processing.
* produced by using
* {@link Form::sessionMessage()}
* @return bool|Member Returns FALSE if authentication fails, otherwise
* the member object
*/
public static function authenticate($RAW_data, Form $form = null)
{
}
/**
* Method that creates the login form for this authentication method
* *
* @param Controller $controller The parent controller, necessary to create the * URL-handling methods may return an array [ "Form" => (form-object) ] which can then
* appropriate form action tag * be merged into a default controller.
* @return Form Returns the login form to use with this authentication
* method
*/
public static function get_login_form(Controller $controller)
{
}
/**
* Method that creates the re-authentication form for the in-CMS view
* *
* @param Controller $controller * @param string $link The base link to use for this RequestHandler
* @return LoginHandler
*/ */
public static function get_cms_login_form(Controller $controller) public function getLoginHandler($link);
{
}
/** /**
* Determine if this authenticator supports in-cms reauthentication * Return the RequestHandler to manage the log-out process.
* *
* @return bool * The default URL of the RequestHandler should log the user out immediately and destroy the session.
*/
public static function supports_cms()
{
return false;
}
/**
* Check if a given authenticator is registered
* *
* @param string $authenticator Name of the authenticator class to check * @param string $link The base link to use for this RequestHandler
* @return bool Returns TRUE if the authenticator is registered, FALSE * @return LogoutHandler
* otherwise.
*/ */
public static function is_registered($authenticator) public function getLogOutHandler($link);
{
$authenticators = self::config()->get('authenticators');
if (count($authenticators) === 0) {
$authenticators = [self::config()->get('default_authenticator')];
}
return in_array($authenticator, $authenticators, true);
}
/** /**
* Get all registered authenticators * Return RequestHandler to manage the change-password process.
* *
* @return array Returns an array with the class names of all registered * The default URL of the RequetHandler should return the initial change-password form,
* authenticators. * any other URL may be added for other steps & processing.
*
* URL-handling methods may return an array [ "Form" => (form-object) ] which can then
* be merged into a default controller.
*
* @param string $link The base link to use for this RequestHnadler
*/ */
public static function get_authenticators() public function getChangePasswordHandler($link);
{
$authenticators = self::config()->get('authenticators');
$default = self::config()->get('default_authenticator');
if (count($authenticators) === 0) {
$authenticators = [$default];
}
// put default authenticator first (mainly for tab-order on loginform)
// But only if there's no other authenticator
if (($key = array_search($default, $authenticators, true)) && count($authenticators) > 1) {
unset($authenticators[$key]);
array_unshift($authenticators, $default);
}
return $authenticators;
}
/** /**
* @return string * @param string $link
* @return mixed
*/ */
public static function get_default_authenticator() public function getLostPasswordHandler($link);
{
return self::config()->get('default_authenticator'); /**
} * Method to authenticate an user.
*
* @param array $data Raw data to authenticate the user.
* @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
*/
public function authenticate($data, &$result = null);
} }

View File

@ -2,12 +2,14 @@
namespace SilverStripe\Security; namespace SilverStripe\Security;
use SilverStripe\Control\Controller;
use SilverStripe\Control\Director; use SilverStripe\Control\Director;
use SilverStripe\Control\HTTPRequest;
use SilverStripe\Control\HTTPResponse; use SilverStripe\Control\HTTPResponse;
use SilverStripe\Control\HTTPResponse_Exception; use SilverStripe\Control\HTTPResponse_Exception;
use SilverStripe\Core\Config\Config;
use SilverStripe\Core\Config\Configurable; use SilverStripe\Core\Config\Configurable;
use SilverStripe\Dev\SapphireTest; use SilverStripe\Dev\SapphireTest;
use SilverStripe\Security\MemberAuthenticator\MemberAuthenticator;
/** /**
* Provides an interface to HTTP basic authentication. * Provides an interface to HTTP basic authentication.
@ -41,24 +43,29 @@ class BasicAuth
* @var String Message that shows in the authentication box. * @var String Message that shows in the authentication box.
* Set this value through {@link protect_entire_site()}. * Set this value through {@link protect_entire_site()}.
*/ */
private static $entire_site_protected_message = "SilverStripe test website. Use your CMS login."; private static $entire_site_protected_message = 'SilverStripe test website. Use your CMS login.';
/** /**
* Require basic authentication. Will request a username and password if none is given. * Require basic authentication. Will request a username and password if none is given.
* *
* Used by {@link Controller::init()}. * Used by {@link Controller::init()}.
* *
* @throws HTTPResponse_Exception
* *
* @param HTTPRequest $request
* @param string $realm * @param string $realm
* @param string|array $permissionCode Optional * @param string|array $permissionCode Optional
* @param boolean $tryUsingSessionLogin If true, then the method with authenticate against the * @param boolean $tryUsingSessionLogin If true, then the method with authenticate against the
* session log-in if those credentials are disabled. * session log-in if those credentials are disabled.
* @return Member|bool $member * @return bool|Member
* @throws HTTPResponse_Exception
*/ */
public static function requireLogin($realm, $permissionCode = null, $tryUsingSessionLogin = true) public static function requireLogin(
{ HTTPRequest $request,
$isRunningTests = (class_exists('SilverStripe\\Dev\\SapphireTest', false) && SapphireTest::is_running_test()); $realm,
$permissionCode = null,
$tryUsingSessionLogin = true
) {
$isRunningTests = (class_exists(SapphireTest::class, false) && SapphireTest::is_running_test());
if (!Security::database_is_ready() || (Director::is_cli() && !$isRunningTests)) { if (!Security::database_is_ready() || (Director::is_cli() && !$isRunningTests)) {
return true; return true;
} }
@ -71,25 +78,37 @@ class BasicAuth
* The follow rewrite rule must be in the sites .htaccess file to enable this workaround * The follow rewrite rule must be in the sites .htaccess file to enable this workaround
* RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] * RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
*/ */
$authHeader = (isset($_SERVER['HTTP_AUTHORIZATION']) ? $_SERVER['HTTP_AUTHORIZATION'] : $authHeader = $request->getHeader('Authorization');
(isset($_SERVER['REDIRECT_HTTP_AUTHORIZATION']) ? $_SERVER['REDIRECT_HTTP_AUTHORIZATION'] : null));
$matches = array(); $matches = array();
if ($authHeader && preg_match('/Basic\s+(.*)$/i', $authHeader, $matches)) { if ($authHeader && preg_match('/Basic\s+(.*)$/i', $authHeader, $matches)) {
list($name, $password) = explode(':', base64_decode($matches[1])); list($name, $password) = explode(':', base64_decode($matches[1]));
$_SERVER['PHP_AUTH_USER'] = strip_tags($name); $request->addHeader('PHP_AUTH_USER', strip_tags($name));
$_SERVER['PHP_AUTH_PW'] = strip_tags($password); $request->addHeader('PHP_AUTH_PW', strip_tags($password));
} }
$member = null; $member = null;
if (isset($_SERVER['PHP_AUTH_USER']) && isset($_SERVER['PHP_AUTH_PW'])) {
$member = MemberAuthenticator::authenticate(array( if ($request->getHeader('PHP_AUTH_USER') && $request->getHeader('PHP_AUTH_PW')) {
'Email' => $_SERVER['PHP_AUTH_USER'], /** @var MemberAuthenticator $authenticator */
'Password' => $_SERVER['PHP_AUTH_PW'], $authenticators = Security::singleton()->getApplicableAuthenticators(Authenticator::LOGIN);
), null);
foreach ($authenticators as $name => $authenticator) {
$member = $authenticator->authenticate([
'Email' => $request->getHeader('PHP_AUTH_USER'),
'Password' => $request->getHeader('PHP_AUTH_PW'),
]);
if ($member instanceof Member) {
break;
}
}
}
if ($member instanceof Member) {
Security::setCurrentUser($member);
} }
if (!$member && $tryUsingSessionLogin) { if (!$member && $tryUsingSessionLogin) {
$member = Member::currentUser(); $member = Security::getCurrentUser();
} }
// If we've failed the authentication mechanism, then show the login form // If we've failed the authentication mechanism, then show the login form
@ -97,10 +116,20 @@ class BasicAuth
$response = new HTTPResponse(null, 401); $response = new HTTPResponse(null, 401);
$response->addHeader('WWW-Authenticate', "Basic realm=\"$realm\""); $response->addHeader('WWW-Authenticate', "Basic realm=\"$realm\"");
if (isset($_SERVER['PHP_AUTH_USER'])) { if ($request->getHeader('PHP_AUTH_USER')) {
$response->setBody(_t('SilverStripe\\Security\\BasicAuth.ERRORNOTREC', "That username / password isn't recognised")); $response->setBody(
_t(
'SilverStripe\\Security\\BasicAuth.ERRORNOTREC',
"That username / password isn't recognised"
)
);
} else { } else {
$response->setBody(_t('SilverStripe\\Security\\BasicAuth.ENTERINFO', "Please enter a username and password.")); $response->setBody(
_t(
'SilverStripe\\Security\\BasicAuth.ENTERINFO',
'Please enter a username and password.'
)
);
} }
// Exception is caught by RequestHandler->handleRequest() and will halt further execution // Exception is caught by RequestHandler->handleRequest() and will halt further execution
@ -113,8 +142,13 @@ class BasicAuth
$response = new HTTPResponse(null, 401); $response = new HTTPResponse(null, 401);
$response->addHeader('WWW-Authenticate', "Basic realm=\"$realm\""); $response->addHeader('WWW-Authenticate', "Basic realm=\"$realm\"");
if (isset($_SERVER['PHP_AUTH_USER'])) { if ($request->getHeader('PHP_AUTH_USER')) {
$response->setBody(_t('SilverStripe\\Security\\BasicAuth.ERRORNOTADMIN', "That user is not an administrator.")); $response->setBody(
_t(
'SilverStripe\\Security\\BasicAuth.ERRORNOTADMIN',
'That user is not an administrator.'
)
);
} }
// Exception is caught by RequestHandler->handleRequest() and will halt further execution // Exception is caught by RequestHandler->handleRequest() and will halt further execution
@ -146,9 +180,9 @@ class BasicAuth
*/ */
public static function protect_entire_site($protect = true, $code = 'ADMIN', $message = null) public static function protect_entire_site($protect = true, $code = 'ADMIN', $message = null)
{ {
Config::inst()->update('SilverStripe\\Security\\BasicAuth', 'entire_site_protected', $protect); static::config()->set('entire_site_protected', $protect);
Config::inst()->update('SilverStripe\\Security\\BasicAuth', 'entire_site_protected_code', $code); static::config()->set('entire_site_protected_code', $code);
Config::inst()->update('SilverStripe\\Security\\BasicAuth', 'entire_site_protected_message', $message); static::config()->set('entire_site_protected_message', $message);
} }
/** /**
@ -160,9 +194,16 @@ class BasicAuth
*/ */
public static function protect_site_if_necessary() public static function protect_site_if_necessary()
{ {
$config = Config::forClass('SilverStripe\\Security\\BasicAuth'); $config = static::config();
if ($config->entire_site_protected) { $request = Controller::curr()->getRequest();
self::requireLogin($config->entire_site_protected_message, $config->entire_site_protected_code, false); if ($config->get('entire_site_protected')) {
/** @noinspection ExceptionsAnnotatingAndHandlingInspection */
static::requireLogin(
$request,
$config->get('entire_site_protected_message'),
$config->get('entire_site_protected_code'),
false
);
} }
} }
} }

View File

@ -1,38 +1,30 @@
<?php <?php
namespace SilverStripe\Security; namespace SilverStripe\Security\MemberAuthenticator;
use SilverStripe\Control\Controller; use SilverStripe\Control\Controller;
use SilverStripe\Control\RequestHandler;
use SilverStripe\Forms\CheckboxField; use SilverStripe\Forms\CheckboxField;
use SilverStripe\Forms\FieldList; use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\FormAction; use SilverStripe\Forms\FormAction;
use SilverStripe\Forms\HiddenField; use SilverStripe\Forms\HiddenField;
use SilverStripe\Forms\LiteralField; use SilverStripe\Forms\LiteralField;
use SilverStripe\Forms\PasswordField; use SilverStripe\Forms\PasswordField;
use SilverStripe\Security\Security;
/** /**
* Provides the in-cms session re-authentication form for the "member" authenticator * Provides the in-cms session re-authentication form for the "member" authenticator
*/ */
class CMSMemberLoginForm extends LoginForm class CMSMemberLoginForm extends MemberLoginForm
{ {
/**
* Get link to use for external security actions
*
* @param string $action Action
* @return string
*/
public function getExternalLink($action = null)
{
return Security::singleton()->Link($action);
}
/** /**
* CMSMemberLoginForm constructor. * CMSMemberLoginForm constructor.
* @param Controller $controller * @param RequestHandler $controller
* @param string $authenticatorClass * @param string $authenticatorClass
* @param FieldList $name * @param FieldList $name
*/ */
public function __construct(Controller $controller, $authenticatorClass, $name) public function __construct(RequestHandler $controller, $authenticatorClass, $name)
{ {
$this->controller = $controller; $this->controller = $controller;
@ -42,7 +34,7 @@ class CMSMemberLoginForm extends LoginForm
$actions = $this->getFormActions(); $actions = $this->getFormActions();
parent::__construct($controller, $name, $fields, $actions); parent::__construct($controller, $authenticatorClass, $name, $fields, $actions);
} }
/** /**
@ -51,7 +43,7 @@ class CMSMemberLoginForm extends LoginForm
public function getFormFields() public function getFormFields()
{ {
// Set default fields // Set default fields
$fields = new FieldList( $fields = FieldList::create([
HiddenField::create("AuthenticationMethod", null, $this->authenticator_class, $this), HiddenField::create("AuthenticationMethod", null, $this->authenticator_class, $this),
HiddenField::create('tempid', null, $this->controller->getRequest()->requestVar('tempid')), HiddenField::create('tempid', null, $this->controller->getRequest()->requestVar('tempid')),
PasswordField::create("Password", _t('SilverStripe\\Security\\Member.PASSWORD', 'Password')), PasswordField::create("Password", _t('SilverStripe\\Security\\Member.PASSWORD', 'Password')),
@ -63,9 +55,9 @@ class CMSMemberLoginForm extends LoginForm
_t('SilverStripe\\Security\\CMSMemberLoginForm.BUTTONFORGOTPASSWORD', "Forgot password?") _t('SilverStripe\\Security\\CMSMemberLoginForm.BUTTONFORGOTPASSWORD', "Forgot password?")
) )
) )
); ]);
if (Security::config()->autologin_enabled) { if (Security::config()->get('autologin_enabled')) {
$fields->push(CheckboxField::create( $fields->push(CheckboxField::create(
"Remember", "Remember",
_t('SilverStripe\\Security\\Member.REMEMBERME', "Remember me next time?") _t('SilverStripe\\Security\\Member.REMEMBERME', "Remember me next time?")
@ -88,8 +80,8 @@ class CMSMemberLoginForm extends LoginForm
} }
// Make actions // Make actions
$actions = new FieldList( $actions = FieldList::create([
FormAction::create('dologin', _t('SilverStripe\\Security\\CMSMemberLoginForm.BUTTONLOGIN', "Log back in")), FormAction::create('doLogin', _t('SilverStripe\\Security\\CMSMemberLoginForm.BUTTONLOGIN', "Log back in")),
LiteralField::create( LiteralField::create(
'doLogout', 'doLogout',
sprintf( sprintf(
@ -98,14 +90,20 @@ class CMSMemberLoginForm extends LoginForm
_t('SilverStripe\\Security\\CMSMemberLoginForm.BUTTONLOGOUT', "Log out") _t('SilverStripe\\Security\\CMSMemberLoginForm.BUTTONLOGOUT', "Log out")
) )
) )
); ]);
return $actions; return $actions;
} }
protected function buildRequestHandler() /**
* Get link to use for external security actions
*
* @param string $action Action
* @return string
*/
public function getExternalLink($action = null)
{ {
return CMSMemberLoginHandler::create($this); return Security::singleton()->Link($action);
} }
/** /**

View File

@ -3,11 +3,11 @@
namespace SilverStripe\Security; namespace SilverStripe\Security;
use SilverStripe\Admin\AdminRootController; use SilverStripe\Admin\AdminRootController;
use SilverStripe\Control\HTTPResponse;
use SilverStripe\Core\Convert;
use SilverStripe\Control\Director;
use SilverStripe\Control\Controller; use SilverStripe\Control\Controller;
use SilverStripe\Control\Director;
use SilverStripe\Control\HTTPResponse;
use SilverStripe\Control\Session; use SilverStripe\Control\Session;
use SilverStripe\Core\Convert;
use SilverStripe\ORM\FieldType\DBField; use SilverStripe\ORM\FieldType\DBField;
use SilverStripe\View\Requirements; use SilverStripe\View\Requirements;
@ -22,6 +22,7 @@ class CMSSecurity extends Security
); );
private static $allowed_actions = array( private static $allowed_actions = array(
'login',
'LoginForm', 'LoginForm',
'success' 'success'
); );
@ -41,12 +42,27 @@ class CMSSecurity extends Security
Requirements::javascript(FRAMEWORK_ADMIN_DIR . '/client/dist/js/vendor.js'); Requirements::javascript(FRAMEWORK_ADMIN_DIR . '/client/dist/js/vendor.js');
} }
public function login($request = null, $service = Authenticator::CMS_LOGIN)
{
return parent::login($request, Authenticator::CMS_LOGIN);
}
public function Link($action = null) public function Link($action = null)
{ {
/** @skipUpgrade */ /** @skipUpgrade */
return Controller::join_links(Director::baseURL(), "CMSSecurity", $action); return Controller::join_links(Director::baseURL(), "CMSSecurity", $action);
} }
protected function getAuthenticator($name = 'cms')
{
return parent::getAuthenticator($name);
}
public function getApplicableAuthenticators($service = Authenticator::CMS_LOGIN)
{
return parent::getApplicableAuthenticators($service);
}
/** /**
* Get known logged out member * Get known logged out member
* *
@ -57,6 +73,7 @@ class CMSSecurity extends Security
if ($tempid = $this->getRequest()->requestVar('tempid')) { if ($tempid = $this->getRequest()->requestVar('tempid')) {
return Member::member_from_tempid($tempid); return Member::member_from_tempid($tempid);
} }
return null; return null;
} }
@ -78,7 +95,7 @@ class CMSSecurity extends Security
public function getTitle() public function getTitle()
{ {
// Check if logged in already // Check if logged in already
if (Member::currentUserID()) { if (Security::getCurrentUser()) {
return _t('SilverStripe\\Security\\CMSSecurity.SUCCESS', 'Success'); return _t('SilverStripe\\Security\\CMSSecurity.SUCCESS', 'Success');
} }
@ -129,6 +146,7 @@ setTimeout(function(){top.location.href = "$loginURLJS";}, 0);
PHP PHP
); );
$this->setResponse($response); $this->setResponse($response);
return $response; return $response;
} }
@ -142,19 +160,6 @@ PHP
return parent::preLogin(); return parent::preLogin();
} }
public function GetLoginForms()
{
$forms = array();
$authenticators = Authenticator::get_authenticators();
foreach ($authenticators as $authenticator) {
// Get only CMS-supporting authenticators
if ($authenticator::supports_cms()) {
$forms[] = $authenticator::get_cms_login_form($this);
}
}
return $forms;
}
/** /**
* Determine if CMSSecurity is enabled * Determine if CMSSecurity is enabled
* *
@ -163,28 +168,11 @@ PHP
public static function enabled() public static function enabled()
{ {
// Disable shortcut // Disable shortcut
if (!static::config()->reauth_enabled) { if (!static::config()->get('reauth_enabled')) {
return false; return false;
} }
// Count all cms-supported methods return count(Security::singleton()->getApplicableAuthenticators(Authenticator::CMS_LOGIN)) > 0;
$authenticators = Authenticator::get_authenticators();
foreach ($authenticators as $authenticator) {
// Supported if at least one authenticator is supported
if ($authenticator::supports_cms()) {
return true;
}
}
return false;
}
public function LoginForm()
{
$authenticator = $this->getAuthenticator();
if ($authenticator && $authenticator::supports_cms()) {
return $authenticator::get_cms_login_form($this);
}
user_error('Passed invalid authentication method', E_USER_ERROR);
} }
/** /**
@ -195,7 +183,7 @@ PHP
public function success() public function success()
{ {
// Ensure member is properly logged in // Ensure member is properly logged in
if (!Member::currentUserID() || !class_exists(AdminRootController::class)) { if (!Security::getCurrentUser() || !class_exists(AdminRootController::class)) {
return $this->redirectToExternalLogin(); return $this->redirectToExternalLogin();
} }
@ -204,7 +192,7 @@ PHP
$backURLs = array( $backURLs = array(
$this->getRequest()->requestVar('BackURL'), $this->getRequest()->requestVar('BackURL'),
Session::get('BackURL'), Session::get('BackURL'),
Director::absoluteURL(AdminRootController::config()->url_base, true), Director::absoluteURL(AdminRootController::config()->get('url_base'), true),
); );
$backURL = null; $backURL = null;
foreach ($backURLs as $backURL) { foreach ($backURLs as $backURL) {
@ -217,7 +205,7 @@ PHP
$controller = $controller->customise(array( $controller = $controller->customise(array(
'Content' => _t( 'Content' => _t(
'SilverStripe\\Security\\CMSSecurity.SUCCESSCONTENT', 'SilverStripe\\Security\\CMSSecurity.SUCCESSCONTENT',
'<p>Login success. If you are not automatically redirected '. '<p>Login success. If you are not automatically redirected ' .
'<a target="_top" href="{link}">click here</a></p>', '<a target="_top" href="{link}">click here</a></p>',
'Login message displayed in the cms popup once a user has re-authenticated themselves', 'Login message displayed in the cms popup once a user has re-authenticated themselves',
array('link' => Convert::raw2att($backURL)) array('link' => Convert::raw2att($backURL))

View File

@ -1,65 +0,0 @@
<?php
namespace SilverStripe\Security;
use SilverStripe\Control\Session;
use SilverStripe\Control\RequestHandler;
use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\FormField;
use SilverStripe\Forms\PasswordField;
use SilverStripe\Forms\FormAction;
use SilverStripe\Forms\HiddenField;
use SilverStripe\Forms\Form;
/**
* Standard Change Password Form
*/
class ChangePasswordForm extends Form
{
/**
* Constructor
*
* @param RequestHandler $controller The parent controller, necessary to create the appropriate form action tag.
* @param string $name The method on the controller that will return this form object.
* @param FieldList|FormField $fields All of the fields in the form - a {@link FieldList} of
* {@link FormField} objects.
* @param FieldList|FormAction $actions All of the action buttons in the form - a {@link FieldList} of
*/
public function __construct($controller, $name, $fields = null, $actions = null)
{
$backURL = $controller->getBackURL() ?: Session::get('BackURL');
if (!$fields) {
$fields = new FieldList();
// Security/changepassword?h=XXX redirects to Security/changepassword
// without GET parameter to avoid potential HTTP referer leakage.
// In this case, a user is not logged in, and no 'old password' should be necessary.
if (Member::currentUser()) {
$fields->push(new PasswordField("OldPassword", _t('SilverStripe\\Security\\Member.YOUROLDPASSWORD', "Your old password")));
}
$fields->push(new PasswordField("NewPassword1", _t('SilverStripe\\Security\\Member.NEWPASSWORD', "New Password")));
$fields->push(new PasswordField("NewPassword2", _t('SilverStripe\\Security\\Member.CONFIRMNEWPASSWORD', "Confirm New Password")));
}
if (!$actions) {
$actions = new FieldList(
new FormAction("doChangePassword", _t('SilverStripe\\Security\\Member.BUTTONCHANGEPASSWORD', "Change Password"))
);
}
if ($backURL) {
$fields->push(new HiddenField('BackURL', false, $backURL));
}
parent::__construct($controller, $name, $fields, $actions);
}
/**
* @return ChangePasswordHandler
*/
protected function buildRequestHandler()
{
return ChangePasswordHandler::create($this);
}
}

View File

@ -1,103 +0,0 @@
<?php
namespace SilverStripe\Security;
use SilverStripe\Control\HTTPResponse;
use SilverStripe\Control\Session;
use SilverStripe\Forms\FormRequestHandler;
class ChangePasswordHandler extends FormRequestHandler
{
/**
* Change the password
*
* @param array $data The user submitted data
* @return HTTPResponse
*/
public function doChangePassword(array $data)
{
$member = Member::currentUser();
// The user was logged in, check the current password
if ($member && (
empty($data['OldPassword']) ||
!$member->checkPassword($data['OldPassword'])->isValid()
)) {
$this->form->sessionMessage(
_t('SilverStripe\\Security\\Member.ERRORPASSWORDNOTMATCH', "Your current password does not match, please try again"),
"bad"
);
// redirect back to the form, instead of using redirectBack() which could send the user elsewhere.
return $this->redirectBackToForm();
}
if (!$member) {
if (Session::get('AutoLoginHash')) {
$member = Member::member_from_autologinhash(Session::get('AutoLoginHash'));
}
// The user is not logged in and no valid auto login hash is available
if (!$member) {
Session::clear('AutoLoginHash');
return $this->redirect($this->addBackURLParam(Security::singleton()->Link('login')));
}
}
// Check the new password
if (empty($data['NewPassword1'])) {
$this->form->sessionMessage(
_t('SilverStripe\\Security\\Member.EMPTYNEWPASSWORD', "The new password can't be empty, please try again"),
"bad"
);
// redirect back to the form, instead of using redirectBack() which could send the user elsewhere.
return $this->redirectBackToForm();
}
// Fail if passwords do not match
if ($data['NewPassword1'] !== $data['NewPassword2']) {
$this->form->sessionMessage(
_t('SilverStripe\\Security\\Member.ERRORNEWPASSWORD', "You have entered your new password differently, try again"),
"bad"
);
// redirect back to the form, instead of using redirectBack() which could send the user elsewhere.
return $this->redirectBackToForm();
}
// Check if the new password is accepted
$validationResult = $member->changePassword($data['NewPassword1']);
if (!$validationResult->isValid()) {
$this->form->setSessionValidationResult($validationResult);
return $this->redirectBackToForm();
}
// Clear locked out status
$member->LockedOutUntil = null;
$member->FailedLoginCount = null;
$member->write();
if ($member->canLogIn()->isValid()) {
$member->logIn();
}
// TODO Add confirmation message to login redirect
Session::clear('AutoLoginHash');
// Redirect to backurl
$backURL = $this->getBackURL();
if ($backURL) {
return $this->redirect($backURL);
}
// Redirect to default location - the login form saying "You are logged in as..."
$url = Security::singleton()->Link('login');
return $this->redirect($url);
}
public function redirectBackToForm()
{
// Redirect back to form
$url = $this->addBackURLParam(CMSSecurity::singleton()->Link('changepassword'));
return $this->redirect($url);
}
}

View File

@ -4,24 +4,24 @@ namespace SilverStripe\Security;
use SilverStripe\Admin\SecurityAdmin; use SilverStripe\Admin\SecurityAdmin;
use SilverStripe\Core\Convert; use SilverStripe\Core\Convert;
use SilverStripe\Forms\Form;
use SilverStripe\Forms\GridField\GridFieldAddExistingAutocompleter;
use SilverStripe\Forms\GridField\GridFieldDetailForm;
use SilverStripe\Forms\TextField;
use SilverStripe\Forms\DropdownField; use SilverStripe\Forms\DropdownField;
use SilverStripe\Forms\TextareaField;
use SilverStripe\Forms\Tab;
use SilverStripe\Forms\TabSet;
use SilverStripe\Forms\FieldList; use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\LiteralField; use SilverStripe\Forms\Form;
use SilverStripe\Forms\ListboxField; use SilverStripe\Forms\GridField\GridField;
use SilverStripe\Forms\HiddenField; use SilverStripe\Forms\GridField\GridFieldAddExistingAutocompleter;
use SilverStripe\Forms\HTMLEditor\HTMLEditorConfig;
use SilverStripe\Forms\GridField\GridFieldConfig_RelationEditor;
use SilverStripe\Forms\GridField\GridFieldButtonRow; use SilverStripe\Forms\GridField\GridFieldButtonRow;
use SilverStripe\Forms\GridField\GridFieldConfig_RelationEditor;
use SilverStripe\Forms\GridField\GridFieldDetailForm;
use SilverStripe\Forms\GridField\GridFieldExportButton; use SilverStripe\Forms\GridField\GridFieldExportButton;
use SilverStripe\Forms\GridField\GridFieldPrintButton; use SilverStripe\Forms\GridField\GridFieldPrintButton;
use SilverStripe\Forms\GridField\GridField; use SilverStripe\Forms\HiddenField;
use SilverStripe\Forms\HTMLEditor\HTMLEditorConfig;
use SilverStripe\Forms\ListboxField;
use SilverStripe\Forms\LiteralField;
use SilverStripe\Forms\Tab;
use SilverStripe\Forms\TabSet;
use SilverStripe\Forms\TextareaField;
use SilverStripe\Forms\TextField;
use SilverStripe\ORM\ArrayList; use SilverStripe\ORM\ArrayList;
use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\DataQuery; use SilverStripe\ORM\DataQuery;
@ -29,7 +29,6 @@ use SilverStripe\ORM\HasManyList;
use SilverStripe\ORM\Hierarchy\Hierarchy; use SilverStripe\ORM\Hierarchy\Hierarchy;
use SilverStripe\ORM\ManyManyList; use SilverStripe\ORM\ManyManyList;
use SilverStripe\ORM\UnsavedRelationList; use SilverStripe\ORM\UnsavedRelationList;
use SilverStripe\View\Requirements;
/** /**
* A security group. * A security group.
@ -95,6 +94,7 @@ class Group extends DataObject
$doSet = new ArrayList(); $doSet = new ArrayList();
$children = Group::get()->filter("ParentID", $this->ID); $children = Group::get()->filter("ParentID", $this->ID);
/** @var Group $child */
foreach ($children as $child) { foreach ($children as $child) {
$doSet->push($child); $doSet->push($child);
$doSet->merge($child->getAllChildren()); $doSet->merge($child->getAllChildren());
@ -159,7 +159,7 @@ class Group extends DataObject
$detailForm = $config->getComponentByType(GridFieldDetailForm::class); $detailForm = $config->getComponentByType(GridFieldDetailForm::class);
$detailForm $detailForm
->setValidator(Member_Validator::create()) ->setValidator(Member_Validator::create())
->setItemEditFormCallback(function ($form, $component) use ($group) { ->setItemEditFormCallback(function ($form) use ($group) {
/** @var Form $form */ /** @var Form $form */
$record = $form->getRecord(); $record = $form->getRecord();
$groupsField = $form->Fields()->dataFieldByName('DirectGroups'); $groupsField = $form->Fields()->dataFieldByName('DirectGroups');
@ -369,9 +369,9 @@ class Group extends DataObject
{ {
$parent = $this; $parent = $this;
$items = []; $items = [];
while (isset($parent) && $parent instanceof Group) { while ($parent instanceof Group) {
$items[] = $parent->ID; $items[] = $parent->ID;
$parent = $parent->Parent; $parent = $parent->getParent();
} }
return $items; return $items;
} }
@ -395,12 +395,14 @@ class Group extends DataObject
->sort('"Sort"'); ->sort('"Sort"');
} }
/**
* @return string
*/
public function getTreeTitle() public function getTreeTitle()
{ {
if ($this->hasMethod('alternateTreeTitle')) { $title = htmlspecialchars($this->Title, ENT_QUOTES);
return $this->alternateTreeTitle(); $this->extend('updateTreeTitle', $title);
} return $title;
return htmlspecialchars($this->Title, ENT_QUOTES);
} }
/** /**
@ -476,7 +478,7 @@ class Group extends DataObject
public function canEdit($member = null) public function canEdit($member = null)
{ {
if (!$member) { if (!$member) {
$member = Member::currentUser(); $member = Security::getCurrentUser();
} }
// extended access checks // extended access checks
@ -512,7 +514,7 @@ class Group extends DataObject
public function canView($member = null) public function canView($member = null)
{ {
if (!$member) { if (!$member) {
$member = Member::currentUser(); $member = Security::getCurrentUser();
} }
// extended access checks // extended access checks
@ -534,7 +536,7 @@ class Group extends DataObject
public function canDelete($member = null) public function canDelete($member = null)
{ {
if (!$member) { if (!$member) {
$member = Member::currentUser(); $member = Security::getCurrentUser();
} }
// extended access checks // extended access checks

View File

@ -2,8 +2,8 @@
namespace SilverStripe\Security; namespace SilverStripe\Security;
use SilverStripe\ORM\DataObject;
use SilverStripe\Dev\CsvBulkLoader; use SilverStripe\Dev\CsvBulkLoader;
use SilverStripe\ORM\DataObject;
/** /**
* @todo Migrate Permission->Arg and Permission->Type values * @todo Migrate Permission->Arg and Permission->Type values
@ -15,12 +15,8 @@ class GroupCsvBulkLoader extends CsvBulkLoader
'Code' => 'Code', 'Code' => 'Code',
); );
public function __construct($objectClass = null) public function __construct($objectClass = Group::class)
{ {
if (!$objectClass) {
$objectClass = 'SilverStripe\\Security\\Group';
}
parent::__construct($objectClass); parent::__construct($objectClass);
} }

View File

@ -0,0 +1,30 @@
<?php
namespace SilverStripe\Security;
use SilverStripe\Control\HTTPRequest;
use SilverStripe\Control\HTTPResponse;
/**
* Represents an authentication handler that can have identities logged into & out of it.
* For example, SessionAuthenticationHandler is an IdentityStore (as we can write a new member to it)
* but BasicAuthAuthenticationHandler is not (as it's up to the browser to handle log-in / log-out)
*/
interface IdentityStore
{
/**
* Log the given member into this identity store.
*
* @param Member $member The member to log in.
* @param Boolean $persistent boolean If set to true, the login may persist beyond the current session.
* @param HTTPRequest $request The request of the visitor that is logging in, to get, for example, cookies.
*/
public function logIn(Member $member, $persistent = false, HTTPRequest $request = null);
/**
* Log any logged-in member out of this identity store.
*
* @param HTTPRequest $request The request of the visitor that is logging out, to get, for example, cookies.
*/
public function logOut(HTTPRequest $request = null);
}

View File

@ -158,13 +158,13 @@ class InheritedPermissions implements PermissionChecker
{ {
switch ($permission) { switch ($permission) {
case self::EDIT: case self::EDIT:
$this->canEditMultiple($ids, Member::currentUser(), false); $this->canEditMultiple($ids, Security::getCurrentUser(), false);
break; break;
case self::VIEW: case self::VIEW:
$this->canViewMultiple($ids, Member::currentUser(), false); $this->canViewMultiple($ids, Security::getCurrentUser(), false);
break; break;
case self::DELETE: case self::DELETE:
$this->canDeleteMultiple($ids, Member::currentUser(), false); $this->canDeleteMultiple($ids, Security::getCurrentUser(), false);
break; break;
default: default:
throw new InvalidArgumentException("Invalid permission type $permission"); throw new InvalidArgumentException("Invalid permission type $permission");

View File

@ -2,7 +2,6 @@
namespace SilverStripe\Security; namespace SilverStripe\Security;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\Forms\FieldList; use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\Form; use SilverStripe\Forms\Form;

View File

@ -3,16 +3,16 @@
namespace SilverStripe\Security; namespace SilverStripe\Security;
use IntlDateFormatter; use IntlDateFormatter;
use InvalidArgumentException;
use SilverStripe\Admin\LeftAndMain; use SilverStripe\Admin\LeftAndMain;
use SilverStripe\CMS\Controllers\CMSMain; use SilverStripe\CMS\Controllers\CMSMain;
use SilverStripe\Control\Cookie; use SilverStripe\Control\Controller;
use SilverStripe\Control\Director; use SilverStripe\Control\Director;
use SilverStripe\Control\Email\Email; use SilverStripe\Control\Email\Email;
use SilverStripe\Control\Email\Mailer; use SilverStripe\Control\Email\Mailer;
use SilverStripe\Control\Session;
use SilverStripe\Core\Convert; use SilverStripe\Core\Convert;
use SilverStripe\Core\Injector\Injector; use SilverStripe\Core\Injector\Injector;
use SilverStripe\Dev\SapphireTest; use SilverStripe\Dev\Deprecation;
use SilverStripe\Dev\TestMailer; use SilverStripe\Dev\TestMailer;
use SilverStripe\Forms\ConfirmedPasswordField; use SilverStripe\Forms\ConfirmedPasswordField;
use SilverStripe\Forms\DropdownField; use SilverStripe\Forms\DropdownField;
@ -20,20 +20,17 @@ use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\HTMLEditor\HTMLEditorConfig; use SilverStripe\Forms\HTMLEditor\HTMLEditorConfig;
use SilverStripe\Forms\ListboxField; use SilverStripe\Forms\ListboxField;
use SilverStripe\i18n\i18n; use SilverStripe\i18n\i18n;
use SilverStripe\MSSQL\MSSQLDatabase;
use SilverStripe\ORM\ArrayList; use SilverStripe\ORM\ArrayList;
use SilverStripe\ORM\DataList;
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\ORM\HasManyList; use SilverStripe\ORM\HasManyList;
use SilverStripe\ORM\ManyManyList; use SilverStripe\ORM\ManyManyList;
use SilverStripe\ORM\SS_List;
use SilverStripe\ORM\Map; use SilverStripe\ORM\Map;
use SilverStripe\ORM\SS_List;
use SilverStripe\ORM\ValidationException; use SilverStripe\ORM\ValidationException;
use SilverStripe\ORM\ValidationResult; use SilverStripe\ORM\ValidationResult;
use SilverStripe\View\SSViewer;
use SilverStripe\View\TemplateGlobalProvider;
use DateTime;
/** /**
* The member class which represents the users of the system * The member class which represents the users of the system
@ -56,30 +53,31 @@ use DateTime;
* @property int $FailedLoginCount * @property int $FailedLoginCount
* @property string $DateFormat * @property string $DateFormat
* @property string $TimeFormat * @property string $TimeFormat
* @property string $SetPassword Pseudo-DB field for temp storage. Not emitted to DB
*/ */
class Member extends DataObject implements TemplateGlobalProvider class Member extends DataObject
{ {
private static $db = array( private static $db = array(
'FirstName' => 'Varchar', 'FirstName' => 'Varchar',
'Surname' => 'Varchar', 'Surname' => 'Varchar',
'Email' => 'Varchar(254)', // See RFC 5321, Section 4.5.3.1.3. (256 minus the < and > character) 'Email' => 'Varchar(254)', // See RFC 5321, Section 4.5.3.1.3. (256 minus the < and > character)
'TempIDHash' => 'Varchar(160)', // Temporary id used for cms re-authentication 'TempIDHash' => 'Varchar(160)', // Temporary id used for cms re-authentication
'TempIDExpired' => 'Datetime', // Expiry of temp login 'TempIDExpired' => 'Datetime', // Expiry of temp login
'Password' => 'Varchar(160)', 'Password' => 'Varchar(160)',
'AutoLoginHash' => 'Varchar(160)', // Used to auto-login the user on password reset 'AutoLoginHash' => 'Varchar(160)', // Used to auto-login the user on password reset
'AutoLoginExpired' => 'Datetime', 'AutoLoginExpired' => 'Datetime',
// This is an arbitrary code pointing to a PasswordEncryptor instance, // This is an arbitrary code pointing to a PasswordEncryptor instance,
// not an actual encryption algorithm. // not an actual encryption algorithm.
// Warning: Never change this field after its the first password hashing without // Warning: Never change this field after its the first password hashing without
// providing a new cleartext password as well. // providing a new cleartext password as well.
'PasswordEncryption' => "Varchar(50)", 'PasswordEncryption' => "Varchar(50)",
'Salt' => 'Varchar(50)', 'Salt' => 'Varchar(50)',
'PasswordExpiry' => 'Date', 'PasswordExpiry' => 'Date',
'LockedOutUntil' => 'Datetime', 'LockedOutUntil' => 'Datetime',
'Locale' => 'Varchar(6)', 'Locale' => 'Varchar(6)',
// handled in registerFailedLogin(), only used if $lock_out_after_incorrect_logins is set // handled in registerFailedLogin(), only used if $lock_out_after_incorrect_logins is set
'FailedLoginCount' => 'Int', 'FailedLoginCount' => 'Int',
); );
private static $belongs_many_many = array( private static $belongs_many_many = array(
@ -87,7 +85,7 @@ class Member extends DataObject implements TemplateGlobalProvider
); );
private static $has_many = array( private static $has_many = array(
'LoggedPasswords' => MemberPassword::class, 'LoggedPasswords' => MemberPassword::class,
'RememberLoginHashes' => RememberLoginHash::class, 'RememberLoginHashes' => RememberLoginHash::class,
); );
@ -191,6 +189,12 @@ class Member extends DataObject implements TemplateGlobalProvider
*/ */
private static $password_expiry_days = null; private static $password_expiry_days = null;
/**
* @config
* @var bool enable or disable logging of previously used passwords. See {@link onAfterWrite}
*/
private static $password_logging_enabled = true;
/** /**
* @config * @config
* @var Int Number of incorrect logins after which * @var Int Number of incorrect logins after which
@ -276,7 +280,7 @@ class Member extends DataObject implements TemplateGlobalProvider
// Find member // Find member
/** @skipUpgrade */ /** @skipUpgrade */
$admin = Member::get() $admin = static::get()
->filter('Email', Security::default_admin_username()) ->filter('Email', Security::default_admin_username())
->first(); ->first();
if (!$admin) { if (!$admin) {
@ -284,7 +288,7 @@ class Member extends DataObject implements TemplateGlobalProvider
// persistent logins in the database. See Security::setDefaultAdmin(). // persistent logins in the database. See Security::setDefaultAdmin().
// Set 'Email' to identify this as the default admin // Set 'Email' to identify this as the default admin
$admin = Member::create(); $admin = Member::create();
$admin->FirstName = _t(__CLASS__.'.DefaultAdminFirstname', 'Default Admin'); $admin->FirstName = _t(__CLASS__ . '.DefaultAdminFirstname', 'Default Admin');
$admin->Email = Security::default_admin_username(); $admin->Email = Security::default_admin_username();
$admin->write(); $admin->write();
} }
@ -323,14 +327,15 @@ class Member extends DataObject implements TemplateGlobalProvider
// Check a password is set on this member // Check a password is set on this member
if (empty($this->Password) && $this->exists()) { if (empty($this->Password) && $this->exists()) {
$result->addError(_t(__CLASS__.'.NoPassword', 'There is no password on this member.')); $result->addError(_t(__CLASS__ . '.NoPassword', 'There is no password on this member.'));
return $result; return $result;
} }
$e = PasswordEncryptor::create_for_algorithm($this->PasswordEncryption); $e = PasswordEncryptor::create_for_algorithm($this->PasswordEncryption);
if (!$e->check($this->Password, $password, $this->Salt, $this)) { if (!$e->check($this->Password, $password, $this->Salt, $this)) {
$result->addError(_t( $result->addError(_t(
__CLASS__.'.ERRORWRONGCRED', __CLASS__ . '.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.'
)); ));
} }
@ -364,16 +369,17 @@ class Member extends DataObject implements TemplateGlobalProvider
if ($this->isLockedOut()) { if ($this->isLockedOut()) {
$result->addError( $result->addError(
_t( _t(
__CLASS__.'.ERRORLOCKEDOUT2', __CLASS__ . '.ERRORLOCKEDOUT2',
'Your account has been temporarily disabled because of too many failed attempts at ' . 'Your account has been temporarily disabled because of too many failed attempts at ' .
'logging in. Please try again in {count} minutes.', 'logging in. Please try again in {count} minutes.',
null, null,
array('count' => $this->config()->lock_out_delay_mins) array('count' => static::config()->get('lock_out_delay_mins'))
) )
); );
} }
$this->extend('canLogIn', $result); $this->extend('canLogIn', $result);
return $result; return $result;
} }
@ -387,36 +393,10 @@ class Member extends DataObject implements TemplateGlobalProvider
if (!$this->LockedOutUntil) { if (!$this->LockedOutUntil) {
return false; return false;
} }
return DBDatetime::now()->getTimestamp() < $this->dbObject('LockedOutUntil')->getTimestamp(); return DBDatetime::now()->getTimestamp() < $this->dbObject('LockedOutUntil')->getTimestamp();
} }
/**
* Regenerate the session_id.
* This wrapper is here to make it easier to disable calls to session_regenerate_id(), should you need to.
* They have caused problems in certain
* quirky problems (such as using the Windmill 0.3.6 proxy).
*/
public static function session_regenerate_id()
{
if (!self::config()->session_regenerate_id) {
return;
}
// This can be called via CLI during testing.
if (Director::is_cli()) {
return;
}
$file = '';
$line = '';
// @ is to supress win32 warnings/notices when session wasn't cleaned up properly
// There's nothing we can do about this, because it's an operating system function!
if (!headers_sent($file, $line)) {
@session_regenerate_id(true);
}
}
/** /**
* Set a {@link PasswordValidator} object to use to validate member's passwords. * Set a {@link PasswordValidator} object to use to validate member's passwords.
* *
@ -443,63 +423,48 @@ class Member extends DataObject implements TemplateGlobalProvider
if (!$this->PasswordExpiry) { if (!$this->PasswordExpiry) {
return false; return false;
} }
return strtotime(date('Y-m-d')) >= strtotime($this->PasswordExpiry); return strtotime(date('Y-m-d')) >= strtotime($this->PasswordExpiry);
} }
/** /**
* Logs this member in * @deprecated 5.0.0 Use Security::setCurrentUser() or IdentityStore::logIn()
* *
* @param bool $remember If set to TRUE, the member will be logged in automatically the next time.
*/ */
public function logIn($remember = false) public function logIn()
{ {
Deprecation::notice(
'5.0.0',
'This method is deprecated and only logs in for the current request. Please use Security::setCurrentUser($user) or an IdentityStore'
);
Security::setCurrentUser($this);
}
/**
* Called before a member is logged in via session/cookie/etc
*/
public function beforeMemberLoggedIn()
{
// @todo Move to middleware on the AuthenticationRequestFilter IdentityStore
$this->extend('beforeMemberLoggedIn'); $this->extend('beforeMemberLoggedIn');
}
self::session_regenerate_id(); /**
* Called after a member is logged in via session/cookie/etc
Session::set("loggedInAs", $this->ID); */
// This lets apache rules detect whether the user has logged in public function afterMemberLoggedIn()
if (Member::config()->login_marker_cookie) { {
Cookie::set(Member::config()->login_marker_cookie, 1, 0);
}
if (Security::config()->autologin_enabled) {
// Cleans up any potential previous hash for this member on this device
if ($alcDevice = Cookie::get('alc_device')) {
RememberLoginHash::get()->filter('DeviceID', $alcDevice)->removeAll();
}
if ($remember) {
$rememberLoginHash = RememberLoginHash::generate($this);
$tokenExpiryDays = RememberLoginHash::config()->uninherited('token_expiry_days');
$deviceExpiryDays = RememberLoginHash::config()->uninherited('device_expiry_days');
Cookie::set(
'alc_enc',
$this->ID . ':' . $rememberLoginHash->getToken(),
$tokenExpiryDays,
null,
null,
null,
true
);
Cookie::set('alc_device', $rememberLoginHash->DeviceID, $deviceExpiryDays, null, null, null, true);
} else {
Cookie::set('alc_enc', null);
Cookie::set('alc_device', null);
Cookie::force_expiry('alc_enc');
Cookie::force_expiry('alc_device');
}
}
// Clear the incorrect log-in count // Clear the incorrect log-in count
$this->registerSuccessfulLogin(); $this->registerSuccessfulLogin();
$this->LockedOutUntil = null; $this->LockedOutUntil = null;
$this->regenerateTempID(); $this->regenerateTempID();
$this->write(); $this->write();
// Audit logging hook // Audit logging hook
$this->extend('memberLoggedIn'); $this->extend('afterMemberLoggedIn');
} }
/** /**
@ -511,9 +476,10 @@ class Member extends DataObject implements TemplateGlobalProvider
public function regenerateTempID() public function regenerateTempID()
{ {
$generator = new RandomGenerator(); $generator = new RandomGenerator();
$lifetime = self::config()->get('temp_id_lifetime');
$this->TempIDHash = $generator->randomToken('sha1'); $this->TempIDHash = $generator->randomToken('sha1');
$this->TempIDExpired = self::config()->temp_id_lifetime $this->TempIDExpired = $lifetime
? date('Y-m-d H:i:s', strtotime(DBDatetime::now()->getValue()) + self::config()->temp_id_lifetime) ? date('Y-m-d H:i:s', strtotime(DBDatetime::now()->getValue()) + $lifetime)
: null; : null;
$this->write(); $this->write();
} }
@ -523,15 +489,20 @@ class Member extends DataObject implements TemplateGlobalProvider
* has a database record of the same ID. If there is * has a database record of the same ID. If there is
* no logged in user, FALSE is returned anyway. * no logged in user, FALSE is returned anyway.
* *
* @deprecated Not needed anymore, as it returns Security::getCurrentUser();
*
* @return boolean TRUE record found FALSE no record found * @return boolean TRUE record found FALSE no record found
*/ */
public static function logged_in_session_exists() public static function logged_in_session_exists()
{ {
if ($id = Member::currentUserID()) { Deprecation::notice(
if ($member = DataObject::get_by_id(Member::class, $id)) { '5.0.0',
if ($member->exists()) { 'This method is deprecated and now does not add value. Please use Security::getCurrentUser()'
return true; );
}
if ($member = Security::getCurrentUser()) {
if ($member && $member->exists()) {
return true;
} }
} }
@ -539,125 +510,21 @@ class Member extends DataObject implements TemplateGlobalProvider
} }
/** /**
* Log the user in if the "remember login" cookie is set * @deprecated Use Security::setCurrentUser(null) or an IdentityStore
*
* The <i>remember login token</i> will be changed on every successful
* auto-login.
*/
public static function autoLogin()
{
// Don't bother trying this multiple times
if (!class_exists(SapphireTest::class, false) || !SapphireTest::is_running_test()) {
self::$_already_tried_to_auto_log_in = true;
}
if (!Security::config()->autologin_enabled
|| strpos(Cookie::get('alc_enc'), ':') === false
|| Session::get("loggedInAs")
|| !Security::database_is_ready()
) {
return;
}
if (strpos(Cookie::get('alc_enc'), ':') && Cookie::get('alc_device') && !Session::get("loggedInAs")) {
list($uid, $token) = explode(':', Cookie::get('alc_enc'), 2);
if (!$uid || !$token) {
return;
}
$deviceID = Cookie::get('alc_device');
/** @var Member $member */
$member = Member::get()->byID($uid);
/** @var RememberLoginHash $rememberLoginHash */
$rememberLoginHash = null;
// check if autologin token matches
if ($member) {
$hash = $member->encryptWithUserSettings($token);
$rememberLoginHash = RememberLoginHash::get()
->filter(array(
'MemberID' => $member->ID,
'DeviceID' => $deviceID,
'Hash' => $hash
))->first();
if (!$rememberLoginHash) {
$member = null;
} else {
// Check for expired token
$expiryDate = new DateTime($rememberLoginHash->ExpiryDate);
$now = DBDatetime::now();
$now = new DateTime($now->Rfc2822());
if ($now > $expiryDate) {
$member = null;
}
}
}
if ($member) {
self::session_regenerate_id();
Session::set("loggedInAs", $member->ID);
// This lets apache rules detect whether the user has logged in
if (Member::config()->login_marker_cookie) {
Cookie::set(Member::config()->login_marker_cookie, 1, 0, null, null, false, true);
}
if ($rememberLoginHash) {
$rememberLoginHash->renew();
$tokenExpiryDays = RememberLoginHash::config()->uninherited('token_expiry_days');
Cookie::set(
'alc_enc',
$member->ID . ':' . $rememberLoginHash->getToken(),
$tokenExpiryDays,
null,
null,
false,
true
);
}
$member->write();
// Audit logging hook
$member->extend('memberAutoLoggedIn');
}
}
}
/**
* Logs this member out. * Logs this member out.
*/ */
public function logOut() public function logOut()
{ {
Deprecation::notice(
'5.0.0',
'This method is deprecated and now does not persist. Please use Security::setCurrentUser(null) or an IdenityStore'
);
$this->extend('beforeMemberLoggedOut'); $this->extend('beforeMemberLoggedOut');
Session::clear("loggedInAs"); Injector::inst()->get(IdentityStore::class)->logOut(Controller::curr()->getRequest());
if (Member::config()->login_marker_cookie) {
Cookie::set(Member::config()->login_marker_cookie, null, 0);
}
Session::destroy();
$this->extend('memberLoggedOut');
// Clears any potential previous hashes for this member
RememberLoginHash::clear($this, Cookie::get('alc_device'));
Cookie::set('alc_enc', null); // // Clear the Remember Me cookie
Cookie::force_expiry('alc_enc');
Cookie::set('alc_device', null);
Cookie::force_expiry('alc_device');
// Switch back to live in order to avoid infinite loops when
// redirecting to the login screen (if this login screen is versioned)
Session::clear('readingMode');
$this->write();
// Audit logging hook // Audit logging hook
$this->extend('memberLoggedOut'); $this->extend('afterMemberLoggedOut');
} }
/** /**
@ -681,6 +548,7 @@ class Member extends DataObject implements TemplateGlobalProvider
// We assume we have PasswordEncryption and Salt available here. // We assume we have PasswordEncryption and Salt available here.
$e = PasswordEncryptor::create_for_algorithm($this->PasswordEncryption); $e = PasswordEncryptor::create_for_algorithm($this->PasswordEncryption);
return $e->encrypt($string, $this->Salt); return $e->encrypt($string, $this->Salt);
} }
@ -723,6 +591,7 @@ class Member extends DataObject implements TemplateGlobalProvider
{ {
$hash = $this->encryptWithUserSettings($autologinToken); $hash = $this->encryptWithUserSettings($autologinToken);
$member = self::member_from_autologinhash($hash, false); $member = self::member_from_autologinhash($hash, false);
return (bool)$member; return (bool)$member;
} }
@ -738,13 +607,13 @@ class Member extends DataObject implements TemplateGlobalProvider
public static function member_from_autologinhash($hash, $login = false) public static function member_from_autologinhash($hash, $login = false)
{ {
/** @var Member $member */ /** @var Member $member */
$member = Member::get()->filter([ $member = static::get()->filter([
'AutoLoginHash' => $hash, 'AutoLoginHash' => $hash,
'AutoLoginExpired:GreaterThan' => DBDatetime::now()->getValue(), 'AutoLoginExpired:GreaterThan' => DBDatetime::now()->getValue(),
])->first(); ])->first();
if ($login && $member) { if ($login && $member) {
$member->logIn(); Injector::inst()->get(IdentityStore::class)->logIn($member);
} }
return $member; return $member;
@ -758,11 +627,12 @@ class Member extends DataObject implements TemplateGlobalProvider
*/ */
public static function member_from_tempid($tempid) public static function member_from_tempid($tempid)
{ {
$members = Member::get() $members = static::get()
->filter('TempIDHash', $tempid); ->filter('TempIDHash', $tempid);
// Exclude expired // Exclude expired
if (static::config()->temp_id_lifetime) { if (static::config()->get('temp_id_lifetime')) {
/** @var DataList|Member[] $members */
$members = $members->filter('TempIDExpired:GreaterThan', DBDatetime::now()->getValue()); $members = $members->filter('TempIDExpired:GreaterThan', DBDatetime::now()->getValue());
} }
@ -773,6 +643,8 @@ class Member extends DataObject implements TemplateGlobalProvider
* Returns the fields for the member form - used in the registration/profile module. * Returns the fields for the member form - used in the registration/profile module.
* It should return fields that are editable by the admin and the logged-in user. * It should return fields that are editable by the admin and the logged-in user.
* *
* @todo possibly move this to an extension
*
* @return FieldList Returns a {@link FieldList} containing the fields for * @return FieldList Returns a {@link FieldList} containing the fields for
* the member form. * the member form.
*/ */
@ -788,11 +660,12 @@ class Member extends DataObject implements TemplateGlobalProvider
i18n::getSources()->getKnownLocales() i18n::getSources()->getKnownLocales()
)); ));
$fields->removeByName(static::config()->hidden_fields); $fields->removeByName(static::config()->get('hidden_fields'));
$fields->removeByName('FailedLoginCount'); $fields->removeByName('FailedLoginCount');
$this->extend('updateMemberFormFields', $fields); $this->extend('updateMemberFormFields', $fields);
return $fields; return $fields;
} }
@ -805,7 +678,7 @@ class Member extends DataObject implements TemplateGlobalProvider
{ {
$editingPassword = $this->isInDB(); $editingPassword = $this->isInDB();
$label = $editingPassword $label = $editingPassword
? _t(__CLASS__.'.EDIT_PASSWORD', 'New Password') ? _t(__CLASS__ . '.EDIT_PASSWORD', 'New Password')
: $this->fieldLabel('Password'); : $this->fieldLabel('Password');
/** @var ConfirmedPasswordField $password */ /** @var ConfirmedPasswordField $password */
$password = ConfirmedPasswordField::create( $password = ConfirmedPasswordField::create(
@ -817,12 +690,13 @@ class Member extends DataObject implements TemplateGlobalProvider
); );
// If editing own password, require confirmation of existing // If editing own password, require confirmation of existing
if ($editingPassword && $this->ID == Member::currentUserID()) { if ($editingPassword && $this->ID == Security::getCurrentUser()->ID) {
$password->setRequireExistingPassword(true); $password->setRequireExistingPassword(true);
} }
$password->setCanBeEmpty(true); $password->setCanBeEmpty(true);
$this->extend('updateMemberPasswordField', $password); $this->extend('updateMemberPasswordField', $password);
return $password; return $password;
} }
@ -850,24 +724,20 @@ class Member extends DataObject implements TemplateGlobalProvider
/** /**
* Returns the current logged in user * Returns the current logged in user
* *
* @deprecated 5.0.0 use Security::getCurrentUser()
*
* @return Member * @return Member
*/ */
public static function currentUser() public static function currentUser()
{ {
$id = Member::currentUserID(); Deprecation::notice(
'5.0.0',
'This method is deprecated. Please use Security::getCurrentUser() or an IdentityStore'
);
if ($id) { return Security::getCurrentUser();
return DataObject::get_by_id(Member::class, $id);
}
} }
/**
* Allow override of the current user ID
*
* @var int|null Set to null to fallback to session, or an explicit ID
*/
protected static $overrideID = null;
/** /**
* Temporarily act as the specified user, limited to a $callback, but * Temporarily act as the specified user, limited to a $callback, but
* without logging in as that user. * without logging in as that user.
@ -881,49 +751,52 @@ class Member extends DataObject implements TemplateGlobalProvider
* *
* @param Member|null|int $member Member or member ID to log in as. * @param Member|null|int $member Member or member ID to log in as.
* Set to null or 0 to act as a logged out user. * Set to null or 0 to act as a logged out user.
* @param $callback * @param callable $callback
*/ */
public static function actAs($member, $callback) public static function actAs($member, $callback)
{ {
$id = ($member instanceof Member ? $member->ID : $member) ?: 0; $previousUser = Security::getCurrentUser();
$previousID = static::$overrideID;
static::$overrideID = $id; // Transform ID to member
if (is_numeric($member)) {
$member = DataObject::get_by_id(Member::class, $member);
}
Security::setCurrentUser($member);
try { try {
return $callback(); return $callback();
} finally { } finally {
static::$overrideID = $previousID; Security::setCurrentUser($previousUser);
} }
} }
/** /**
* Get the ID of the current logged in user * Get the ID of the current logged in user
* *
* @deprecated 5.0.0 use Security::getCurrentUser()
*
* @return int Returns the ID of the current logged in user or 0. * @return int Returns the ID of the current logged in user or 0.
*/ */
public static function currentUserID() public static function currentUserID()
{ {
if (isset(static::$overrideID)) { Deprecation::notice(
return static::$overrideID; '5.0.0',
} 'This method is deprecated. Please use Security::getCurrentUser() or an IdentityStore'
);
$id = Session::get("loggedInAs"); if ($member = Security::getCurrentUser()) {
if (!$id && !self::$_already_tried_to_auto_log_in) { return $member->ID;
self::autoLogin(); } else {
$id = Session::get("loggedInAs"); return 0;
} }
return is_numeric($id) ? $id : 0;
} }
private static $_already_tried_to_auto_log_in = false; /**
* Generate a random password, with randomiser to kick in if there's no words file on the
* filesystem.
/* *
* Generate a random password, with randomiser to kick in if there's no words file on the * @return string Returns a random password.
* filesystem. */
*
* @return string Returns a random password.
*/
public static function create_new_password() public static function create_new_password()
{ {
$words = Security::config()->uninherited('word_list'); $words = Security::config()->uninherited('word_list');
@ -932,16 +805,17 @@ class Member extends DataObject implements TemplateGlobalProvider
$words = file($words); $words = file($words);
list($usec, $sec) = explode(' ', microtime()); list($usec, $sec) = explode(' ', microtime());
srand($sec + ((float) $usec * 100000)); mt_srand($sec + ((float)$usec * 100000));
$word = trim($words[rand(0, sizeof($words)-1)]); $word = trim($words[random_int(0, count($words) - 1)]);
$number = rand(10, 999); $number = random_int(10, 999);
return $word . $number; return $word . $number;
} else { } else {
$random = rand(); $random = mt_rand();
$string = md5($random); $string = md5($random);
$output = substr($string, 0, 8); $output = substr($string, 0, 8);
return $output; return $output;
} }
} }
@ -958,7 +832,7 @@ class Member extends DataObject implements TemplateGlobalProvider
// If a member with the same "unique identifier" already exists with a different ID, don't allow merging. // If a member with the same "unique identifier" already exists with a different ID, don't allow merging.
// Note: This does not a full replacement for safeguards in the controller layer (e.g. in a registration form), // Note: This does not a full replacement for safeguards in the controller layer (e.g. in a registration form),
// but rather a last line of defense against data inconsistencies. // but rather a last line of defense against data inconsistencies.
$identifierField = Member::config()->unique_identifier_field; $identifierField = Member::config()->get('unique_identifier_field');
if ($this->$identifierField) { if ($this->$identifierField) {
// Note: Same logic as Member_Validator class // Note: Same logic as Member_Validator class
$filter = [ $filter = [
@ -971,12 +845,12 @@ class Member extends DataObject implements TemplateGlobalProvider
if ($existingRecord) { if ($existingRecord) {
throw new ValidationException(_t( throw new ValidationException(_t(
__CLASS__.'.ValidationIdentifierFailed', __CLASS__ . '.ValidationIdentifierFailed',
'Can\'t overwrite existing member #{id} with identical identifier ({name} = {value}))', 'Can\'t overwrite existing member #{id} with identical identifier ({name} = {value}))',
'Values in brackets show "fieldname = value", usually denoting an existing email address', 'Values in brackets show "fieldname = value", usually denoting an existing email address',
array( array(
'id' => $existingRecord->ID, 'id' => $existingRecord->ID,
'name' => $identifierField, 'name' => $identifierField,
'value' => $this->$identifierField 'value' => $this->$identifierField
) )
)); ));
@ -985,16 +859,17 @@ class Member extends DataObject implements TemplateGlobalProvider
// We don't send emails out on dev/tests sites to prevent accidentally spamming users. // We don't send emails out on dev/tests sites to prevent accidentally spamming users.
// However, if TestMailer is in use this isn't a risk. // However, if TestMailer is in use this isn't a risk.
// @todo some developers use external tools, so emailing might be a good idea anyway
if ((Director::isLive() || Injector::inst()->get(Mailer::class) instanceof TestMailer) if ((Director::isLive() || Injector::inst()->get(Mailer::class) instanceof TestMailer)
&& $this->isChanged('Password') && $this->isChanged('Password')
&& $this->record['Password'] && $this->record['Password']
&& $this->config()->notify_password_change && static::config()->get('notify_password_change')
) { ) {
Email::create() Email::create()
->setHTMLTemplate('SilverStripe\\Control\\Email\\ChangePasswordEmail') ->setHTMLTemplate('SilverStripe\\Control\\Email\\ChangePasswordEmail')
->setData($this) ->setData($this)
->setTo($this->Email) ->setTo($this->Email)
->setSubject(_t(__CLASS__.'.SUBJECTPASSWORDCHANGED', "Your password has been changed", 'Email subject')) ->setSubject(_t(__CLASS__ . '.SUBJECTPASSWORDCHANGED', "Your password has been changed", 'Email subject'))
->send(); ->send();
} }
@ -1002,7 +877,7 @@ class Member extends DataObject implements TemplateGlobalProvider
// Note that this only works with cleartext passwords, as we can't rehash // Note that this only works with cleartext passwords, as we can't rehash
// existing passwords. // existing passwords.
if ((!$this->ID && $this->Password) || $this->isChanged('Password')) { if ((!$this->ID && $this->Password) || $this->isChanged('Password')) {
//reset salt so that it gets regenerated - this will invalidate any persistant login cookies //reset salt so that it gets regenerated - this will invalidate any persistent login cookies
// or other information encrypted with this Member's settings (see self::encryptWithUserSettings) // or other information encrypted with this Member's settings (see self::encryptWithUserSettings)
$this->Salt = ''; $this->Salt = '';
// Password was changed: encrypt the password according the settings // Password was changed: encrypt the password according the settings
@ -1010,7 +885,7 @@ class Member extends DataObject implements TemplateGlobalProvider
$this->Password, // this is assumed to be cleartext $this->Password, // this is assumed to be cleartext
$this->Salt, $this->Salt,
($this->PasswordEncryption) ? ($this->PasswordEncryption) ?
$this->PasswordEncryption : Security::config()->password_encryption_algorithm, $this->PasswordEncryption : Security::config()->get('password_encryption_algorithm'),
$this $this
); );
@ -1022,8 +897,8 @@ class Member extends DataObject implements TemplateGlobalProvider
// If we haven't manually set a password expiry // If we haven't manually set a password expiry
if (!$this->isChanged('PasswordExpiry')) { if (!$this->isChanged('PasswordExpiry')) {
// then set it for us // then set it for us
if (self::config()->password_expiry_days) { if (static::config()->get('password_expiry_days')) {
$this->PasswordExpiry = date('Y-m-d', time() + 86400 * self::config()->password_expiry_days); $this->PasswordExpiry = date('Y-m-d', time() + 86400 * static::config()->get('password_expiry_days'));
} else { } else {
$this->PasswordExpiry = null; $this->PasswordExpiry = null;
} }
@ -1044,7 +919,7 @@ class Member extends DataObject implements TemplateGlobalProvider
Permission::reset(); Permission::reset();
if ($this->isChanged('Password')) { if ($this->isChanged('Password') && static::config()->get('password_logging_enabled')) {
MemberPassword::log($this); MemberPassword::log($this);
} }
} }
@ -1068,6 +943,7 @@ class Member extends DataObject implements TemplateGlobalProvider
$password->delete(); $password->delete();
$password->destroy(); $password->destroy();
} }
return $this; return $this;
} }
@ -1086,9 +962,10 @@ class Member extends DataObject implements TemplateGlobalProvider
} }
// If there are no admin groups in this set then it's ok // If there are no admin groups in this set then it's ok
$adminGroups = Permission::get_groups_by_permission('ADMIN'); $adminGroups = Permission::get_groups_by_permission('ADMIN');
$adminGroupIDs = ($adminGroups) ? $adminGroups->column('ID') : array(); $adminGroupIDs = ($adminGroups) ? $adminGroups->column('ID') : array();
return count(array_intersect($ids, $adminGroupIDs)) == 0;
return count(array_intersect($ids, $adminGroupIDs)) == 0;
} }
@ -1131,7 +1008,7 @@ class Member extends DataObject implements TemplateGlobalProvider
} elseif ($group instanceof Group) { } elseif ($group instanceof Group) {
$groupCheckObj = $group; $groupCheckObj = $group;
} else { } else {
user_error('Member::inGroup(): Wrong format for $group parameter', E_USER_ERROR); throw new InvalidArgumentException('Member::inGroup(): Wrong format for $group parameter');
} }
if (!$groupCheckObj) { if (!$groupCheckObj) {
@ -1199,10 +1076,17 @@ class Member extends DataObject implements TemplateGlobalProvider
*/ */
public static function set_title_columns($columns, $sep = ' ') public static function set_title_columns($columns, $sep = ' ')
{ {
Deprecation::notice('5.0', 'Use Member.title_format config instead');
if (!is_array($columns)) { if (!is_array($columns)) {
$columns = array($columns); $columns = array($columns);
} }
self::config()->title_format = array('columns' => $columns, 'sep' => $sep); self::config()->set(
'title_format',
[
'columns' => $columns,
'sep' => $sep
]
);
} }
//------------------- HELPER METHODS -----------------------------------// //------------------- HELPER METHODS -----------------------------------//
@ -1219,13 +1103,14 @@ class Member extends DataObject implements TemplateGlobalProvider
*/ */
public function getTitle() public function getTitle()
{ {
$format = $this->config()->title_format; $format = static::config()->get('title_format');
if ($format) { if ($format) {
$values = array(); $values = array();
foreach ($format['columns'] as $col) { foreach ($format['columns'] as $col) {
$values[] = $this->getField($col); $values[] = $this->getField($col);
} }
return join($format['sep'], $values);
return implode($format['sep'], $values);
} }
if ($this->getField('ID') === 0) { if ($this->getField('ID') === 0) {
return $this->getField('Surname'); return $this->getField('Surname');
@ -1250,25 +1135,24 @@ class Member extends DataObject implements TemplateGlobalProvider
*/ */
public static function get_title_sql() public static function get_title_sql()
{ {
// This should be abstracted to SSDatabase concatOperator or similar.
$op = (DB::get_conn() instanceof MSSQLDatabase) ? " + " : " || ";
// Get title_format with fallback to default // Get title_format with fallback to default
$format = static::config()->title_format; $format = static::config()->get('title_format');
if (!$format) { if (!$format) {
$format = [ $format = [
'columns' => ['Surname', 'FirstName'], 'columns' => ['Surname', 'FirstName'],
'sep' => ' ', 'sep' => ' ',
]; ];
} }
$columnsWithTablename = array(); $columnsWithTablename = array();
foreach ($format['columns'] as $column) { foreach ($format['columns'] as $column) {
$columnsWithTablename[] = static::getSchema()->sqlColumnForField(__CLASS__, $column); $columnsWithTablename[] = static::getSchema()->sqlColumnForField(__CLASS__, $column);
} }
$sepSQL = Convert::raw2sql($format['sep'], true); $sepSQL = Convert::raw2sql($format['sep'], true);
return "(".join(" $op $sepSQL $op ", $columnsWithTablename).")"; $op = DB::get_conn()->concatOperator();
return "(" . join(" $op $sepSQL $op ", $columnsWithTablename) . ")";
} }
@ -1339,6 +1223,7 @@ class Member extends DataObject implements TemplateGlobalProvider
if ($locale) { if ($locale) {
return $locale; return $locale;
} }
return i18n::get_locale(); return i18n::get_locale();
} }
@ -1415,16 +1300,18 @@ class Member extends DataObject implements TemplateGlobalProvider
// No groups, return all Members // No groups, return all Members
if (!$groupIDList) { if (!$groupIDList) {
return Member::get()->sort(array('Surname'=>'ASC', 'FirstName'=>'ASC'))->map(); return static::get()->sort(array('Surname' => 'ASC', 'FirstName' => 'ASC'))->map();
} }
$membersList = new ArrayList(); $membersList = new ArrayList();
// This is a bit ineffective, but follow the ORM style // This is a bit ineffective, but follow the ORM style
/** @var Group $group */
foreach (Group::get()->byIDs($groupIDList) as $group) { foreach (Group::get()->byIDs($groupIDList) as $group) {
$membersList->merge($group->Members()); $membersList->merge($group->Members());
} }
$membersList->removeDuplicates('ID'); $membersList->removeDuplicates('ID');
return $membersList->map(); return $membersList->map();
} }
@ -1446,7 +1333,7 @@ class Member extends DataObject implements TemplateGlobalProvider
return ArrayList::create()->map(); return ArrayList::create()->map();
} }
if (!$groups || $groups->Count() == 0) { if (count($groups) == 0) {
$perms = array('ADMIN', 'CMS_ACCESS_AssetAdmin'); $perms = array('ADMIN', 'CMS_ACCESS_AssetAdmin');
if (class_exists(CMSMain::class)) { if (class_exists(CMSMain::class)) {
@ -1479,7 +1366,7 @@ class Member extends DataObject implements TemplateGlobalProvider
} }
/** @skipUpgrade */ /** @skipUpgrade */
$members = Member::get() $members = static::get()
->innerJoin("Group_Members", '"Group_Members"."MemberID" = "Member"."ID"') ->innerJoin("Group_Members", '"Group_Members"."MemberID" = "Member"."ID"')
->innerJoin("Group", '"Group"."ID" = "Group_Members"."GroupID"'); ->innerJoin("Group", '"Group"."ID" = "Group_Members"."GroupID"');
if ($groupIDList) { if ($groupIDList) {
@ -1539,12 +1426,12 @@ class Member extends DataObject implements TemplateGlobalProvider
$mainFields->replaceField('Locale', new DropdownField( $mainFields->replaceField('Locale', new DropdownField(
"Locale", "Locale",
_t(__CLASS__.'.INTERFACELANG', "Interface Language", 'Language of the CMS'), _t(__CLASS__ . '.INTERFACELANG', "Interface Language", 'Language of the CMS'),
i18n::getSources()->getKnownLocales() i18n::getSources()->getKnownLocales()
)); ));
$mainFields->removeByName($this->config()->hidden_fields); $mainFields->removeByName(static::config()->get('hidden_fields'));
if (! $this->config()->lock_out_after_incorrect_logins) { if (!static::config()->get('lock_out_after_incorrect_logins')) {
$mainFields->removeByName('FailedLoginCount'); $mainFields->removeByName('FailedLoginCount');
} }
@ -1570,7 +1457,7 @@ class Member extends DataObject implements TemplateGlobalProvider
->setSource($groupsMap) ->setSource($groupsMap)
->setAttribute( ->setAttribute(
'data-placeholder', 'data-placeholder',
_t(__CLASS__.'.ADDGROUP', 'Add group', 'Placeholder text for a dropdown') _t(__CLASS__ . '.ADDGROUP', 'Add group', 'Placeholder text for a dropdown')
) )
); );
@ -1609,21 +1496,22 @@ class Member extends DataObject implements TemplateGlobalProvider
{ {
$labels = parent::fieldLabels($includerelations); $labels = parent::fieldLabels($includerelations);
$labels['FirstName'] = _t(__CLASS__.'.FIRSTNAME', 'First Name'); $labels['FirstName'] = _t(__CLASS__ . '.FIRSTNAME', 'First Name');
$labels['Surname'] = _t(__CLASS__.'.SURNAME', 'Surname'); $labels['Surname'] = _t(__CLASS__ . '.SURNAME', 'Surname');
/** @skipUpgrade */ /** @skipUpgrade */
$labels['Email'] = _t(__CLASS__.'.EMAIL', 'Email'); $labels['Email'] = _t(__CLASS__ . '.EMAIL', 'Email');
$labels['Password'] = _t(__CLASS__.'.db_Password', 'Password'); $labels['Password'] = _t(__CLASS__ . '.db_Password', 'Password');
$labels['PasswordExpiry'] = _t(__CLASS__.'.db_PasswordExpiry', 'Password Expiry Date', 'Password expiry date'); $labels['PasswordExpiry'] = _t(__CLASS__ . '.db_PasswordExpiry', 'Password Expiry Date', 'Password expiry date');
$labels['LockedOutUntil'] = _t(__CLASS__.'.db_LockedOutUntil', 'Locked out until', 'Security related date'); $labels['LockedOutUntil'] = _t(__CLASS__ . '.db_LockedOutUntil', 'Locked out until', 'Security related date');
$labels['Locale'] = _t(__CLASS__.'.db_Locale', 'Interface Locale'); $labels['Locale'] = _t(__CLASS__ . '.db_Locale', 'Interface Locale');
if ($includerelations) { if ($includerelations) {
$labels['Groups'] = _t( $labels['Groups'] = _t(
__CLASS__.'.belongs_many_many_Groups', __CLASS__ . '.belongs_many_many_Groups',
'Groups', 'Groups',
'Security Groups this member belongs to' 'Security Groups this member belongs to'
); );
} }
return $labels; return $labels;
} }
@ -1639,7 +1527,7 @@ class Member extends DataObject implements TemplateGlobalProvider
{ {
//get member //get member
if (!$member) { if (!$member) {
$member = Member::currentUser(); $member = Security::getCurrentUser();
} }
//check for extensions, we do this first as they can overrule everything //check for extensions, we do this first as they can overrule everything
$extended = $this->extendedCan(__FUNCTION__, $member); $extended = $this->extendedCan(__FUNCTION__, $member);
@ -1655,6 +1543,7 @@ class Member extends DataObject implements TemplateGlobalProvider
if ($this->ID == $member->ID) { if ($this->ID == $member->ID) {
return true; return true;
} }
//standard check //standard check
return Permission::checkMember($member, 'CMS_ACCESS_SecurityAdmin'); return Permission::checkMember($member, 'CMS_ACCESS_SecurityAdmin');
} }
@ -1670,7 +1559,7 @@ class Member extends DataObject implements TemplateGlobalProvider
{ {
//get member //get member
if (!$member) { if (!$member) {
$member = Member::currentUser(); $member = Security::getCurrentUser();
} }
//check for extensions, we do this first as they can overrule everything //check for extensions, we do this first as they can overrule everything
$extended = $this->extendedCan(__FUNCTION__, $member); $extended = $this->extendedCan(__FUNCTION__, $member);
@ -1691,9 +1580,11 @@ class Member extends DataObject implements TemplateGlobalProvider
if ($this->ID == $member->ID) { if ($this->ID == $member->ID) {
return true; return true;
} }
//standard check //standard check
return Permission::checkMember($member, 'CMS_ACCESS_SecurityAdmin'); return Permission::checkMember($member, 'CMS_ACCESS_SecurityAdmin');
} }
/** /**
* Users can edit their own record. * Users can edit their own record.
* Otherwise they'll need ADMIN or CMS_ACCESS_SecurityAdmin permissions * Otherwise they'll need ADMIN or CMS_ACCESS_SecurityAdmin permissions
@ -1704,7 +1595,7 @@ class Member extends DataObject implements TemplateGlobalProvider
public function canDelete($member = null) public function canDelete($member = null)
{ {
if (!$member) { if (!$member) {
$member = Member::currentUser(); $member = Security::getCurrentUser();
} }
//check for extensions, we do this first as they can overrule everything //check for extensions, we do this first as they can overrule everything
$extended = $this->extendedCan(__FUNCTION__, $member); $extended = $this->extendedCan(__FUNCTION__, $member);
@ -1726,10 +1617,11 @@ class Member extends DataObject implements TemplateGlobalProvider
// this is a hack because what this should do is to stop a user // this is a hack because what this should do is to stop a user
// deleting a member who has more privileges (e.g. a non-Admin deleting an Admin) // deleting a member who has more privileges (e.g. a non-Admin deleting an Admin)
if (Permission::checkMember($this, 'ADMIN')) { if (Permission::checkMember($this, 'ADMIN')) {
if (! Permission::checkMember($member, 'ADMIN')) { if (!Permission::checkMember($member, 'ADMIN')) {
return false; return false;
} }
} }
//standard check //standard check
return Permission::checkMember($member, 'CMS_ACCESS_SecurityAdmin'); return Permission::checkMember($member, 'CMS_ACCESS_SecurityAdmin');
} }
@ -1782,13 +1674,14 @@ class Member extends DataObject implements TemplateGlobalProvider
*/ */
public function registerFailedLogin() public function registerFailedLogin()
{ {
if (self::config()->lock_out_after_incorrect_logins) { $lockOutAfterCount = self::config()->get('lock_out_after_incorrect_logins');
if ($lockOutAfterCount) {
// Keep a tally of the number of failed log-ins so that we can lock people out // Keep a tally of the number of failed log-ins so that we can lock people out
$this->FailedLoginCount = $this->FailedLoginCount + 1; $this->FailedLoginCount = $this->FailedLoginCount + 1;
if ($this->FailedLoginCount >= self::config()->lock_out_after_incorrect_logins) { if ($this->FailedLoginCount >= $lockOutAfterCount) {
$lockoutMins = self::config()->lock_out_delay_mins; $lockoutMins = self::config()->get('lock_out_delay_mins');
$this->LockedOutUntil = date('Y-m-d H:i:s', DBDatetime::now()->getTimestamp() + $lockoutMins*60); $this->LockedOutUntil = date('Y-m-d H:i:s', DBDatetime::now()->getTimestamp() + $lockoutMins * 60);
$this->FailedLoginCount = 0; $this->FailedLoginCount = 0;
} }
} }
@ -1801,7 +1694,7 @@ class Member extends DataObject implements TemplateGlobalProvider
*/ */
public function registerSuccessfulLogin() public function registerSuccessfulLogin()
{ {
if (self::config()->lock_out_after_incorrect_logins) { if (self::config()->get('lock_out_after_incorrect_logins')) {
// Forgive all past login failures // Forgive all past login failures
$this->FailedLoginCount = 0; $this->FailedLoginCount = 0;
$this->write(); $this->write();
@ -1834,12 +1727,4 @@ class Member extends DataObject implements TemplateGlobalProvider
// If can't find a suitable editor, just default to cms // If can't find a suitable editor, just default to cms
return $currentName ? $currentName : 'cms'; return $currentName ? $currentName : 'cms';
} }
public static function get_template_global_variables()
{
return array(
'CurrentMember' => 'currentUser',
'currentUser',
);
}
} }

View File

@ -1,219 +0,0 @@
<?php
namespace SilverStripe\Security;
use SilverStripe\Control\Controller;
use SilverStripe\Control\Session;
use SilverStripe\Forms\Form;
use SilverStripe\ORM\ValidationResult;
use InvalidArgumentException;
/**
* Authenticator for the default "member" method
*
* @author Markus Lanthaler <markus@silverstripe.com>
*/
class MemberAuthenticator extends Authenticator
{
/**
* Contains encryption algorithm identifiers.
* If set, will migrate to new precision-safe password hashing
* upon login. See http://open.silverstripe.org/ticket/3004
*
* @var array
*/
private static $migrate_legacy_hashes = array(
'md5' => 'md5_v2.4',
'sha1' => 'sha1_v2.4'
);
/**
* Attempt to find and authenticate member if possible from the given data
*
* @param array $data
* @param Form $form
* @param bool &$success Success flag
* @return Member Found member, regardless of successful login
*/
protected static function authenticate_member($data, $form, &$success)
{
// Default success to false
$success = false;
// Attempt to identify by temporary ID
$member = null;
$email = null;
if (!empty($data['tempid'])) {
// Find user by tempid, in case they are re-validating an existing session
$member = Member::member_from_tempid($data['tempid']);
if ($member) {
$email = $member->Email;
}
}
// Otherwise, get email from posted value instead
/** @skipUpgrade */
if (!$member && !empty($data['Email'])) {
$email = $data['Email'];
}
// Check default login (see Security::setDefaultAdmin())
$asDefaultAdmin = $email === Security::default_admin_username();
if ($asDefaultAdmin) {
// If logging is as default admin, ensure record is setup correctly
$member = Member::default_admin();
$success = !$member->isLockedOut() && Security::check_default_admin($email, $data['Password']);
//protect against failed login
if ($success) {
return $member;
}
}
// Attempt to identify user by email
if (!$member && $email) {
// Find user by email
$member = Member::get()
->filter(Member::config()->unique_identifier_field, $email)
->first();
}
// Validate against member if possible
if ($member && !$asDefaultAdmin) {
$result = $member->checkPassword($data['Password']);
$success = $result->isValid();
} else {
$result = ValidationResult::create()->addError(_t(
'SilverStripe\\Security\\Member.ERRORWRONGCRED',
'The provided details don\'t seem to be correct. Please try again.'
));
}
// Emit failure to member and form (if available)
if (!$success) {
if ($member) {
$member->registerFailedLogin();
}
if ($form) {
$form->setSessionValidationResult($result, true);
}
} else {
if ($member) {
$member->registerSuccessfulLogin();
}
}
return $member;
}
/**
* Log login attempt
* TODO We could handle this with an extension
*
* @param array $data
* @param Member $member
* @param bool $success
*/
protected static function record_login_attempt($data, $member, $success)
{
if (!Security::config()->login_recording) {
return;
}
// Check email is valid
/** @skipUpgrade */
$email = isset($data['Email']) ? $data['Email'] : null;
if (is_array($email)) {
throw new InvalidArgumentException("Bad email passed to MemberAuthenticator::authenticate(): $email");
}
$attempt = new LoginAttempt();
if ($success) {
// successful login (member is existing with matching password)
$attempt->MemberID = $member->ID;
$attempt->Status = 'Success';
// Audit logging hook
$member->extend('authenticated');
} else {
// Failed login - we're trying to see if a user exists with this email (disregarding wrong passwords)
$attempt->Status = 'Failure';
if ($member) {
// Audit logging hook
$attempt->MemberID = $member->ID;
$member->extend('authenticationFailed');
} else {
// Audit logging hook
Member::singleton()->extend('authenticationFailedUnknownUser', $data);
}
}
$attempt->Email = $email;
$attempt->IP = Controller::curr()->getRequest()->getIP();
$attempt->write();
}
/**
* Method to authenticate an user
*
* @param array $data Raw data to authenticate the user
* @param Form $form Optional: If passed, better error messages can be
* produced by using
* {@link Form::sessionMessage()}
* @return bool|Member Returns FALSE if authentication fails, otherwise
* the member object
* @see Security::setDefaultAdmin()
*/
public static function authenticate($data, Form $form = null)
{
// Find authenticated member
$member = static::authenticate_member($data, $form, $success);
// Optionally record every login attempt as a {@link LoginAttempt} object
static::record_login_attempt($data, $member, $success);
// Legacy migration to precision-safe password hashes.
// A login-event with cleartext passwords is the only time
// when we can rehash passwords to a different hashing algorithm,
// bulk-migration doesn't work due to the nature of hashing.
// See PasswordEncryptor_LegacyPHPHash class.
if ($success && $member && isset(self::$migrate_legacy_hashes[$member->PasswordEncryption])) {
$member->Password = $data['Password'];
$member->PasswordEncryption = self::$migrate_legacy_hashes[$member->PasswordEncryption];
$member->write();
}
if ($success) {
Session::clear('BackURL');
}
return $success ? $member : null;
}
/**
* Method that creates the login form for this authentication method
*
* @param Controller $controller The parent controller, necessary to create the
* appropriate form action tag
* @return Form Returns the login form to use with this authentication
* method
*/
public static function get_login_form(Controller $controller)
{
/** @skipUpgrade */
return MemberLoginForm::create($controller, self::class, "LoginForm");
}
public static function get_cms_login_form(Controller $controller)
{
/** @skipUpgrade */
return CMSMemberLoginForm::create($controller, self::class, "LoginForm");
}
public static function supports_cms()
{
// Don't automatically support subclasses of MemberAuthenticator
return get_called_class() === __CLASS__;
}
}

View File

@ -1,27 +1,28 @@
<?php <?php
namespace SilverStripe\Security; namespace SilverStripe\Security\MemberAuthenticator;
use SilverStripe\Control\HTTPResponse; use SilverStripe\Control\HTTPResponse;
use SilverStripe\Core\Convert; use SilverStripe\Core\Convert;
use SilverStripe\Security\CMSSecurity;
use SilverStripe\Security\Security;
class CMSMemberLoginHandler extends MemberLoginHandler class CMSLoginHandler extends LoginHandler
{ {
/** private static $allowed_actions = [
* Login form handler method 'LoginForm'
* ];
* This method is called when the user clicks on "Log in"
*
* @param array $data Submitted data
* @return HTTPResponse
*/
public function dologin($data)
{
if ($this->performLogin($data)) {
return $this->logInUserAndRedirect($data);
}
return $this->redirectBackToForm(); /**
* Return the CMSMemberLoginForm form
*/
public function loginForm()
{
return CMSMemberLoginForm::create(
$this,
get_class($this->authenticator),
'LoginForm'
);
} }
public function redirectBackToForm() public function redirectBackToForm()
@ -75,13 +76,12 @@ PHP
/** /**
* Send user to the right location after login * Send user to the right location after login
* *
* @param array $data
* @return HTTPResponse * @return HTTPResponse
*/ */
protected function logInUserAndRedirect($data) protected function redirectAfterSuccessfulLogin()
{ {
// Check password expiry // Check password expiry
if (Member::currentUser()->isPasswordExpired()) { if (Security::getCurrentUser()->isPasswordExpired()) {
// Redirect the user to the external password change form if necessary // Redirect the user to the external password change form if necessary
return $this->redirectToChangePassword(); return $this->redirectToChangePassword();
} }

View File

@ -0,0 +1,45 @@
<?php
namespace SilverStripe\Security\MemberAuthenticator;
use SilverStripe\ORM\ValidationResult;
use SilverStripe\Security\Authenticator as BaseAuthenticator;
use SilverStripe\Security\Member;
class CMSMemberAuthenticator extends MemberAuthenticator
{
public function supportedServices()
{
return BaseAuthenticator::CMS_LOGIN;
}
/**
* @param array $data
* @param ValidationResult|null $result
* @param Member|null $member
* @return Member
*/
protected function authenticateMember($data, &$result = null, $member = null)
{
// Attempt to identify by temporary ID
if (!empty($data['tempid'])) {
// Find user by tempid, in case they are re-validating an existing session
$member = Member::member_from_tempid($data['tempid']);
if ($member) {
$data['email'] = $member->Email;
}
}
return parent::authenticateMember($data, $result, $member);
}
/**
* @param string $link
* @return CMSLoginHandler
*/
public function getLoginHandler($link)
{
return CMSLoginHandler::create($link, $this);
}
}

View File

@ -0,0 +1,81 @@
<?php
namespace SilverStripe\Security\MemberAuthenticator;
use SilverStripe\Control\RequestHandler;
use SilverStripe\Control\Session;
use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\Form;
use SilverStripe\Forms\FormAction;
use SilverStripe\Forms\FormField;
use SilverStripe\Forms\HiddenField;
use SilverStripe\Forms\PasswordField;
use SilverStripe\Security\Security;
/**
* Standard Change Password Form
*/
class ChangePasswordForm extends Form
{
/**
* Constructor
*
* @param RequestHandler $controller The parent controller, necessary to create the appropriate form action tag.
* @param string $name The method on the controller that will return this form object.
* @param FieldList|FormField $fields All of the fields in the form - a {@link FieldList} of
* {@link FormField} objects.
* @param FieldList|FormAction $actions All of the action buttons in the form - a {@link FieldList} of
*/
public function __construct($controller, $name, $fields = null, $actions = null)
{
$backURL = $controller->getBackURL() ?: Session::get('BackURL');
if (!$fields) {
$fields = $this->getFormFields();
}
if (!$actions) {
$actions = $this->getFormActions();
}
if ($backURL) {
$fields->push(HiddenField::create('BackURL', false, $backURL));
}
parent::__construct($controller, $name, $fields, $actions);
}
/**
* @return FieldList
*/
protected function getFormFields()
{
$fields = FieldList::create();
// Security/changepassword?h=XXX redirects to Security/changepassword
// without GET parameter to avoid potential HTTP referer leakage.
// In this case, a user is not logged in, and no 'old password' should be necessary.
if (Security::getCurrentUser()) {
$fields->push(PasswordField::create('OldPassword', _t('SilverStripe\\Security\\Member.YOUROLDPASSWORD', 'Your old password')));
}
$fields->push(PasswordField::create('NewPassword1', _t('SilverStripe\\Security\\Member.NEWPASSWORD', 'New Password')));
$fields->push(PasswordField::create('NewPassword2', _t('SilverStripe\\Security\\Member.CONFIRMNEWPASSWORD', 'Confirm New Password')));
return $fields;
}
/**
* @return FieldList
*/
protected function getFormActions()
{
$actions = FieldList::create(
FormAction::create(
'doChangePassword',
_t('SilverStripe\\Security\\Member.BUTTONCHANGEPASSWORD', 'Change Password')
)
);
return $actions;
}
}

View File

@ -0,0 +1,308 @@
<?php
namespace SilverStripe\Security\MemberAuthenticator;
use SilverStripe\Control\Controller;
use SilverStripe\Control\HTTPResponse;
use SilverStripe\Control\RequestHandler;
use SilverStripe\Control\Session;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\ORM\FieldType\DBDatetime;
use SilverStripe\ORM\FieldType\DBField;
use SilverStripe\Security\Authenticator;
use SilverStripe\Security\IdentityStore;
use SilverStripe\Security\Member;
use SilverStripe\Security\Security;
class ChangePasswordHandler extends RequestHandler
{
/**
* @var Authenticator
*/
protected $authenticator;
/**
* @var string
*/
protected $link;
/**
* @var array Allowed Actions
*/
private static $allowed_actions = [
'changepassword',
'changePasswordForm',
];
/**
* @var array URL Handlers. All should point to changepassword
*/
private static $url_handlers = [
'' => 'changepassword',
];
/**
* @param string $link The URL to recreate this request handler
* @param MemberAuthenticator $authenticator
*/
public function __construct($link, MemberAuthenticator $authenticator)
{
$this->link = $link;
$this->authenticator = $authenticator;
parent::__construct();
}
/**
* Handle the change password request
* @todo this could use some spring cleaning
*
* @return array|HTTPResponse
*/
public function changepassword()
{
$request = $this->getRequest();
// Extract the member from the URL.
/** @var Member $member */
$member = null;
if ($request->getVar('m') !== null) {
$member = Member::get()->filter(['ID' => (int)$request->getVar('m')])->first();
}
$token = $request->getVar('t');
// Check whether we are merely changin password, or resetting.
if ($token !== null && $member && $member->validateAutoLoginToken($token)) {
$this->setSessionToken($member, $token);
// Redirect to myself, but without the hash in the URL
return $this->redirect($this->link);
}
if (Session::get('AutoLoginHash')) {
$message = DBField::create_field(
'HTMLFragment',
'<p>' . _t(
'SilverStripe\\Security\\Security.ENTERNEWPASSWORD',
'Please enter a new password.'
) . '</p>'
);
// Subsequent request after the "first load with hash" (see previous if clause).
return [
'Content' => $message,
'Form' => $this->changePasswordForm()
];
}
if (Security::getCurrentUser()) {
// Logged in user requested a password change form.
$message = DBField::create_field(
'HTMLFragment',
'<p>' . _t(
'SilverStripe\\Security\\Security.CHANGEPASSWORDBELOW',
'You can change your password below.'
) . '</p>'
);
return [
'Content' => $message,
'Form' => $this->changePasswordForm()
];
}
// Show a friendly message saying the login token has expired
if ($token !== null && $member && !$member->validateAutoLoginToken($token)) {
$message = [
'Content' => DBField::create_field(
'HTMLFragment',
_t(
'SilverStripe\\Security\\Security.NOTERESETLINKINVALID',
'<p>The password reset link is invalid or expired.</p>'
. '<p>You can request a new one <a href="{link1}">here</a> or change your password after'
. ' you <a href="{link2}">logged in</a>.</p>',
[
'link1' => $this->link('lostpassword'),
'link2' => $this->link('login')
]
)
)
];
return [
'Content' => $message,
];
}
// Someone attempted to go to changepassword without token or being logged in
return Security::permissionFailure(
Controller::curr(),
_t(
'SilverStripe\\Security\\Security.ERRORPASSWORDPERMISSION',
'You must be logged in in order to change your password!'
)
);
}
/**
* @param Member $member
* @param string $token
*/
protected function setSessionToken($member, $token)
{
// if there is a current member, they should be logged out
if ($curMember = Security::getCurrentUser()) {
/** @var LogoutHandler $handler */
Injector::inst()->get(IdentityStore::class)->logOut();
}
// Store the hash for the change password form. Will be unset after reload within the ChangePasswordForm.
Session::set('AutoLoginHash', $member->encryptWithUserSettings($token));
}
/**
* Return a link to this request handler.
* The link returned is supplied in the constructor
* @param null $action
* @return string
*/
public function link($action = null)
{
if ($action) {
return Controller::join_links($this->link, $action);
}
return $this->link;
}
/**
* Factory method for the lost password form
*
* @skipUpgrade
* @return ChangePasswordForm Returns the lost password form
*/
public function changePasswordForm()
{
return ChangePasswordForm::create(
$this,
'ChangePasswordForm'
);
}
/**
* Change the password
*
* @param array $data The user submitted data
* @param ChangePasswordForm $form
* @return HTTPResponse
*/
public function doChangePassword(array $data, $form)
{
$member = Security::getCurrentUser();
// The user was logged in, check the current password
if ($member && (
empty($data['OldPassword']) ||
!$member->checkPassword($data['OldPassword'])->isValid()
)
) {
$form->sessionMessage(
_t(
'SilverStripe\\Security\\Member.ERRORPASSWORDNOTMATCH',
'Your current password does not match, please try again'
),
'bad'
);
// redirect back to the form, instead of using redirectBack() which could send the user elsewhere.
return $this->redirectBackToForm();
}
if (!$member) {
if (Session::get('AutoLoginHash')) {
$member = Member::member_from_autologinhash(Session::get('AutoLoginHash'));
}
// The user is not logged in and no valid auto login hash is available
if (!$member) {
Session::clear('AutoLoginHash');
return $this->redirect($this->addBackURLParam(Security::singleton()->Link('login')));
}
}
// Check the new password
if (empty($data['NewPassword1'])) {
$form->sessionMessage(
_t(
'SilverStripe\\Security\\Member.EMPTYNEWPASSWORD',
"The new password can't be empty, please try again"
),
'bad'
);
// redirect back to the form, instead of using redirectBack() which could send the user elsewhere.
return $this->redirectBackToForm();
}
// Fail if passwords do not match
if ($data['NewPassword1'] !== $data['NewPassword2']) {
$form->sessionMessage(
_t(
'SilverStripe\\Security\\Member.ERRORNEWPASSWORD',
'You have entered your new password differently, try again'
),
'bad'
);
// redirect back to the form, instead of using redirectBack() which could send the user elsewhere.
return $this->redirectBackToForm();
}
// Check if the new password is accepted
$validationResult = $member->changePassword($data['NewPassword1']);
if (!$validationResult->isValid()) {
$form->setSessionValidationResult($validationResult);
return $this->redirectBackToForm();
}
// Clear locked out status
$member->LockedOutUntil = null;
$member->FailedLoginCount = null;
// Clear the members login hashes
$member->AutoLoginHash = null;
$member->AutoLoginExpired = DBDatetime::create()->now();
$member->write();
if ($member->canLogIn()->isValid()) {
Injector::inst()->get(IdentityStore::class)->logIn($member, false, $this->getRequest());
}
// TODO Add confirmation message to login redirect
Session::clear('AutoLoginHash');
// Redirect to backurl
$backURL = $this->getBackURL();
if ($backURL) {
return $this->redirect($backURL);
}
// Redirect to default location - the login form saying "You are logged in as..."
$url = Security::singleton()->Link('login');
return $this->redirect($url);
}
/**
* Something went wrong, go back to the changepassword
*
* @return HTTPResponse
*/
public function redirectBackToForm()
{
// Redirect back to form
$url = $this->addBackURLParam(Security::singleton()->Link('changepassword'));
return $this->redirect($url);
}
}

View File

@ -0,0 +1,245 @@
<?php
namespace SilverStripe\Security\MemberAuthenticator;
use SilverStripe\Control\Cookie;
use SilverStripe\Control\HTTPRequest;
use SilverStripe\ORM\FieldType\DBDatetime;
use SilverStripe\Security\AuthenticationHandler;
use SilverStripe\Security\IdentityStore;
use SilverStripe\Security\Member;
use SilverStripe\Security\RememberLoginHash;
use SilverStripe\Security\Security;
/**
* Authenticate a member pased on a session cookie
*/
class CookieAuthenticationHandler implements AuthenticationHandler
{
/**
* @var string
*/
private $deviceCookieName;
/**
* @var string
*/
private $tokenCookieName;
/**
* @var IdentityStore
*/
private $cascadeInTo;
/**
* Get the name of the cookie used to track this device
*
* @return string
*/
public function getDeviceCookieName()
{
return $this->deviceCookieName;
}
/**
* Set the name of the cookie used to track this device
*
* @param string $deviceCookieName
* @return $this
*/
public function setDeviceCookieName($deviceCookieName)
{
$this->deviceCookieName = $deviceCookieName;
return $this;
}
/**
* Get the name of the cookie used to store an login token
*
* @return string
*/
public function getTokenCookieName()
{
return $this->tokenCookieName;
}
/**
* Set the name of the cookie used to store an login token
*
* @param string $tokenCookieName
* @return $this
*/
public function setTokenCookieName($tokenCookieName)
{
$this->tokenCookieName = $tokenCookieName;
return $this;
}
/**
* Once a member is found by authenticateRequest() pass it to this identity store
*
* @return IdentityStore
*/
public function getCascadeLogInTo()
{
return $this->cascadeInTo;
}
/**
* Set the name of the cookie used to store an login token
*
* @param IdentityStore $cascadeInTo
* @return $this
*/
public function setCascadeLogInTo(IdentityStore $cascadeInTo)
{
$this->cascadeInTo = $cascadeInTo;
return $this;
}
/**
* @param HTTPRequest $request
* @return Member
*/
public function authenticateRequest(HTTPRequest $request)
{
$uidAndToken = Cookie::get($this->getTokenCookieName());
$deviceID = Cookie::get($this->getDeviceCookieName());
// @todo Consider better placement of database_is_ready test
if ($deviceID === null || strpos($uidAndToken, ':') === false || !Security::database_is_ready()) {
return null;
}
list($uid, $token) = explode(':', $uidAndToken, 2);
if (!$uid || !$token) {
return null;
}
// check if autologin token matches
/** @var Member $member */
$member = Member::get()->byID($uid);
if (!$member) {
return null;
}
$hash = $member->encryptWithUserSettings($token);
/** @var RememberLoginHash $rememberLoginHash */
$rememberLoginHash = RememberLoginHash::get()
->filter(array(
'MemberID' => $member->ID,
'DeviceID' => $deviceID,
'Hash' => $hash
))->first();
if (!$rememberLoginHash) {
return null;
}
// Check for expired token
$expiryDate = new \DateTime($rememberLoginHash->ExpiryDate);
$now = DBDatetime::now();
$now = new \DateTime($now->Rfc2822());
if ($now > $expiryDate) {
return null;
}
if ($this->cascadeInTo) {
// @todo look at how to block "regular login" triggers from happening here
// @todo deal with the fact that the Session::current_session() isn't correct here :-/
$this->cascadeInTo->logIn($member, false, $request);
}
// @todo Consider whether response should be part of logIn() as well
// Renew the token
$rememberLoginHash->renew();
$tokenExpiryDays = RememberLoginHash::config()->uninherited('token_expiry_days');
Cookie::set(
$this->getTokenCookieName(),
$member->ID . ':' . $rememberLoginHash->getToken(),
$tokenExpiryDays,
null,
null,
false,
true
);
// Audit logging hook
$member->extend('memberAutoLoggedIn');
return $member;
}
/**
* @param Member $member
* @param bool $persistent
* @param HTTPRequest $request
*/
public function logIn(Member $member, $persistent = false, HTTPRequest $request = null)
{
// Cleans up any potential previous hash for this member on this device
if ($alcDevice = Cookie::get($this->getDeviceCookieName())) {
RememberLoginHash::get()->filter('DeviceID', $alcDevice)->removeAll();
}
// Set a cookie for persistent log-ins
if ($persistent) {
$rememberLoginHash = RememberLoginHash::generate($member);
$tokenExpiryDays = RememberLoginHash::config()->uninherited('token_expiry_days');
$deviceExpiryDays = RememberLoginHash::config()->uninherited('device_expiry_days');
Cookie::set(
$this->getTokenCookieName(),
$member->ID . ':' . $rememberLoginHash->getToken(),
$tokenExpiryDays,
null,
null,
null,
true
);
Cookie::set(
$this->getDeviceCookieName(),
$rememberLoginHash->DeviceID,
$deviceExpiryDays,
null,
null,
null,
true
);
} else {
// Clear a cookie for non-persistent log-ins
$this->clearCookies();
}
}
/**
* @param HTTPRequest $request
*/
public function logOut(HTTPRequest $request = null)
{
$member = Security::getCurrentUser();
if ($member) {
RememberLoginHash::clear($member, Cookie::get('alc_device'));
}
$this->clearCookies();
if ($this->cascadeInTo) {
$this->cascadeInTo->logOut($request);
}
Security::setCurrentUser(null);
}
/**
* Clear the cookies set for the user
*/
protected function clearCookies()
{
Cookie::set($this->getTokenCookieName(), null);
Cookie::set($this->getDeviceCookieName(), null);
Cookie::force_expiry($this->getTokenCookieName());
Cookie::force_expiry($this->getDeviceCookieName());
}
}

View File

@ -0,0 +1,257 @@
<?php
namespace SilverStripe\Security\MemberAuthenticator;
use SilverStripe\Control\Controller;
use SilverStripe\Control\HTTPRequest;
use SilverStripe\Control\HTTPResponse;
use SilverStripe\Control\RequestHandler;
use SilverStripe\Control\Session;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\ORM\ValidationResult;
use SilverStripe\Security\Authenticator;
use SilverStripe\Security\IdentityStore;
use SilverStripe\Security\Member;
use SilverStripe\Security\Security;
/**
* Handle login requests from MemberLoginForm
*/
class LoginHandler extends RequestHandler
{
/**
* @var Authenticator
*/
protected $authenticator;
/**
* @var array
*/
private static $url_handlers = [
'' => 'login',
];
/**
* @var array
* @config
*/
private static $allowed_actions = [
'login',
'LoginForm',
'logout',
];
/**
* @var string Called link on this handler
*/
private $link;
/**
* @param string $link The URL to recreate this request handler
* @param MemberAuthenticator $authenticator The authenticator to use
*/
public function __construct($link, MemberAuthenticator $authenticator)
{
$this->link = $link;
$this->authenticator = $authenticator;
parent::__construct();
}
/**
* Return a link to this request handler.
* The link returned is supplied in the constructor
* @param null|string $action
* @return string
*/
public function link($action = null)
{
if ($action) {
return Controller::join_links($this->link, $action);
}
return $this->link;
}
/**
* URL handler for the log-in screen
*
* @return array
*/
public function login()
{
return [
'Form' => $this->loginForm(),
];
}
/**
* Return the MemberLoginForm form
*
* @return MemberLoginForm
*/
public function loginForm()
{
return MemberLoginForm::create(
$this,
get_class($this->authenticator),
'LoginForm'
);
}
/**
* Login form handler method
*
* This method is called when the user finishes the login flow
*
* @param array $data Submitted data
* @param MemberLoginForm $form
* @return HTTPResponse
*/
public function doLogin($data, $form)
{
$failureMessage = null;
$this->extend('beforeLogin');
// Successful login
if ($member = $this->checkLogin($data, $result)) {
$this->performLogin($member, $data, $form->getRequestHandler()->getRequest());
// Allow operations on the member after successful login
$this->extend('afterLogin', $member);
return $this->redirectAfterSuccessfulLogin();
}
$this->extend('failedLogin');
$message = implode("; ", array_map(
function ($message) {
return $message['message'];
},
$result->getMessages()
));
$form->sessionMessage($message, 'bad');
// Failed login
/** @skipUpgrade */
if (array_key_exists('Email', $data)) {
$rememberMe = (isset($data['Remember']) && Security::config()->get('autologin_enabled') === true);
Session::set('SessionForms.MemberLoginForm.Email', $data['Email']);
Session::set('SessionForms.MemberLoginForm.Remember', $rememberMe);
}
// Fail to login redirects back to form
return $form->getRequestHandler()->redirectBackToForm();
}
public function getReturnReferer()
{
return $this->link();
}
/**
* Login in the user and figure out where to redirect the browser.
*
* The $data has this format
* array(
* 'AuthenticationMethod' => 'MemberAuthenticator',
* 'Email' => 'sam@silverstripe.com',
* 'Password' => '1nitialPassword',
* 'BackURL' => 'test/link',
* [Optional: 'Remember' => 1 ]
* )
*
* @return HTTPResponse
*/
protected function redirectAfterSuccessfulLogin()
{
Session::clear('SessionForms.MemberLoginForm.Email');
Session::clear('SessionForms.MemberLoginForm.Remember');
$member = Security::getCurrentUser();
if ($member->isPasswordExpired()) {
return $this->redirectToChangePassword();
}
// Absolute redirection URLs may cause spoofing
$backURL = $this->getBackURL();
if ($backURL) {
return $this->redirect($backURL);
}
// If a default login dest has been set, redirect to that.
$defaultLoginDest = Security::config()->get('default_login_dest');
if ($defaultLoginDest) {
return $this->redirect($defaultLoginDest);
}
// Redirect the user to the page where they came from
if ($member) {
// Welcome message
$message = _t(
'SilverStripe\\Security\\Member.WELCOMEBACK',
'Welcome Back, {firstname}',
['firstname' => $member->FirstName]
);
Security::singleton()->setLoginMessage($message, ValidationResult::TYPE_GOOD);
}
// Redirect back
return $this->redirectBack();
}
/**
* Try to authenticate the user
*
* @param array $data Submitted data
* @param ValidationResult $result
* @return Member Returns the member object on successful authentication
* or NULL on failure.
*/
public function checkLogin($data, &$result)
{
$member = $this->authenticator->authenticate($data, $result);
if ($member instanceof Member) {
return $member;
}
return null;
}
/**
* Try to authenticate the user
*
* @param Member $member
* @param array $data Submitted data
* @param HTTPRequest $request
* @return Member Returns the member object on successful authentication
* or NULL on failure.
*/
public function performLogin($member, $data, $request)
{
/** IdentityStore */
$rememberMe = (isset($data['Remember']) && Security::config()->get('autologin_enabled'));
Injector::inst()->get(IdentityStore::class)->logIn($member, $rememberMe, $request);
return $member;
}
/**
* Invoked if password is expired and must be changed
*
* @skipUpgrade
* @return HTTPResponse
*/
protected function redirectToChangePassword()
{
$cp = ChangePasswordForm::create($this, 'ChangePasswordForm');
$cp->sessionMessage(
_t('SilverStripe\\Security\\Member.PASSWORDEXPIRED', 'Your password has expired. Please choose a new one.'),
'good'
);
$changedPasswordLink = Security::singleton()->Link('changepassword');
return $this->redirect($this->addBackURLParam($changedPasswordLink));
}
}

View File

@ -0,0 +1,64 @@
<?php
namespace SilverStripe\Security\MemberAuthenticator;
use SilverStripe\Control\RequestHandler;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\Security\IdentityStore;
use SilverStripe\Security\Member;
use SilverStripe\Security\Security;
/**
* Class LogoutHandler handles logging out Members from their session and/or cookie.
* The logout process destroys all traces of the member on the server (not the actual computer user
* at the other end of the line, don't worry)
*
*/
class LogoutHandler extends RequestHandler
{
/**
* @var array
*/
private static $url_handlers = [
'' => 'logout'
];
/**
* @var array
*/
private static $allowed_actions = [
'logout'
];
/**
* Log out form handler method
*
* This method is called when the user clicks on "logout" on the form
* created when the parameter <i>$checkCurrentUser</i> of the
* {@link __construct constructor} was set to TRUE and the user was
* currently logged in.
*
* @return bool|Member
*/
public function logout()
{
$member = Security::getCurrentUser();
return $this->doLogOut($member);
}
/**
*
* @param Member $member
* @return bool|Member Return a member if something goes wrong
*/
public function doLogOut($member)
{
if ($member instanceof Member) {
Injector::inst()->get(IdentityStore::class)->logOut($this->getRequest());
}
return true;
}
}

View File

@ -0,0 +1,45 @@
<?php
namespace SilverStripe\Security\MemberAuthenticator;
use SilverStripe\Forms\EmailField;
use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\FormAction;
/**
* Class LostPasswordForm handles the requests for lost password form generation
*
* We need the MemberLoginForm for the getFormFields logic.
*/
class LostPasswordForm extends MemberLoginForm
{
/**
* Create a single EmailField form that has the capability
* of using the MemberLoginForm Authenticator
*
* @return FieldList
*/
public function getFormFields()
{
return FieldList::create(
EmailField::create('Email', _t('SilverStripe\\Security\\Member.EMAIL', 'Email'))
);
}
/**
* Give the member a friendly button to push
*
* @return FieldList
*/
public function getFormActions()
{
return FieldList::create(
FormAction::create(
'forgotPassword',
_t('SilverStripe\\Security\\Security.BUTTONSEND', 'Send me the password reset link')
)
);
}
}

View File

@ -0,0 +1,230 @@
<?php
namespace SilverStripe\Security\MemberAuthenticator;
use SilverStripe\Control\Controller;
use SilverStripe\Control\Email\Email;
use SilverStripe\Control\HTTPResponse;
use SilverStripe\Control\RequestHandler;
use SilverStripe\Core\Convert;
use SilverStripe\Forms\Form;
use SilverStripe\ORM\FieldType\DBField;
use SilverStripe\Security\Member;
use SilverStripe\Security\Security;
/**
* Handle login requests from MemberLoginForm
*/
class LostPasswordHandler extends RequestHandler
{
/**
* Authentication class to use
* @var string
*/
protected $authenticatorClass = MemberAuthenticator::class;
/**
* @var array
*/
private static $url_handlers = [
'passwordsent/$EmailAddress' => 'passwordsent',
'' => 'lostpassword',
];
/**
* Since the logout and dologin actions may be conditionally removed, it's necessary to ensure these
* remain valid actions regardless of the member login state.
*
* @var array
* @config
*/
private static $allowed_actions = [
'lostpassword',
'LostPasswordForm',
'passwordsent',
];
private $link = null;
/**
* @param string $link The URL to recreate this request handler
*/
public function __construct($link)
{
$this->link = $link;
parent::__construct();
}
/**
* Return a link to this request handler.
* The link returned is supplied in the constructor
*
* @param string $action
* @return string
*/
public function link($action = null)
{
if ($action) {
return Controller::join_links($this->link, $action);
}
return $this->link;
}
/**
* URL handler for the initial lost-password screen
*
* @return array
*/
public function lostpassword()
{
$message = _t(
'SilverStripe\\Security\\Security.NOTERESETPASSWORD',
'Enter your e-mail address and we will send you a link with which you can reset your password'
);
return [
'Content' => DBField::create_field('HTMLFragment', "<p>$message</p>"),
'Form' => $this->lostPasswordForm(),
];
}
/**
* Show the "password sent" page, after a user has requested
* to reset their password.
*
* @return array
*/
public function passwordsent()
{
$request = $this->getRequest();
$email = Convert::raw2xml(rawurldecode($request->param('EmailAddress')) . '.' . $request->getExtension());
$message = _t(
'SilverStripe\\Security\\Security.PASSWORDSENTTEXT',
"Thank you! A reset link has been sent to '{email}', provided an account exists for this email"
. " address.",
['email' => Convert::raw2xml($email)]
);
return [
'Title' => _t(
'SilverStripe\\Security\\Security.PASSWORDSENTHEADER',
"Password reset link sent to '{email}'",
array('email' => $email)
),
'Content' => DBField::create_field('HTMLFragment', "<p>$message</p>"),
'Email' => $email
];
}
/**
* Factory method for the lost password form
*
* @skipUpgrade
* @return Form Returns the lost password form
*/
public function lostPasswordForm()
{
return LostPasswordForm::create(
$this,
$this->authenticatorClass,
'lostPasswordForm',
null,
null,
false
);
}
/**
* Redirect to password recovery form
*
* @return HTTPResponse
*/
public function redirectToLostPassword()
{
$lostPasswordLink = Security::singleton()->Link('lostpassword');
return $this->redirect($this->addBackURLParam($lostPasswordLink));
}
/**
* Forgot password form handler method.
* Called when the user clicks on "I've lost my password".
* Extensions can use the 'forgotPassword' method to veto executing
* the logic, by returning FALSE. In this case, the user will be redirected back
* to the form without further action. It is recommended to set a message
* in the form detailing why the action was denied.
*
* @skipUpgrade
* @param array $data Submitted data
* @param LostPasswordForm $form
* @return HTTPResponse
*/
public function forgotPassword($data, $form)
{
// Ensure password is given
if (empty($data['Email'])) {
$form->sessionMessage(
_t(
'SilverStripe\\Security\\Member.ENTEREMAIL',
'Please enter an email address to get a password reset link.'
),
'bad'
);
return $this->redirectToLostPassword();
}
// Find existing member
$field = Member::config()->get('unique_identifier_field');
/** @var Member $member */
$member = Member::get()->filter([$field => $data['Email']])->first();
// Allow vetoing forgot password requests
$results = $this->extend('forgotPassword', $member);
if ($results && is_array($results) && in_array(false, $results, true)) {
return $this->redirectToLostPassword();
}
if ($member) {
$token = $member->generateAutologinTokenAndStoreHash();
$this->sendEmail($member, $token);
}
// Avoid information disclosure by displaying the same status,
// regardless wether the email address actually exists
$link = Controller::join_links(
$this->link('passwordsent'),
rawurlencode($data['Email']),
'/'
);
return $this->redirect($this->addBackURLParam($link));
}
/**
* Send the email to the member that requested a reset link
* @param Member $member
* @param string $token
* @return bool
*/
protected function sendEmail($member, $token)
{
/** @var Email $email */
$email = Email::create()
->setHTMLTemplate('SilverStripe\\Control\\Email\\ForgotPasswordEmail')
->setData($member)
->setSubject(_t(
'SilverStripe\\Security\\Member.SUBJECTPASSWORDRESET',
"Your password reset link",
'Email subject'
))
->addData('PasswordResetLink', Security::getPasswordResetLink($member, $token))
->setTo($member->Email);
return $email->send();
}
}

View File

@ -0,0 +1,199 @@
<?php
namespace SilverStripe\Security\MemberAuthenticator;
use InvalidArgumentException;
use SilverStripe\Control\Controller;
use SilverStripe\Control\Session;
use SilverStripe\ORM\ValidationResult;
use SilverStripe\Security\Authenticator;
use SilverStripe\Security\LoginAttempt;
use SilverStripe\Security\Member;
use SilverStripe\Security\Security;
/**
* Authenticator for the default "member" method
*
* @author Sam Minnee <sam@silverstripe.com>
* @author Simon Erkelens <simonerkelens@silverstripe.com>
*/
class MemberAuthenticator implements Authenticator
{
public function supportedServices()
{
// Bitwise-OR of all the supported services in this Authenticator, to make a bitmask
return Authenticator::LOGIN | Authenticator::LOGOUT | Authenticator::CHANGE_PASSWORD
| Authenticator::RESET_PASSWORD;
}
/**
* @param array $data
* @param null|ValidationResult $result
* @return null|Member
*/
public function authenticate($data, &$result = null)
{
// Find authenticated member
$member = $this->authenticateMember($data, $result);
// Optionally record every login attempt as a {@link LoginAttempt} object
$this->recordLoginAttempt($data, $member, $result->isValid());
if ($member) {
Session::clear('BackURL');
}
return $result->isValid() ? $member : null;
}
/**
* Attempt to find and authenticate member if possible from the given data
*
* @param array $data Form submitted data
* @param ValidationResult $result
* @param Member|null This third parameter is used in the CMSAuthenticator(s)
* @return Member Found member, regardless of successful login
*/
protected function authenticateMember($data, &$result = null, $member = null)
{
// Default success to false
$email = !empty($data['Email']) ? $data['Email'] : null;
$result = new ValidationResult();
// Check default login (see Security::setDefaultAdmin())
$asDefaultAdmin = $email === Security::default_admin_username();
if ($asDefaultAdmin) {
// If logging is as default admin, ensure record is setup correctly
$member = Member::default_admin();
$success = Security::check_default_admin($email, $data['Password']);
$result = $member->canLogIn();
//protect against failed login
if ($success && $result->isValid()) {
return $member;
} else {
$result->addError(_t(
'SilverStripe\\Security\\Member.ERRORWRONGCRED',
"The provided details don't seem to be correct. Please try again."
));
}
}
// Attempt to identify user by email
if (!$member && $email) {
// Find user by email
/** @var Member $member */
$member = Member::get()
->filter([Member::config()->get('unique_identifier_field') => $email])
->first();
}
// Validate against member if possible
if ($member && !$asDefaultAdmin) {
$result = $member->checkPassword($data['Password']);
}
// Emit failure to member and form (if available)
if (!$result->isValid()) {
if ($member) {
$member->registerFailedLogin();
}
} else {
if ($member) {
$member->registerSuccessfulLogin();
} else {
// A non-existing member occurred. This will make the result "valid" so let's invalidate
$result->addError(_t(
'SilverStripe\\Security\\Member.ERRORWRONGCRED',
"The provided details don't seem to be correct. Please try again."
));
$member = null;
}
}
return $member;
}
/**
* Log login attempt
* TODO We could handle this with an extension
*
* @param array $data
* @param Member $member
* @param boolean $success
*/
protected function recordLoginAttempt($data, $member, $success)
{
if (!Security::config()->get('login_recording')) {
return;
}
// Check email is valid
/** @skipUpgrade */
$email = isset($data['Email']) ? $data['Email'] : null;
if (is_array($email)) {
throw new InvalidArgumentException("Bad email passed to MemberAuthenticator::authenticate(): $email");
}
$attempt = LoginAttempt::create();
if ($success && $member) {
// successful login (member is existing with matching password)
$attempt->MemberID = $member->ID;
$attempt->Status = 'Success';
// Audit logging hook
$member->extend('authenticated');
} else {
// Failed login - we're trying to see if a user exists with this email (disregarding wrong passwords)
$attempt->Status = 'Failure';
if ($member) {
// Audit logging hook
$attempt->MemberID = $member->ID;
$member->extend('authenticationFailed');
} else {
// Audit logging hook
Member::singleton()->extend('authenticationFailedUnknownUser', $data);
}
}
$attempt->Email = $email;
$attempt->IP = Controller::curr()->getRequest()->getIP();
$attempt->write();
}
/**
* @param string $link
* @return LostPasswordHandler
*/
public function getLostPasswordHandler($link)
{
return LostPasswordHandler::create($link, $this);
}
/**
* @param string $link
* @return ChangePasswordHandler
*/
public function getChangePasswordHandler($link)
{
return ChangePasswordHandler::create($link, $this);
}
/**
* @param string $link
* @return LoginHandler
*/
public function getLoginHandler($link)
{
return LoginHandler::create($link, $this);
}
/**
* @param string $link
* @return LogoutHandler
*/
public function getLogoutHandler($link)
{
return LogoutHandler::create($link, $this);
}
}

View File

@ -1,19 +1,23 @@
<?php <?php
namespace SilverStripe\Security; namespace SilverStripe\Security\MemberAuthenticator;
use SilverStripe\Control\Director; use SilverStripe\Control\Director;
use SilverStripe\Control\RequestHandler;
use SilverStripe\Control\Session; use SilverStripe\Control\Session;
use SilverStripe\Control\Controller; use SilverStripe\Forms\CheckboxField;
use SilverStripe\Forms\HiddenField;
use SilverStripe\Forms\FieldList; use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\FormAction; use SilverStripe\Forms\FormAction;
use SilverStripe\Forms\TextField; use SilverStripe\Forms\HiddenField;
use SilverStripe\Forms\PasswordField;
use SilverStripe\Forms\CheckboxField;
use SilverStripe\Forms\LiteralField; use SilverStripe\Forms\LiteralField;
use SilverStripe\Forms\PasswordField;
use SilverStripe\Forms\RequiredFields; use SilverStripe\Forms\RequiredFields;
use SilverStripe\Forms\TextField;
use SilverStripe\ORM\ValidationResult; use SilverStripe\ORM\ValidationResult;
use SilverStripe\Security\LoginForm as BaseLoginForm;
use SilverStripe\Security\Member;
use SilverStripe\Security\RememberLoginHash;
use SilverStripe\Security\Security;
use SilverStripe\View\Requirements; use SilverStripe\View\Requirements;
/** /**
@ -26,7 +30,7 @@ use SilverStripe\View\Requirements;
* allowing extensions to "veto" execution by returning FALSE. * allowing extensions to "veto" execution by returning FALSE.
* Arguments: $member containing the detected Member record * Arguments: $member containing the detected Member record
*/ */
class MemberLoginForm extends LoginForm class MemberLoginForm extends BaseLoginForm
{ {
/** /**
@ -37,15 +41,20 @@ class MemberLoginForm extends LoginForm
/** /**
* Required fields for validation * Required fields for validation
*
* @config
* @var array * @var array
*/ */
private static $required_fields; private static $required_fields = [
'Email',
'Password',
];
/** /**
* Constructor * Constructor
* *
* @skipUpgrade * @skipUpgrade
* @param Controller $controller The parent controller, necessary to * @param RequestHandler $controller The parent controller, necessary to
* create the appropriate form action tag. * create the appropriate form action tag.
* @param string $authenticatorClass Authenticator for this LoginForm * @param string $authenticatorClass Authenticator for this LoginForm
* @param string $name The method on the controller that will return this * @param string $name The method on the controller that will return this
@ -69,6 +78,7 @@ class MemberLoginForm extends LoginForm
$checkCurrentUser = true $checkCurrentUser = true
) { ) {
$this->controller = $controller;
$this->authenticator_class = $authenticatorClass; $this->authenticator_class = $authenticatorClass;
$customCSS = project() . '/css/member_login.css'; $customCSS = project() . '/css/member_login.css';
@ -76,18 +86,17 @@ class MemberLoginForm extends LoginForm
Requirements::css($customCSS); Requirements::css($customCSS);
} }
if ($controller->request->getVar('BackURL')) { if ($checkCurrentUser && Security::getCurrentUser()) {
$backURL = $controller->request->getVar('BackURL'); // @todo find a more elegant way to handle this
} else { $logoutAction = Security::logout_url();
$backURL = Session::get('BackURL');
}
if ($checkCurrentUser && Member::currentUser() && Member::logged_in_session_exists()) {
$fields = FieldList::create( $fields = FieldList::create(
HiddenField::create("AuthenticationMethod", null, $this->authenticator_class, $this) HiddenField::create('AuthenticationMethod', null, $this->authenticator_class, $this)
); );
$actions = FieldList::create( $actions = FieldList::create(
FormAction::create("logout", _t('SilverStripe\\Security\\Member.BUTTONLOGINOTHER', "Log in as someone else")) FormAction::create('logout', _t(
'SilverStripe\\Security\\Member.BUTTONLOGINOTHER',
'Log in as someone else'
))
); );
} else { } else {
if (!$fields) { if (!$fields) {
@ -98,15 +107,14 @@ class MemberLoginForm extends LoginForm
} }
} }
if (isset($backURL)) {
$fields->push(HiddenField::create('BackURL', 'BackURL', $backURL));
}
// Reduce attack surface by enforcing POST requests // Reduce attack surface by enforcing POST requests
$this->setFormMethod('POST', true); $this->setFormMethod('POST', true);
parent::__construct($controller, $name, $fields, $actions); parent::__construct($controller, $name, $fields, $actions);
if (isset($logoutAction)) {
$this->setFormAction($logoutAction);
}
$this->setValidator(RequiredFields::create(self::config()->get('required_fields'))); $this->setValidator(RequiredFields::create(self::config()->get('required_fields')));
} }
@ -117,6 +125,12 @@ class MemberLoginForm extends LoginForm
*/ */
protected function getFormFields() protected function getFormFields()
{ {
if ($this->controller->request->getVar('BackURL')) {
$backURL = $this->controller->request->getVar('BackURL');
} else {
$backURL = Session::get('BackURL');
}
$label = Member::singleton()->fieldLabel(Member::config()->unique_identifier_field); $label = Member::singleton()->fieldLabel(Member::config()->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),
@ -128,14 +142,14 @@ class MemberLoginForm extends LoginForm
); );
$emailField->setAttribute('autofocus', 'true'); $emailField->setAttribute('autofocus', 'true');
if (Security::config()->remember_username) { if (Security::config()->get('remember_username')) {
$emailField->setValue(Session::get('SessionForms.MemberLoginForm.Email')); $emailField->setValue(Session::get('SessionForms.MemberLoginForm.Email'));
} else { } else {
// Some browsers won't respect this attribute unless it's added to the form // Some browsers won't respect this attribute unless it's added to the form
$this->setAttribute('autocomplete', 'off'); $this->setAttribute('autocomplete', 'off');
$emailField->setAttribute('autocomplete', 'off'); $emailField->setAttribute('autocomplete', 'off');
} }
if (Security::config()->autologin_enabled) { if (Security::config()->get('autologin_enabled')) {
$fields->push( $fields->push(
CheckboxField::create( CheckboxField::create(
"Remember", "Remember",
@ -150,6 +164,10 @@ class MemberLoginForm extends LoginForm
); );
} }
if (isset($backURL)) {
$fields->push(HiddenField::create('BackURL', 'BackURL', $backURL));
}
return $fields; return $fields;
} }
@ -161,7 +179,7 @@ class MemberLoginForm extends LoginForm
protected function getFormActions() protected function getFormActions()
{ {
$actions = FieldList::create( $actions = FieldList::create(
FormAction::create('dologin', _t('SilverStripe\\Security\\Member.BUTTONLOGIN', "Log in")), FormAction::create('doLogin', _t('SilverStripe\\Security\\Member.BUTTONLOGIN', "Log in")),
LiteralField::create( LiteralField::create(
'forgotPassword', 'forgotPassword',
'<p id="ForgotPassword"><a href="' . Security::lost_password_url() . '">' '<p id="ForgotPassword"><a href="' . Security::lost_password_url() . '">'
@ -177,7 +195,7 @@ class MemberLoginForm extends LoginForm
parent::restoreFormState(); parent::restoreFormState();
$forceMessage = Session::get('MemberLoginForm.force_message'); $forceMessage = Session::get('MemberLoginForm.force_message');
if (($member = Member::currentUser()) && !$forceMessage) { if (($member = Security::getCurrentUser()) && !$forceMessage) {
$message = _t( $message = _t(
'SilverStripe\\Security\\Member.LOGGEDINAS', 'SilverStripe\\Security\\Member.LOGGEDINAS',
"You're logged in as {name}.", "You're logged in as {name}.",
@ -194,14 +212,6 @@ class MemberLoginForm extends LoginForm
return $this; return $this;
} }
/**
* @return MemberLoginHandler
*/
protected function buildRequestHandler()
{
return MemberLoginHandler::create($this);
}
/** /**
* The name of this login form, to display in the frontend * The name of this login form, to display in the frontend
* Replaces Authenticator::get_name() * Replaces Authenticator::get_name()

View File

@ -0,0 +1,107 @@
<?php
namespace SilverStripe\Security\MemberAuthenticator;
use SilverStripe\Control\Cookie;
use SilverStripe\Control\Director;
use SilverStripe\Control\HTTPRequest;
use SilverStripe\Control\Session;
use SilverStripe\Security\AuthenticationHandler;
use SilverStripe\Security\Member;
/**
* Authenticate a member pased on a session cookie
*/
class SessionAuthenticationHandler implements AuthenticationHandler
{
/**
* @var string
*/
private $sessionVariable;
/**
* Get the session variable name used to track member ID
*
* @return string
*/
public function getSessionVariable()
{
return $this->sessionVariable;
}
/**
* Set the session variable name used to track member ID
*
* @param string $sessionVariable
*/
public function setSessionVariable($sessionVariable)
{
$this->sessionVariable = $sessionVariable;
}
/**
* @param HTTPRequest $request
* @return Member
*/
public function authenticateRequest(HTTPRequest $request)
{
// If ID is a bad ID it will be treated as if the user is not logged in, rather than throwing a
// ValidationException
$id = Session::get($this->getSessionVariable());
if (!$id) {
return null;
}
/** @var Member $member */
$member = Member::get()->byID($id);
return $member;
}
/**
* @param Member $member
* @param bool $persistent
* @param HTTPRequest $request
*/
public function logIn(Member $member, $persistent = false, HTTPRequest $request = null)
{
static::regenerateSessionId();
Session::set($this->getSessionVariable(), $member->ID);
// This lets apache rules detect whether the user has logged in
// @todo make this a setting on the authentication handler
if (Member::config()->get('login_marker_cookie')) {
Cookie::set(Member::config()->get('login_marker_cookie'), 1, 0);
}
}
/**
* Regenerate the session_id.
*/
protected static function regenerateSessionId()
{
if (!Member::config()->get('session_regenerate_id')) {
return;
}
// This can be called via CLI during testing.
if (Director::is_cli()) {
return;
}
$file = '';
$line = '';
// @ is to supress win32 warnings/notices when session wasn't cleaned up properly
// There's nothing we can do about this, because it's an operating system function!
if (!headers_sent($file, $line)) {
@session_regenerate_id(true);
}
}
/**
* @param HTTPRequest $request
*/
public function logOut(HTTPRequest $request = null)
{
Session::clear($this->getSessionVariable());
}
}

View File

@ -1,240 +0,0 @@
<?php
namespace SilverStripe\Security;
use SilverStripe\Control\Controller;
use SilverStripe\Control\Email\Email;
use SilverStripe\Control\HTTPResponse;
use SilverStripe\Control\Session;
use SilverStripe\Forms\FormRequestHandler;
use SilverStripe\ORM\ValidationResult;
/**
* Handle login requests from MemberLoginForm
*/
class MemberLoginHandler extends FormRequestHandler
{
protected $authenticator_class = MemberAuthenticator::class;
/**
* Since the logout and dologin actions may be conditionally removed, it's necessary to ensure these
* remain valid actions regardless of the member login state.
*
* @var array
* @config
*/
private static $allowed_actions = [
'dologin',
'logout',
];
/**
* Login form handler method
*
* This method is called when the user clicks on "Log in"
*
* @param array $data Submitted data
* @return HTTPResponse
*/
public function dologin($data)
{
if ($this->performLogin($data)) {
return $this->logInUserAndRedirect($data);
}
/** @skipUpgrade */
if (array_key_exists('Email', $data)) {
Session::set('SessionForms.MemberLoginForm.Email', $data['Email']);
Session::set('SessionForms.MemberLoginForm.Remember', isset($data['Remember']));
}
// Fail to login redirects back to form
return $this->redirectBackToForm();
}
/**
* Redirect to password recovery form
*
* @return HTTPResponse
*/
public function redirectToLostPassword()
{
$lostPasswordLink = Security::singleton()->Link('lostpassword');
return $this->redirect($this->addBackURLParam($lostPasswordLink));
}
public function getReturnReferer()
{
// Home of login form is always this url
return Security::singleton()->Link('login');
}
/**
* Login in the user and figure out where to redirect the browser.
*
* The $data has this format
* array(
* 'AuthenticationMethod' => 'MemberAuthenticator',
* 'Email' => 'sam@silverstripe.com',
* 'Password' => '1nitialPassword',
* 'BackURL' => 'test/link',
* [Optional: 'Remember' => 1 ]
* )
*
* @param array $data
* @return HTTPResponse
*/
protected function logInUserAndRedirect($data)
{
Session::clear('SessionForms.MemberLoginForm.Email');
Session::clear('SessionForms.MemberLoginForm.Remember');
$member = Member::currentUser();
if ($member->isPasswordExpired()) {
return $this->redirectToChangePassword();
}
// Absolute redirection URLs may cause spoofing
$backURL = $this->getBackURL();
if ($backURL) {
return $this->redirect($backURL);
}
// If a default login dest has been set, redirect to that.
$defaultLoginDest = Security::config()->get('default_login_dest');
if ($defaultLoginDest) {
return $this->redirect($defaultLoginDest);
}
// Redirect the user to the page where they came from
if ($member) {
if (!empty($data['Remember'])) {
Session::set('SessionForms.MemberLoginForm.Remember', '1');
$member->logIn(true);
} else {
$member->logIn();
}
// Welcome message
$message = _t(
'SilverStripe\\Security\\Member.WELCOMEBACK',
"Welcome Back, {firstname}",
['firstname' => $member->FirstName]
);
Security::setLoginMessage($message, ValidationResult::TYPE_GOOD);
}
// Redirect back
return $this->redirectBack();
}
/**
* Log out form handler method
*
* This method is called when the user clicks on "logout" on the form
* created when the parameter <i>$checkCurrentUser</i> of the
* {@link __construct constructor} was set to TRUE and the user was
* currently logged in.
*
* @return HTTPResponse
*/
public function logout()
{
return Security::singleton()->logout();
}
/**
* Try to authenticate the user
*
* @param array $data Submitted data
* @return Member Returns the member object on successful authentication
* or NULL on failure.
*/
public function performLogin($data)
{
$member = call_user_func_array(
[$this->authenticator_class, 'authenticate'],
[$data, $this->form]
);
if ($member) {
$member->LogIn(isset($data['Remember']));
return $member;
}
// No member, can't login
$this->extend('authenticationFailed', $data);
return null;
}
/**
* Forgot password form handler method.
* Called when the user clicks on "I've lost my password".
* Extensions can use the 'forgotPassword' method to veto executing
* the logic, by returning FALSE. In this case, the user will be redirected back
* to the form without further action. It is recommended to set a message
* in the form detailing why the action was denied.
*
* @skipUpgrade
* @param array $data Submitted data
* @return HTTPResponse
*/
public function forgotPassword($data)
{
// Ensure password is given
if (empty($data['Email'])) {
$this->form->sessionMessage(
_t('SilverStripe\\Security\\Member.ENTEREMAIL', 'Please enter an email address to get a password reset link.'),
'bad'
);
return $this->redirectToLostPassword();
}
// Find existing member
/** @var Member $member */
$member = Member::get()->filter("Email", $data['Email'])->first();
// Allow vetoing forgot password requests
$results = $this->extend('forgotPassword', $member);
if ($results && is_array($results) && in_array(false, $results, true)) {
return $this->redirectToLostPassword();
}
if ($member) {
$token = $member->generateAutologinTokenAndStoreHash();
Email::create()
->setHTMLTemplate('SilverStripe\\Control\\Email\\ForgotPasswordEmail')
->setData($member)
->setSubject(_t('SilverStripe\\Security\\Member.SUBJECTPASSWORDRESET', "Your password reset link", 'Email subject'))
->addData('PasswordResetLink', Security::getPasswordResetLink($member, $token))
->setTo($member->Email)
->send();
}
// Avoid information disclosure by displaying the same status,
// regardless wether the email address actually exists
$link = Controller::join_links(
Security::singleton()->Link('passwordsent'),
rawurlencode($data['Email']),
'/'
);
return $this->redirect($this->addBackURLParam($link));
}
/**
* Invoked if password is expired and must be changed
*
* @skipUpgrade
* @return HTTPResponse
*/
protected function redirectToChangePassword()
{
$cp = ChangePasswordForm::create($this->form->getController(), 'ChangePasswordForm');
$cp->sessionMessage(
_t('SilverStripe\\Security\\Member.PASSWORDEXPIRED', 'Your password has expired. Please choose a new one.'),
'good'
);
$changedPasswordLink = Security::singleton()->Link('changepassword');
return $this->redirect($this->addBackURLParam($changedPasswordLink));
}
}

View File

@ -109,7 +109,7 @@ class Member_GroupSet extends ManyManyList
{ {
$id = $this->getForeignID(); $id = $this->getForeignID();
if ($id) { if ($id) {
return DataObject::get_by_id('SilverStripe\\Security\\Member', $id); return DataObject::get_by_id(Member::class, $id);
} }
} }
} }

View File

@ -2,8 +2,8 @@
namespace SilverStripe\Security; namespace SilverStripe\Security;
use SilverStripe\Forms\RequiredFields;
use SilverStripe\Forms\GridField\GridFieldDetailForm_ItemRequest; use SilverStripe\Forms\GridField\GridFieldDetailForm_ItemRequest;
use SilverStripe\Forms\RequiredFields;
/** /**
* Member Validator * Member Validator

View File

@ -2,8 +2,8 @@
namespace SilverStripe\Security; namespace SilverStripe\Security;
use SilverStripe\Core\Config\Config;
use ReflectionClass; use ReflectionClass;
use SilverStripe\Core\Config\Config;
/** /**
* Allows pluggable password encryption. * Allows pluggable password encryption.

View File

@ -6,9 +6,9 @@ use SilverStripe\Core\ClassInfo;
use SilverStripe\Core\Resettable; use SilverStripe\Core\Resettable;
use SilverStripe\Dev\TestOnly; use SilverStripe\Dev\TestOnly;
use SilverStripe\i18n\i18nEntityProvider; use SilverStripe\i18n\i18nEntityProvider;
use SilverStripe\ORM\DB;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\ArrayList; use SilverStripe\ORM\ArrayList;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\DB;
use SilverStripe\ORM\SS_List; use SilverStripe\ORM\SS_List;
use SilverStripe\View\TemplateGlobalProvider; use SilverStripe\View\TemplateGlobalProvider;
@ -131,10 +131,10 @@ class Permission extends DataObject implements TemplateGlobalProvider, Resettabl
public static function check($code, $arg = "any", $member = null, $strict = true) public static function check($code, $arg = "any", $member = null, $strict = true)
{ {
if (!$member) { if (!$member) {
if (!Member::currentUserID()) { if (!Security::getCurrentUser()) {
return false; return false;
} }
$member = Member::currentUserID(); $member = Security::getCurrentUser();
} }
return self::checkMember($member, $code, $arg, $strict); return self::checkMember($member, $code, $arg, $strict);
@ -171,10 +171,9 @@ class Permission extends DataObject implements TemplateGlobalProvider, Resettabl
public static function checkMember($member, $code, $arg = "any", $strict = true) public static function checkMember($member, $code, $arg = "any", $strict = true)
{ {
if (!$member) { if (!$member) {
$memberID = $member = Member::currentUserID(); $member = Security::getCurrentUser();
} else {
$memberID = (is_object($member)) ? $member->ID : $member;
} }
$memberID = ($member instanceof Member) ? $member->ID : $member;
if (!$memberID) { if (!$memberID) {
return false; return false;
@ -347,7 +346,7 @@ class Permission extends DataObject implements TemplateGlobalProvider, Resettabl
{ {
// Default to current member, with session-caching // Default to current member, with session-caching
if (!$memberID) { if (!$memberID) {
$member = Member::currentUser(); $member = Security::getCurrentUser();
if ($member && isset($_SESSION['Permission_groupList'][$member->ID])) { if ($member && isset($_SESSION['Permission_groupList'][$member->ID])) {
return $_SESSION['Permission_groupList'][$member->ID]; return $_SESSION['Permission_groupList'][$member->ID];
} }
@ -459,7 +458,7 @@ class Permission extends DataObject implements TemplateGlobalProvider, Resettabl
/** /**
* Returns all members for a specific permission. * Returns all members for a specific permission.
* *
* @param $code String|array Either a single permission code, or a list of permission codes * @param string|array $code Either a single permission code, or a list of permission codes
* @return SS_List Returns a set of member that have the specified * @return SS_List Returns a set of member that have the specified
* permission. * permission.
*/ */

View File

@ -2,14 +2,13 @@
namespace SilverStripe\Security; namespace SilverStripe\Security;
use InvalidArgumentException;
use SilverStripe\Core\Config\Config; use SilverStripe\Core\Config\Config;
use SilverStripe\Forms\FormField; use SilverStripe\Forms\FormField;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\SS_List;
use SilverStripe\ORM\ArrayList; use SilverStripe\ORM\ArrayList;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\DataObjectInterface; use SilverStripe\ORM\DataObjectInterface;
use SilverStripe\View\Requirements; use SilverStripe\ORM\SS_List;
use InvalidArgumentException;
/** /**
* Shows a categorized list of available permissions (through {@link Permission::get_codes()}). * Shows a categorized list of available permissions (through {@link Permission::get_codes()}).

View File

@ -2,10 +2,10 @@
namespace SilverStripe\Security; namespace SilverStripe\Security;
use SilverStripe\ORM\FieldType\DBDatetime;
use SilverStripe\ORM\DataObject;
use DateTime;
use DateInterval; use DateInterval;
use DateTime;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\FieldType\DBDatetime;
/** /**
* Persists a token associated with a device for users who opted for the "Remember Me" * Persists a token associated with a device for users who opted for the "Remember Me"
@ -16,7 +16,8 @@ use DateInterval;
* is discarded as well. * is discarded as well.
* *
* @property string $DeviceID * @property string $DeviceID
* @property string $RememberLoginHash * @property string $ExpiryDate
* @property string $Hash
* @method Member Member() * @method Member Member()
*/ */
class RememberLoginHash extends DataObject class RememberLoginHash extends DataObject

View File

@ -0,0 +1,83 @@
<?php
namespace SilverStripe\Security;
use SilverStripe\Control\HTTPRequest;
/**
* Core authentication handler / store
*/
class RequestAuthenticationHandler implements AuthenticationHandler
{
/**
* @var AuthenticationHandler[]
*/
protected $handlers = [];
/**
* This method currently uses a fallback as loading the handlers via YML has proven unstable
*
* @return AuthenticationHandler[]
*/
protected function getHandlers()
{
return $this->handlers;
}
/**
* Set an associative array of handlers
*
* @param AuthenticationHandler[] $handlers
* @return $this
*/
public function setHandlers(array $handlers)
{
$this->handlers = $handlers;
return $this;
}
public function authenticateRequest(HTTPRequest $request)
{
/** @var AuthenticationHandler $handler */
foreach ($this->getHandlers() as $name => $handler) {
// in order to add cookies, etc
$member = $handler->authenticateRequest($request);
if ($member) {
Security::setCurrentUser($member);
return;
}
}
}
/**
* Log into the identity-store handlers attached to this request filter
*
* @param Member $member
* @param bool $persistent
* @param HTTPRequest $request
*/
public function logIn(Member $member, $persistent = false, HTTPRequest $request = null)
{
$member->beforeMemberLoggedIn();
foreach ($this->getHandlers() as $handler) {
$handler->logIn($member, $persistent, $request);
}
Security::setCurrentUser($member);
$member->afterMemberLoggedIn();
}
/**
* Log out of all the identity-store handlers attached to this request filter
*
* @param HTTPRequest $request
*/
public function logOut(HTTPRequest $request = null)
{
foreach ($this->getHandlers() as $handler) {
$handler->logOut($request);
}
Security::setCurrentUser(null);
}
}

View File

@ -2,33 +2,32 @@
namespace SilverStripe\Security; namespace SilverStripe\Security;
use Page;
use LogicException; use LogicException;
use SilverStripe\CMS\Controllers\ContentController; use Page;
use SilverStripe\CMS\Controllers\ModelAsController;
use SilverStripe\Control\Controller; use SilverStripe\Control\Controller;
use SilverStripe\Control\Director; use SilverStripe\Control\Director;
use SilverStripe\Control\HTTPRequest; use SilverStripe\Control\HTTPRequest;
use SilverStripe\Control\HTTPResponse; use SilverStripe\Control\HTTPResponse;
use SilverStripe\Control\HTTPResponse_Exception;
use SilverStripe\Control\RequestHandler;
use SilverStripe\Control\Session; use SilverStripe\Control\Session;
use SilverStripe\Core\ClassInfo; use SilverStripe\Core\ClassInfo;
use SilverStripe\Core\Config\Config;
use SilverStripe\Core\Convert; use SilverStripe\Core\Convert;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\Dev\Deprecation; use SilverStripe\Dev\Deprecation;
use SilverStripe\Dev\TestOnly; use SilverStripe\Dev\TestOnly;
use SilverStripe\Forms\EmailField;
use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\Form; use SilverStripe\Forms\Form;
use SilverStripe\Forms\FormAction;
use SilverStripe\ORM\ArrayList; use SilverStripe\ORM\ArrayList;
use SilverStripe\ORM\DB; use SilverStripe\ORM\DataModel;
use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\DB;
use SilverStripe\ORM\FieldType\DBField; use SilverStripe\ORM\FieldType\DBField;
use SilverStripe\ORM\FieldType\DBHTMLText;
use SilverStripe\ORM\ValidationResult; use SilverStripe\ORM\ValidationResult;
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 Exception;
use SilverStripe\View\ViewableData_Customised;
use Subsite; use Subsite;
/** /**
@ -46,13 +45,10 @@ class Security extends Controller implements TemplateGlobalProvider
'passwordsent', 'passwordsent',
'changepassword', 'changepassword',
'ping', 'ping',
'LoginForm',
'ChangePasswordForm',
'LostPasswordForm',
); );
/** /**
* Default user name. Only used in dev-mode by {@link setDefaultAdmin()} * Default user name. {@link setDefaultAdmin()}
* *
* @var string * @var string
* @see setDefaultAdmin() * @see setDefaultAdmin()
@ -60,7 +56,7 @@ class Security extends Controller implements TemplateGlobalProvider
protected static $default_username; protected static $default_username;
/** /**
* Default password. Only used in dev-mode by {@link setDefaultAdmin()} * Default password. {@link setDefaultAdmin()}
* *
* @var string * @var string
* @see setDefaultAdmin() * @see setDefaultAdmin()
@ -74,7 +70,7 @@ class Security extends Controller implements TemplateGlobalProvider
* @config * @config
* @var bool * @var bool
*/ */
protected static $strict_path_checking = false; private static $strict_path_checking = false;
/** /**
* The password encryption algorithm to use by default. * The password encryption algorithm to use by default.
@ -96,7 +92,7 @@ class Security extends Controller implements TemplateGlobalProvider
/** /**
* Determine if login username may be remembered between login sessions * Determine if login username may be remembered between login sessions
* If set to false this will disable autocomplete and prevent username persisting in the session * If set to false this will disable auto-complete and prevent username persisting in the session
* *
* @config * @config
* @var bool * @var bool
@ -118,7 +114,7 @@ class Security extends Controller implements TemplateGlobalProvider
private static $template = 'BlankPage'; private static $template = 'BlankPage';
/** /**
* Template thats used to render the pages. * Template that is used to render the pages.
* *
* @var string * @var string
* @config * @config
@ -157,7 +153,7 @@ class Security extends Controller implements TemplateGlobalProvider
* *
* @var string * @var string
*/ */
private static $login_url = "Security/login"; private static $login_url = 'Security/login';
/** /**
* The default logout URL * The default logout URL
@ -166,7 +162,7 @@ class Security extends Controller implements TemplateGlobalProvider
* *
* @var string * @var string
*/ */
private static $logout_url = "Security/logout"; private static $logout_url = 'Security/logout';
/** /**
* The default lost password URL * The default lost password URL
@ -175,7 +171,7 @@ class Security extends Controller implements TemplateGlobalProvider
* *
* @var string * @var string
*/ */
private static $lost_password_url = "Security/lostpassword"; private static $lost_password_url = 'Security/lostpassword';
/** /**
* Value of X-Frame-Options header * Value of X-Frame-Options header
@ -206,7 +202,7 @@ class Security extends Controller implements TemplateGlobalProvider
* @var boolean If set to TRUE or FALSE, {@link database_is_ready()} * @var boolean If set to TRUE or FALSE, {@link database_is_ready()}
* will always return FALSE. Used for unit testing. * will always return FALSE. Used for unit testing.
*/ */
static $force_database_is_ready = null; protected static $force_database_is_ready;
/** /**
* When the database has once been verified as ready, it will not do the * When the database has once been verified as ready, it will not do the
@ -214,7 +210,107 @@ class Security extends Controller implements TemplateGlobalProvider
* *
* @var bool * @var bool
*/ */
static $database_is_ready = false; protected static $database_is_ready = false;
/**
* @var Authenticator[] available authenticators
*/
private $authenticators = [];
/**
* @var Member Currently logged in user (if available)
*/
protected static $currentUser;
/**
* @return Authenticator[]
*/
public function getAuthenticators()
{
return $this->authenticators;
}
/**
* @param Authenticator[] $authenticators
*/
public function setAuthenticators(array $authenticators)
{
$this->authenticators = $authenticators;
}
protected function init()
{
parent::init();
// Prevent clickjacking, see https://developer.mozilla.org/en-US/docs/HTTP/X-Frame-Options
$frameOptions = static::config()->get('frame_options');
if ($frameOptions) {
$this->getResponse()->addHeader('X-Frame-Options', $frameOptions);
}
// Prevent search engines from indexing the login page
$robotsTag = static::config()->get('robots_tag');
if ($robotsTag) {
$this->getResponse()->addHeader('X-Robots-Tag', $robotsTag);
}
}
public function index()
{
return $this->httpError(404); // no-op
}
/**
* Get the selected authenticator for this request
*
* @param string $name The identifier of the authenticator in your config
* @return Authenticator Class name of Authenticator
* @throws LogicException
*/
protected function getAuthenticator($name = 'default')
{
$authenticators = $this->authenticators;
if (isset($authenticators[$name])) {
return $authenticators[$name];
}
throw new LogicException('No valid authenticator found');
}
/**
* Get all registered authenticators
*
* @param int $service The type of service that is requested
* @return Authenticator[] Return an array of Authenticator objects
*/
public function getApplicableAuthenticators($service = Authenticator::LOGIN)
{
$authenticators = $this->authenticators;
/** @var Authenticator $authenticator */
foreach ($authenticators as $name => $authenticator) {
if (!($authenticator->supportedServices() & $service)) {
unset($authenticators[$name]);
}
}
return $authenticators;
}
/**
* Check if a given authenticator is registered
*
* @param string $authenticator The configured identifier of the authenicator
* @return bool Returns TRUE if the authenticator is registered, FALSE
* otherwise.
*/
public function hasAuthenticator($authenticator)
{
$authenticators = $this->authenticators;
return !empty($authenticators[$authenticator]);
}
/** /**
* Register that we've had a permission failure trying to view the given page * Register that we've had a permission failure trying to view the given page
@ -252,14 +348,19 @@ class Security extends Controller implements TemplateGlobalProvider
if (Director::is_ajax()) { if (Director::is_ajax()) {
$response = ($controller) ? $controller->getResponse() : new HTTPResponse(); $response = ($controller) ? $controller->getResponse() : new HTTPResponse();
$response->setStatusCode(403); $response->setStatusCode(403);
if (!Member::currentUser()) { if (!static::getCurrentUser()) {
$response->setBody(_t('SilverStripe\\CMS\\Controllers\\ContentController.NOTLOGGEDIN', 'Not logged in')); $response->setBody(
$response->setStatusDescription(_t('SilverStripe\\CMS\\Controllers\\ContentController.NOTLOGGEDIN', 'Not logged in')); _t('SilverStripe\\CMS\\Controllers\\ContentController.NOTLOGGEDIN', 'Not logged in')
// Tell the CMS to allow re-aunthentication );
$response->setStatusDescription(
_t('SilverStripe\\CMS\\Controllers\\ContentController.NOTLOGGEDIN', 'Not logged in')
);
// Tell the CMS to allow re-authentication
if (CMSSecurity::enabled()) { if (CMSSecurity::enabled()) {
$response->addHeader('X-Reauthenticate', '1'); $response->addHeader('X-Reauthenticate', '1');
} }
} }
return $response; return $response;
} }
@ -269,15 +370,15 @@ class Security extends Controller implements TemplateGlobalProvider
$messageSet = $configMessageSet; $messageSet = $configMessageSet;
} else { } else {
$messageSet = array( $messageSet = array(
'default' => _t( 'default' => _t(
'SilverStripe\\Security\\Security.NOTEPAGESECURED', 'SilverStripe\\Security\\Security.NOTEPAGESECURED',
"That page is secured. Enter your credentials below and we will send " "That page is secured. Enter your credentials below and we will send "
. "you right along." . "you right along."
), ),
'alreadyLoggedIn' => _t( 'alreadyLoggedIn' => _t(
'SilverStripe\\Security\\Security.ALREADYLOGGEDIN', 'SilverStripe\\Security\\Security.ALREADYLOGGEDIN',
"You don't have access to this page. If you have another account that " "You don't have access to this page. If you have another account that "
. "can access that page, you can log in again below.", . "can access that page, you can log in again below.",
"%s will be replaced with a link to log in." "%s will be replaced with a link to log in."
) )
); );
@ -288,7 +389,7 @@ class Security extends Controller implements TemplateGlobalProvider
$messageSet = array('default' => $messageSet); $messageSet = array('default' => $messageSet);
} }
$member = Member::currentUser(); $member = static::getCurrentUser();
// Work out the right message to show // Work out the right message to show
if ($member && $member->exists()) { if ($member && $member->exists()) {
@ -303,12 +404,8 @@ class Security extends Controller implements TemplateGlobalProvider
$message = $messageSet['default']; $message = $messageSet['default'];
} }
// Somewhat hackish way to render a login form with an error message. static::singleton()->setLoginMessage($message, ValidationResult::TYPE_WARNING);
$me = new Security(); $loginResponse = static::singleton()->login();
$form = $me->LoginForm();
$form->sessionMessage($message, ValidationResult::TYPE_WARNING);
Session::set('MemberLoginForm.force_message', 1);
$loginResponse = $me->login();
if ($loginResponse instanceof HTTPResponse) { if ($loginResponse instanceof HTTPResponse) {
return $loginResponse; return $loginResponse;
} }
@ -322,7 +419,7 @@ class Security extends Controller implements TemplateGlobalProvider
$message = $messageSet['default']; $message = $messageSet['default'];
} }
static::setLoginMessage($message, ValidationResult::TYPE_WARNING); static::singleton()->setLoginMessage($message, ValidationResult::TYPE_WARNING);
Session::set("BackURL", $_SERVER['REQUEST_URI']); Session::set("BackURL", $_SERVER['REQUEST_URI']);
@ -336,79 +433,43 @@ class Security extends Controller implements TemplateGlobalProvider
)); ));
} }
protected function init() /**
* @param null|Member $currentUser
*/
public static function setCurrentUser($currentUser = null)
{ {
parent::init(); self::$currentUser = $currentUser;
// Prevent clickjacking, see https://developer.mozilla.org/en-US/docs/HTTP/X-Frame-Options
$frameOptions = $this->config()->get('frame_options');
if ($frameOptions) {
$this->getResponse()->addHeader('X-Frame-Options', $frameOptions);
}
// Prevent search engines from indexing the login page
$robotsTag = $this->config()->get('robots_tag');
if ($robotsTag) {
$this->getResponse()->addHeader('X-Robots-Tag', $robotsTag);
}
}
public function index()
{
return $this->httpError(404); // no-op
} }
/** /**
* Get the selected authenticator for this request * @return null|Member
*
* @return string Class name of Authenticator
* @throws LogicException
*/ */
protected function getAuthenticator() public static function getCurrentUser()
{ {
$authenticator = $this->getRequest()->requestVar('AuthenticationMethod'); return self::$currentUser;
if ($authenticator && Authenticator::is_registered($authenticator)) {
return $authenticator;
} elseif ($authenticator !== '' && Authenticator::is_registered(Authenticator::get_default_authenticator())) {
return Authenticator::get_default_authenticator();
}
throw new LogicException('No valid authenticator found');
}
/**
* Get the login form to process according to the submitted data
*
* @return Form
* @throws Exception
*/
public function LoginForm()
{
$authenticator = $this->getAuthenticator();
if ($authenticator) {
return $authenticator::get_login_form($this);
}
throw new Exception('Passed invalid authentication method');
} }
/** /**
* Get the login forms for all available authentication methods * Get the login forms for all available authentication methods
* *
* @deprecated 5.0.0 Now handled by {@link static::delegateToMultipleHandlers}
*
* @return array Returns an array of available login forms (array of Form * @return array Returns an array of available login forms (array of Form
* objects). * objects).
* *
* @todo Check how to activate/deactivate authentication methods
*/ */
public function GetLoginForms() public function getLoginForms()
{ {
$forms = array(); Deprecation::notice('5.0.0', 'Now handled by delegateToMultipleHandlers');
$authenticators = Authenticator::get_authenticators(); return array_map(
foreach ($authenticators as $authenticator) { function (Authenticator $authenticator) {
$forms[] = $authenticator::get_login_form($this); return [
} $authenticator->getLoginHandler($this->Link())->loginForm()
];
return $forms; },
$this->getApplicableAuthenticators()
);
} }
@ -436,6 +497,12 @@ class Security extends Controller implements TemplateGlobalProvider
/** /**
* Log the currently logged in user out * Log the currently logged in user out
* *
* Logging out without ID-parameter in the URL, will log the user out of all applicable Authenticators.
*
* Adding an ID will only log the user out of that Authentication method.
*
* Logging out of Default will <i>always</i> completely log out the user.
*
* @param bool $redirect Redirect the user back to where they came. * @param bool $redirect Redirect the user back to where they came.
* - If it's false, the code calling logout() is * - If it's false, the code calling logout() is
* responsible for sending the user where-ever * responsible for sending the user where-ever
@ -444,14 +511,34 @@ class Security extends Controller implements TemplateGlobalProvider
*/ */
public function logout($redirect = true) public function logout($redirect = true)
{ {
$member = Member::currentUser(); $this->extend('beforeMemberLoggedOut');
if ($member) { $member = static::getCurrentUser();
$member->logOut();
if ($member) { // If we don't have a member, there's not much to log out.
/** @var array|Authenticator[] $authenticators */
$authenticators = $this->getApplicableAuthenticators(Authenticator::LOGOUT);
/** @var Authenticator[] $authenticator */
foreach ($authenticators as $name => $authenticator) {
$handler = $authenticator->getLogOutHandler(Controller::join_links($this->Link(), 'logout'));
$this->delegateToHandler($handler, $name);
}
// In the rare case, but plausible with e.g. an external IdentityStore, the user is not logged out.
if (static::getCurrentUser() !== null) {
$this->extend('failureMemberLoggedOut', $authenticator);
return $this->redirectBack();
}
$this->extend('successMemberLoggedOut', $authenticator);
// Member is successfully logged out. Write possible changes to the database.
$member->write();
} }
$this->extend('afterMemberLoggedOut');
if ($redirect && (!$this->getResponse()->isFinished())) { if ($redirect && (!$this->getResponse()->isFinished())) {
return $this->redirectBack(); return $this->redirectBack();
} }
return null; return null;
} }
@ -484,10 +571,10 @@ class Security extends Controller implements TemplateGlobalProvider
// This step is necessary in cases such as automatic redirection where a user is authenticated // This step is necessary in cases such as automatic redirection where a user is authenticated
// upon landing on an SSL secured site and is automatically logged in, or some other case // upon landing on an SSL secured site and is automatically logged in, or some other case
// where the user has permissions to continue but is not given the option. // where the user has permissions to continue but is not given the option.
if ($this->getRequest()->requestVar('BackURL') if (!$this->getLoginMessage()
&& !$this->getLoginMessage() && ($member = static::getCurrentUser())
&& ($member = Member::currentUser())
&& $member->exists() && $member->exists()
&& $this->getRequest()->requestVar('BackURL')
) { ) {
return $this->redirectBack(); return $this->redirectBack();
} }
@ -511,25 +598,24 @@ class Security extends Controller implements TemplateGlobalProvider
// Create new instance of page holder // Create new instance of page holder
/** @var Page $holderPage */ /** @var Page $holderPage */
$holderPage = new $pageClass; $holderPage = Injector::inst()->create($pageClass);
$holderPage->Title = $title; $holderPage->Title = $title;
/** @skipUpgrade */ /** @skipUpgrade */
$holderPage->URLSegment = 'Security'; $holderPage->URLSegment = 'Security';
// Disable ID-based caching of the log-in page by making it a random number // Disable ID-based caching of the log-in page by making it a random number
$holderPage->ID = -1 * rand(1, 10000000); $holderPage->ID = -1 * random_int(1, 10000000);
$controllerClass = $holderPage->getControllerName(); $controller = ModelAsController::controller_for($holderPage);
/** @var ContentController $controller */
$controller = $controllerClass::create($holderPage);
$controller->setDataModel($this->model); $controller->setDataModel($this->model);
$controller->doInit(); $controller->doInit();
return $controller; return $controller;
} }
/** /**
* Combine the given forms into a formset with a tabbed interface * Combine the given forms into a formset with a tabbed interface
* *
* @param array $forms List of LoginForm instances * @param array|Form[] $forms
* @return string * @return string
*/ */
protected function generateLoginFormSet($forms) protected function generateLoginFormSet($forms)
@ -537,6 +623,7 @@ class Security extends Controller implements TemplateGlobalProvider
$viewData = new ArrayData(array( $viewData = new ArrayData(array(
'Forms' => new ArrayList($forms), 'Forms' => new ArrayList($forms),
)); ));
return $viewData->renderWith( return $viewData->renderWith(
$this->getTemplatesFor('MultiAuthenticatorLogin') $this->getTemplatesFor('MultiAuthenticatorLogin')
); );
@ -561,6 +648,7 @@ class Security extends Controller implements TemplateGlobalProvider
if ($messageCast !== ValidationResult::CAST_HTML) { if ($messageCast !== ValidationResult::CAST_HTML) {
$message = Convert::raw2xml($message); $message = Convert::raw2xml($message);
} }
return sprintf('<p class="message %s">%s</p>', Convert::raw2att($messageType), $message); return sprintf('<p class="message %s">%s</p>', Convert::raw2att($messageType), $message);
} }
@ -571,14 +659,14 @@ class Security extends Controller implements TemplateGlobalProvider
* @param string $messageType Message type. One of ValidationResult::TYPE_* * @param string $messageType Message type. One of ValidationResult::TYPE_*
* @param string $messageCast Message cast. One of ValidationResult::CAST_* * @param string $messageCast Message cast. One of ValidationResult::CAST_*
*/ */
public static function setLoginMessage( public function setLoginMessage(
$message, $message,
$messageType = ValidationResult::TYPE_WARNING, $messageType = ValidationResult::TYPE_WARNING,
$messageCast = ValidationResult::CAST_TEXT $messageCast = ValidationResult::CAST_TEXT
) { ) {
Session::set("Security.Message.message", $message); Session::set('Security.Message.message', $message);
Session::set("Security.Message.type", $messageType); Session::set('Security.Message.type', $messageType);
Session::set("Security.Message.cast", $messageCast); Session::set('Security.Message.cast', $messageCast);
} }
/** /**
@ -586,7 +674,7 @@ class Security extends Controller implements TemplateGlobalProvider
*/ */
public static function clearLoginMessage() public static function clearLoginMessage()
{ {
Session::clear("Security.Message"); Session::clear('Security.Message');
} }
@ -596,31 +684,151 @@ class Security extends Controller implements TemplateGlobalProvider
* For multiple authenticators, Security_MultiAuthenticatorLogin is used. * For multiple authenticators, Security_MultiAuthenticatorLogin is used.
* See getTemplatesFor and getIncludeTemplate for how to override template logic * See getTemplatesFor and getIncludeTemplate for how to override template logic
* *
* @return string|HTTPResponse Returns the "login" page as HTML code. * @param null|HTTPRequest $request
* @param int $service
* @return HTTPResponse|string Returns the "login" page as HTML code.
* @throws HTTPResponse_Exception
*/ */
public function login() public function login($request = null, $service = Authenticator::LOGIN)
{ {
// Check pre-login process // Check pre-login process
if ($response = $this->preLogin()) { if ($response = $this->preLogin()) {
return $response; return $response;
} }
$authName = null;
// Get response handler if (!$request) {
$controller = $this->getResponseController(_t('SilverStripe\\Security\\Security.LOGIN', 'Log in')); $request = $this->getRequest();
}
if ($request && $request->param('ID')) {
$authName = $request->param('ID');
}
$link = $this->Link('login');
// Delegate to a single handler - Security/login/<authname>/...
if ($authName && $this->hasAuthenticator($authName)) {
if ($request) {
$request->shift();
}
$authenticator = $this->getAuthenticator($authName);
if (!$authenticator->supportedServices() & $service) {
throw new HTTPResponse_Exception('Invalid Authenticator "' . $authName . '" for login action', 418);
}
$handlers = [$authName => $authenticator];
} else {
// Delegate to all of them, building a tabbed view - Security/login
$handlers = $this->getApplicableAuthenticators($service);
}
array_walk(
$handlers,
function (Authenticator &$auth, $name) use ($link) {
$auth = $auth->getLoginHandler(Controller::join_links($link, $name));
}
);
return $this->delegateToMultipleHandlers(
$handlers,
_t('Security.LOGIN', 'Log in'),
$this->getTemplatesFor('login')
);
}
/**
* Delegate to an number of handlers, extracting their forms and rendering a tabbed form-set.
* This is used to built the log-in page where there are multiple authenticators active.
*
* If a single handler is passed, delegateToHandler() will be called instead
*
* @param array|RequestHandler[] $handlers
* @param string $title The title of the form
* @param array $templates
* @return array|HTTPResponse|RequestHandler|DBHTMLText|string
*/
protected function delegateToMultipleHandlers(array $handlers, $title, array $templates)
{
// Simpler case for a single authenticator
if (count($handlers) === 1) {
return $this->delegateToHandler(array_values($handlers)[0], $title, $templates);
}
// Process each of the handlers
$results = array_map(
function (RequestHandler $handler) {
return $handler->handleRequest($this->getRequest(), DataModel::inst());
},
$handlers
);
// Aggregate all their forms, assuming they all return
$forms = [];
foreach ($results as $authName => $singleResult) {
// The result *must* be an array with a Form key
if (!is_array($singleResult) || !isset($singleResult['Form'])) {
user_error('Authenticator "' . $authName . '" doesn\'t support a tabbed login', E_USER_WARNING);
continue;
}
$forms[] = $singleResult['Form'];
}
if (!$forms) {
throw new \LogicException('No authenticators found compatible with a tabbed login');
}
return $this->renderWrappedController(
$title,
[
'Form' => $this->generateLoginFormSet($forms),
],
$templates
);
}
/**
* Delegate to another RequestHandler, rendering any fragment arrays into an appropriate.
* controller.
*
* @param RequestHandler $handler
* @param string $title The title of the form
* @param array $templates
* @return array|HTTPResponse|RequestHandler|DBHTMLText|string
*/
protected function delegateToHandler(RequestHandler $handler, $title, array $templates = [])
{
$result = $handler->handleRequest($this->getRequest(), DataModel::inst());
// Return the customised controller - used to render in a Form
// Post requests are expected to be login posts, so they'll be handled downstairs
if (is_array($result)) {
$result = $this->renderWrappedController($title, $result, $templates);
}
return $result;
}
/**
* Render the given fragments into a security page controller with the given title.
* @param string $title string The title to give the security page
* @param array $fragments A map of objects to render into the page, e.g. "Form"
* @param array $templates An array of templates to use for the render
* @return HTTPResponse|DBHTMLText
*/
protected function renderWrappedController($title, array $fragments, array $templates)
{
$controller = $this->getResponseController($title);
// if the controller calls Director::redirect(), this will break early // if the controller calls Director::redirect(), this will break early
if (($response = $controller->getResponse()) && $response->isFinished()) { if (($response = $controller->getResponse()) && $response->isFinished()) {
return $response; return $response;
} }
$forms = $this->GetLoginForms();
if (!count($forms)) {
user_error(
'No login-forms found, please use Authenticator::register_authenticator() to add one',
E_USER_ERROR
);
}
// Handle any form messages from validation, etc. // Handle any form messages from validation, etc.
$messageType = ''; $messageType = '';
$message = $this->getLoginMessage($messageType); $message = $this->getLoginMessage($messageType);
@ -628,32 +836,22 @@ class Security extends Controller implements TemplateGlobalProvider
// We've displayed the message in the form output, so reset it for the next run. // We've displayed the message in the form output, so reset it for the next run.
static::clearLoginMessage(); static::clearLoginMessage();
// only display tabs when more than one authenticator is provided if ($message) {
// to save bandwidth and reduce the amount of custom styling needed $messageResult = [
if (count($forms) > 1) { 'Content' => DBField::create_field('HTMLFragment', $message),
$content = $this->generateLoginFormSet($forms); 'Message' => DBField::create_field('HTMLFragment', $message),
} else { 'MessageType' => $messageType
$content = $forms[0]->forTemplate(); ];
$fragments = array_merge($fragments, $messageResult);
} }
// Finally, customise the controller to add any form messages and the form. return $controller->customise($fragments)->renderWith($templates);
$customisedController = $controller->customise(array(
"Content" => DBField::create_field('HTMLFragment', $message),
"Message" => DBField::create_field('HTMLFragment', $message),
"MessageType" => $messageType,
"Form" => $content,
));
// Return the customised controller
return $customisedController->renderWith(
$this->getTemplatesFor('login')
);
} }
public function basicauthlogin() public function basicauthlogin()
{ {
$member = BasicAuth::requireLogin("SilverStripe login", 'ADMIN'); $member = BasicAuth::requireLogin($this->getRequest(), 'SilverStripe login', 'ADMIN');
$member->logIn(); static::setCurrentUser($member);
} }
/** /**
@ -663,113 +861,20 @@ class Security extends Controller implements TemplateGlobalProvider
*/ */
public function lostpassword() public function lostpassword()
{ {
$controller = $this->getResponseController(_t('SilverStripe\\Security\\Security.LOSTPASSWORDHEADER', 'Lost Password')); $handlers = [];
$authenticators = $this->getApplicableAuthenticators(Authenticator::RESET_PASSWORD);
// if the controller calls Director::redirect(), this will break early /** @var Authenticator $authenticator */
if (($response = $controller->getResponse()) && $response->isFinished()) { foreach ($authenticators as $authenticator) {
return $response; $handlers[] = $authenticator->getLostPasswordHandler(
Controller::join_links($this->Link(), 'lostpassword')
);
} }
$message = _t( return $this->delegateToMultipleHandlers(
'SilverStripe\\Security\\Security.NOTERESETPASSWORD', $handlers,
'Enter your e-mail address and we will send you a link with which you can reset your password' _t('SilverStripe\\Security\\Security.LOSTPASSWORDHEADER', 'Lost Password'),
$this->getTemplatesFor('lostpassword')
); );
/** @var ViewableData_Customised $customisedController */
$customisedController = $controller->customise(array(
'Content' => DBField::create_field('HTMLFragment', "<p>$message</p>"),
'Form' => $this->LostPasswordForm(),
));
//Controller::$currentController = $controller;
$result = $customisedController->renderWith($this->getTemplatesFor('lostpassword'));
return $result;
}
/**
* Factory method for the lost password form
*
* @skipUpgrade
* @return Form Returns the lost password form
*/
public function LostPasswordForm()
{
return MemberLoginForm::create(
$this,
Config::inst()->get('Authenticator', 'default_authenticator'),
'LostPasswordForm',
new FieldList(
new EmailField('Email', _t('SilverStripe\\Security\\Member.EMAIL', 'Email'))
),
new FieldList(
new FormAction(
'forgotPassword',
_t(__CLASS__.'.BUTTONSEND', 'Send me the password reset link')
)
),
false
);
}
/**
* Show the "password sent" page, after a user has requested
* to reset their password.
*
* @param HTTPRequest $request The HTTPRequest for this action.
* @return string Returns the "password sent" page as HTML code.
*/
public function passwordsent($request)
{
$controller = $this->getResponseController(_t('SilverStripe\\Security\\Security.LOSTPASSWORDHEADER', 'Lost Password'));
// if the controller calls Director::redirect(), this will break early
if (($response = $controller->getResponse()) && $response->isFinished()) {
return $response;
}
$email = Convert::raw2xml(rawurldecode($request->param('ID')) . '.' . $request->getExtension());
$message = _t(
'SilverStripe\\Security\\Security.PASSWORDSENTTEXT',
"Thank you! A reset link has been sent to '{email}', provided an account exists for this email"
. " address.",
array('email' => Convert::raw2xml($email))
);
$customisedController = $controller->customise(array(
'Title' => _t(
'SilverStripe\\Security\\Security.PASSWORDSENTHEADER',
"Password reset link sent to '{email}'",
array('email' => $email)
),
'Content' => DBField::create_field('HTMLFragment', "<p>$message</p>"),
'Email' => $email
));
//Controller::$currentController = $controller;
return $customisedController->renderWith($this->getTemplatesFor('passwordsent'));
}
/**
* Create a link to the password reset form.
*
* GET parameters used:
* - m: member ID
* - t: plaintext token
*
* @param Member $member Member object associated with this link.
* @param string $autologinToken The auto login token.
* @return string
*/
public static function getPasswordResetLink($member, $autologinToken)
{
$autologinToken = urldecode($autologinToken);
$selfControllerClass = __CLASS__;
/** @var static $selfController */
$selfController = new $selfControllerClass();
return $selfController->Link('changepassword') . "?m={$member->ID}&t=$autologinToken";
} }
/** /**
@ -786,88 +891,36 @@ class Security extends Controller implements TemplateGlobalProvider
*/ */
public function changepassword() public function changepassword()
{ {
$controller = $this->getResponseController(_t('SilverStripe\\Security\\Security.CHANGEPASSWORDHEADER', 'Change your password')); /** @var array|Authenticator[] $authenticators */
$authenticators = $this->getApplicableAuthenticators(Authenticator::CHANGE_PASSWORD);
// if the controller calls Director::redirect(), this will break early $handlers = [];
if (($response = $controller->getResponse()) && $response->isFinished()) { foreach ($authenticators as $authenticator) {
return $response; $handlers[] = $authenticator->getChangePasswordHandler($this->Link('changepassword'));
} }
// Extract the member from the URL. return $this->delegateToMultipleHandlers(
/** @var Member $member */ $handlers,
$member = null; _t('SilverStripe\\Security\\Security.CHANGEPASSWORDHEADER', 'Change your password'),
if (isset($_REQUEST['m'])) { $this->getTemplatesFor('changepassword')
$member = Member::get()->filter('ID', (int)$_REQUEST['m'])->first(); );
}
// Check whether we are merely changin password, or resetting.
if (isset($_REQUEST['t']) && $member && $member->validateAutoLoginToken($_REQUEST['t'])) {
// On first valid password reset request redirect to the same URL without hash to avoid referrer leakage.
// if there is a current member, they should be logged out
if ($curMember = Member::currentUser()) {
$curMember->logOut();
}
// Store the hash for the change password form. Will be unset after reload within the ChangePasswordForm.
Session::set('AutoLoginHash', $member->encryptWithUserSettings($_REQUEST['t']));
return $this->redirect($this->Link('changepassword'));
} elseif (Session::get('AutoLoginHash')) {
// Subsequent request after the "first load with hash" (see previous if clause).
$customisedController = $controller->customise(array(
'Content' => DBField::create_field(
'HTMLFragment',
'<p>' . _t('SilverStripe\\Security\\Security.ENTERNEWPASSWORD', 'Please enter a new password.') . '</p>'
),
'Form' => $this->ChangePasswordForm(),
));
} elseif (Member::currentUser()) {
// Logged in user requested a password change form.
$customisedController = $controller->customise(array(
'Content' => DBField::create_field(
'HTMLFragment',
'<p>' . _t('SilverStripe\\Security\\Security.CHANGEPASSWORDBELOW', 'You can change your password below.') . '</p>'
),
'Form' => $this->ChangePasswordForm()));
} else {
// Show friendly message if it seems like the user arrived here via password reset feature.
if (isset($_REQUEST['m']) || isset($_REQUEST['t'])) {
$customisedController = $controller->customise(
array('Content' => DBField::create_field(
'HTMLFragment',
_t(
'SilverStripe\\Security\\Security.NOTERESETLINKINVALID',
'<p>The password reset link is invalid or expired.</p>'
. '<p>You can request a new one <a href="{link1}">here</a> or change your password after'
. ' you <a href="{link2}">logged in</a>.</p>',
[
'link1' => $this->Link('lostpassword'),
'link2' => $this->Link('login')
]
)
))
);
} else {
return self::permissionFailure(
$this,
_t('SilverStripe\\Security\\Security.ERRORPASSWORDPERMISSION', 'You must be logged in in order to change your password!')
);
}
}
return $customisedController->renderWith($this->getTemplatesFor('changepassword'));
} }
/** /**
* Factory method for the lost password form * Create a link to the password reset form.
* *
* @skipUpgrade * GET parameters used:
* @return ChangePasswordForm Returns the lost password form * - m: member ID
* - t: plaintext token
*
* @param Member $member Member object associated with this link.
* @param string $autologinToken The auto login token.
* @return string
*/ */
public function ChangePasswordForm() public static function getPasswordResetLink($member, $autologinToken)
{ {
return ChangePasswordForm::create($this, 'ChangePasswordForm'); $autologinToken = urldecode($autologinToken);
return static::singleton()->Link('changepassword') . "?m={$member->ID}&t=$autologinToken";
} }
/** /**
@ -880,6 +933,7 @@ class Security extends Controller implements TemplateGlobalProvider
public function getTemplatesFor($action) public function getTemplatesFor($action)
{ {
$templates = SSViewer::get_templates_by_class(static::class, "_{$action}", __CLASS__); $templates = SSViewer::get_templates_by_class(static::class, "_{$action}", __CLASS__);
return array_merge( return array_merge(
$templates, $templates,
[ [
@ -906,12 +960,7 @@ class Security extends Controller implements TemplateGlobalProvider
*/ */
public static function findAnAdministrator() public static function findAnAdministrator()
{ {
// coupling to subsites module static::singleton()->extend('beforeFindAdministrator');
$origSubsite = null;
if (is_callable('Subsite::changeSubsite')) {
$origSubsite = Subsite::currentSubsiteID();
Subsite::changeSubsite(0);
}
/** @var Member $member */ /** @var Member $member */
$member = null; $member = null;
@ -919,19 +968,13 @@ class Security extends Controller implements TemplateGlobalProvider
// find a group with ADMIN permission // find a group with ADMIN permission
$adminGroup = Permission::get_groups_by_permission('ADMIN')->first(); $adminGroup = Permission::get_groups_by_permission('ADMIN')->first();
if (is_callable('Subsite::changeSubsite')) {
Subsite::changeSubsite($origSubsite);
}
if ($adminGroup) {
$member = $adminGroup->Members()->First();
}
if (!$adminGroup) { if (!$adminGroup) {
Group::singleton()->requireDefaultRecords(); Group::singleton()->requireDefaultRecords();
$adminGroup = Permission::get_groups_by_permission('ADMIN')->first(); $adminGroup = Permission::get_groups_by_permission('ADMIN')->first();
} }
$member = $adminGroup->Members()->First();
if (!$member) { if (!$member) {
Member::singleton()->requireDefaultRecords(); Member::singleton()->requireDefaultRecords();
$member = Permission::get_members_by_permission('ADMIN')->first(); $member = Permission::get_members_by_permission('ADMIN')->first();
@ -953,6 +996,8 @@ class Security extends Controller implements TemplateGlobalProvider
->add($member); ->add($member);
} }
static::singleton()->extend('afterFindAdministrator');
return $member; return $member;
} }
@ -987,6 +1032,7 @@ class Security extends Controller implements TemplateGlobalProvider
self::$default_username = $username; self::$default_username = $username;
self::$default_password = $password; self::$default_password = $password;
return true; return true;
} }
@ -1075,8 +1121,8 @@ class Security extends Controller implements TemplateGlobalProvider
$salt = ($salt) ? $salt : $e->salt($password); $salt = ($salt) ? $salt : $e->salt($password);
return array( return array(
'password' => $e->encrypt($password, $salt, $member), 'password' => $e->encrypt($password, $salt, $member),
'salt' => $salt, 'salt' => $salt,
'algorithm' => $algorithm, 'algorithm' => $algorithm,
'encryptor' => $e 'encryptor' => $e
); );
@ -1137,6 +1183,25 @@ class Security extends Controller implements TemplateGlobalProvider
return true; return true;
} }
/**
* Resets the database_is_ready cache
*/
public static function clear_database_is_ready()
{
self::$database_is_ready = null;
self::$force_database_is_ready = null;
}
/**
* For the database_is_ready call to return a certain value - used for testing
*
* @param bool $isReady
*/
public static function force_database_is_ready($isReady)
{
self::$force_database_is_ready = $isReady;
}
/** /**
* @config * @config
* @var string Set the default login dest * @var string Set the default login dest
@ -1151,7 +1216,7 @@ class Security extends Controller implements TemplateGlobalProvider
/** /**
* Set to true to ignore access to disallowed actions, rather than returning permission failure * Set to true to ignore access to disallowed actions, rather than returning permission failure
* Note that this is just a flag that other code needs to check with Security::ignore_disallowed_actions() * Note that this is just a flag that other code needs to check with Security::ignore_disallowed_actions()
* @param $flag True or false * @param bool $flag True or false
*/ */
public static function set_ignore_disallowed_actions($flag) public static function set_ignore_disallowed_actions($flag)
{ {
@ -1208,9 +1273,11 @@ class Security extends Controller implements TemplateGlobalProvider
public static function get_template_global_variables() public static function get_template_global_variables()
{ {
return array( return array(
"LoginURL" => "login_url", "LoginURL" => "login_url",
"LogoutURL" => "logout_url", "LogoutURL" => "logout_url",
"LostPasswordURL" => "lost_password_url", "LostPasswordURL" => "lost_password_url",
"CurrentMember" => "getCurrentUser",
"currentUser" => "getCurrentUser"
); );
} }
} }

View File

@ -2,11 +2,11 @@
namespace SilverStripe\Security; namespace SilverStripe\Security;
use SilverStripe\Control\Controller;
use SilverStripe\Control\HTTPRequest; use SilverStripe\Control\HTTPRequest;
use SilverStripe\Control\Session;
use SilverStripe\Core\Config\Configurable; use SilverStripe\Core\Config\Configurable;
use SilverStripe\Core\Injector\Injectable; use SilverStripe\Core\Injector\Injectable;
use SilverStripe\Control\Session;
use SilverStripe\Control\Controller;
use SilverStripe\Forms\FieldList; use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\HiddenField; use SilverStripe\Forms\HiddenField;
use SilverStripe\View\TemplateGlobalProvider; use SilverStripe\View\TemplateGlobalProvider;
@ -61,11 +61,11 @@ class SecurityToken implements TemplateGlobalProvider
protected $name = null; protected $name = null;
/** /**
* @param $name * @param string $name
*/ */
public function __construct($name = null) public function __construct($name = null)
{ {
$this->name = ($name) ? $name : self::get_default_name(); $this->name = $name ?: self::get_default_name();
} }
/** /**

View File

@ -312,7 +312,7 @@ class ViewableData implements IteratorAggregate
*/ */
public function castingHelper($field) public function castingHelper($field)
{ {
$specs = $this->config()->get('casting'); $specs = static::config()->get('casting');
if (isset($specs[$field])) { if (isset($specs[$field])) {
return $specs[$field]; return $specs[$field];
} }

View File

@ -6,7 +6,7 @@ Feature: Log in
Scenario: Bad login Scenario: Bad login
Given I log in with "bad@example.com" and "badpassword" Given I log in with "bad@example.com" and "badpassword"
Then I will see a "error" log-in message Then I should see "The provided details don't seem to be correct"
Scenario: Valid login Scenario: Valid login
Given I am logged in with "ADMIN" permissions Given I am logged in with "ADMIN" permissions

View File

@ -23,6 +23,7 @@ use SilverStripe\Dev\Deprecation;
use SilverStripe\Dev\FunctionalTest; use SilverStripe\Dev\FunctionalTest;
use SilverStripe\ORM\DataModel; use SilverStripe\ORM\DataModel;
use SilverStripe\Security\Member; use SilverStripe\Security\Member;
use SilverStripe\Security\Security;
use SilverStripe\View\SSViewer; use SilverStripe\View\SSViewer;
class ControllerTest extends FunctionalTest class ControllerTest extends FunctionalTest
@ -203,7 +204,7 @@ class ControllerTest extends FunctionalTest
'if action is not a method but rather a template discovered by naming convention' 'if action is not a method but rather a template discovered by naming convention'
); );
$this->session()->inst_set('loggedInAs', $adminUser->ID); Security::setCurrentUser($adminUser);
$response = $this->get("AccessSecuredController/templateaction"); $response = $this->get("AccessSecuredController/templateaction");
$this->assertEquals( $this->assertEquals(
200, 200,
@ -211,8 +212,8 @@ class ControllerTest extends FunctionalTest
'Access granted for logged in admin on action with $allowed_actions on defining controller, ' . 'Access granted for logged in admin on action with $allowed_actions on defining controller, ' .
'if action is not a method but rather a template discovered by naming convention' 'if action is not a method but rather a template discovered by naming convention'
); );
$this->session()->inst_set('loggedInAs', null);
Security::setCurrentUser(null);
$response = $this->get("AccessSecuredController/adminonly"); $response = $this->get("AccessSecuredController/adminonly");
$this->assertEquals( $this->assertEquals(
403, 403,
@ -236,15 +237,15 @@ class ControllerTest extends FunctionalTest
"Access denied to protected method even if its listed in allowed_actions" "Access denied to protected method even if its listed in allowed_actions"
); );
$this->session()->inst_set('loggedInAs', $adminUser->ID); Security::setCurrentUser($adminUser);
$response = $this->get("AccessSecuredController/adminonly"); $response = $this->get("AccessSecuredController/adminonly");
$this->assertEquals( $this->assertEquals(
200, 200,
$response->getStatusCode(), $response->getStatusCode(),
"Permission codes are respected when set in \$allowed_actions" "Permission codes are respected when set in \$allowed_actions"
); );
$this->session()->inst_set('loggedInAs', null);
Security::setCurrentUser(null);
$response = $this->get('AccessBaseController/extensionmethod1'); $response = $this->get('AccessBaseController/extensionmethod1');
$this->assertEquals( $this->assertEquals(
200, 200,
@ -285,7 +286,7 @@ class ControllerTest extends FunctionalTest
"and doesn't satisfy checks" "and doesn't satisfy checks"
); );
$this->session()->inst_set('loggedInAs', $adminUser->ID); Security::setCurrentUser($adminUser);
$response = $this->get('IndexSecuredController/'); $response = $this->get('IndexSecuredController/');
$this->assertEquals( $this->assertEquals(
200, 200,
@ -293,7 +294,7 @@ class ControllerTest extends FunctionalTest
"Access granted when index action is limited through allowed_actions, " . "Access granted when index action is limited through allowed_actions, " .
"and does satisfy checks" "and does satisfy checks"
); );
$this->session()->inst_set('loggedInAs', null); Security::setCurrentUser(null);
} }
public function testWildcardAllowedActions() public function testWildcardAllowedActions()

View File

@ -10,11 +10,10 @@ use SilverStripe\Forms\Tests\GridField\GridFieldTest\Team;
use SilverStripe\ORM\ArrayList; use SilverStripe\ORM\ArrayList;
use SilverStripe\ORM\DataList; use SilverStripe\ORM\DataList;
use SilverStripe\ORM\ValidationException; use SilverStripe\ORM\ValidationException;
use SilverStripe\Security\Member; use SilverStripe\Security\Security;
use SilverStripe\Security\SecurityToken; use SilverStripe\Security\SecurityToken;
use SilverStripe\Dev\CSSContentParser; use SilverStripe\Dev\CSSContentParser;
use SilverStripe\Dev\SapphireTest; use SilverStripe\Dev\SapphireTest;
use SilverStripe\Control\Controller;
use SilverStripe\Control\HTTPRequest; use SilverStripe\Control\HTTPRequest;
use SilverStripe\Control\Session; use SilverStripe\Control\Session;
use SilverStripe\Forms\FieldList; use SilverStripe\Forms\FieldList;
@ -67,8 +66,8 @@ class GridFieldDeleteActionTest extends SapphireTest
public function testDontShowDeleteButtons() public function testDontShowDeleteButtons()
{ {
if (Member::currentUser()) { if (Security::getCurrentUser()) {
Member::currentUser()->logOut(); Security::setCurrentUser(null);
} }
$content = new CSSContentParser($this->gridField->FieldHolder()); $content = new CSSContentParser($this->gridField->FieldHolder());
// Check that there are content // Check that there are content
@ -116,8 +115,8 @@ class GridFieldDeleteActionTest extends SapphireTest
public function testDeleteActionWithoutCorrectPermission() public function testDeleteActionWithoutCorrectPermission()
{ {
if (Member::currentUser()) { if (Security::getCurrentUser()) {
Member::currentUser()->logOut(); Security::setCurrentUser(null);
} }
$this->setExpectedException(ValidationException::class); $this->setExpectedException(ValidationException::class);

View File

@ -17,6 +17,7 @@ use SilverStripe\Forms\Form;
use SilverStripe\Forms\GridField\GridFieldConfig; use SilverStripe\Forms\GridField\GridFieldConfig;
use SilverStripe\Forms\GridField\GridFieldEditButton; use SilverStripe\Forms\GridField\GridFieldEditButton;
use SilverStripe\Forms\GridField\GridField; use SilverStripe\Forms\GridField\GridField;
use SilverStripe\Security\Security;
class GridFieldEditButtonTest extends SapphireTest class GridFieldEditButtonTest extends SapphireTest
{ {
@ -62,8 +63,8 @@ class GridFieldEditButtonTest extends SapphireTest
public function testShowEditLinks() public function testShowEditLinks()
{ {
if (Member::currentUser()) { if (Security::getCurrentUser()) {
Member::currentUser()->logOut(); Security::setCurrentUser(null);
} }
$content = new CSSContentParser($this->gridField->FieldHolder()); $content = new CSSContentParser($this->gridField->FieldHolder());

View File

@ -15,7 +15,7 @@ use SilverStripe\Security\Tests\BasicAuthTest\ControllerSecuredWithPermission;
class BasicAuthTest extends FunctionalTest class BasicAuthTest extends FunctionalTest
{ {
static $original_unique_identifier_field; protected static $original_unique_identifier_field;
protected static $fixture_file = 'BasicAuthTest.yml'; protected static $fixture_file = 'BasicAuthTest.yml';
@ -30,7 +30,7 @@ class BasicAuthTest extends FunctionalTest
// Fixtures assume Email is the field used to identify the log in identity // Fixtures assume Email is the field used to identify the log in identity
Member::config()->unique_identifier_field = 'Email'; Member::config()->unique_identifier_field = 'Email';
Security::$force_database_is_ready = true; // Prevents Member test subclasses breaking ready test Security::force_database_is_ready(true); // Prevents Member test subclasses breaking ready test
Member::config()->lock_out_after_incorrect_logins = 10; Member::config()->lock_out_after_incorrect_logins = 10;
} }
@ -42,7 +42,7 @@ class BasicAuthTest extends FunctionalTest
unset($_SERVER['PHP_AUTH_USER']); unset($_SERVER['PHP_AUTH_USER']);
unset($_SERVER['PHP_AUTH_PW']); unset($_SERVER['PHP_AUTH_PW']);
$response = Director::test('BasicAuthTest_ControllerSecuredWithPermission'); $response = Director::test('BasicAuthTest_ControllerSecuredWithPermission', null, $_SESSION, null, null, $_SERVER);
$this->assertEquals(401, $response->getStatusCode()); $this->assertEquals(401, $response->getStatusCode());
$_SERVER['PHP_AUTH_USER'] = $origUser; $_SERVER['PHP_AUTH_USER'] = $origUser;
@ -56,13 +56,13 @@ class BasicAuthTest extends FunctionalTest
unset($_SERVER['PHP_AUTH_USER']); unset($_SERVER['PHP_AUTH_USER']);
unset($_SERVER['PHP_AUTH_PW']); unset($_SERVER['PHP_AUTH_PW']);
$response = Director::test('BasicAuthTest_ControllerSecuredWithPermission'); $response = Director::test('BasicAuthTest_ControllerSecuredWithPermission', null, $_SESSION, null, null, $_SERVER);
$this->assertFalse(BasicAuthTest\ControllerSecuredWithPermission::$index_called); $this->assertFalse(BasicAuthTest\ControllerSecuredWithPermission::$index_called);
$this->assertFalse(BasicAuthTest\ControllerSecuredWithPermission::$post_init_called); $this->assertFalse(BasicAuthTest\ControllerSecuredWithPermission::$post_init_called);
$_SERVER['PHP_AUTH_USER'] = 'user-in-mygroup@test.com'; $_SERVER['PHP_AUTH_USER'] = 'user-in-mygroup@test.com';
$_SERVER['PHP_AUTH_PW'] = 'test'; $_SERVER['PHP_AUTH_PW'] = 'test';
$response = Director::test('BasicAuthTest_ControllerSecuredWithPermission'); $response = Director::test('BasicAuthTest_ControllerSecuredWithPermission', null, $_SESSION, null, null, $_SERVER);
$this->assertTrue(BasicAuthTest\ControllerSecuredWithPermission::$index_called); $this->assertTrue(BasicAuthTest\ControllerSecuredWithPermission::$index_called);
$this->assertTrue(BasicAuthTest\ControllerSecuredWithPermission::$post_init_called); $this->assertTrue(BasicAuthTest\ControllerSecuredWithPermission::$post_init_called);
@ -77,17 +77,17 @@ class BasicAuthTest extends FunctionalTest
$_SERVER['PHP_AUTH_USER'] = 'user-in-mygroup@test.com'; $_SERVER['PHP_AUTH_USER'] = 'user-in-mygroup@test.com';
$_SERVER['PHP_AUTH_PW'] = 'wrongpassword'; $_SERVER['PHP_AUTH_PW'] = 'wrongpassword';
$response = Director::test('BasicAuthTest_ControllerSecuredWithPermission'); $response = Director::test('BasicAuthTest_ControllerSecuredWithPermission', null, $_SESSION, null, null, $_SERVER);
$this->assertEquals(401, $response->getStatusCode(), 'Invalid users dont have access'); $this->assertEquals(401, $response->getStatusCode(), 'Invalid users dont have access');
$_SERVER['PHP_AUTH_USER'] = 'user-without-groups@test.com'; $_SERVER['PHP_AUTH_USER'] = 'user-without-groups@test.com';
$_SERVER['PHP_AUTH_PW'] = 'test'; $_SERVER['PHP_AUTH_PW'] = 'test';
$response = Director::test('BasicAuthTest_ControllerSecuredWithPermission'); $response = Director::test('BasicAuthTest_ControllerSecuredWithPermission', null, $_SESSION, null, null, $_SERVER);
$this->assertEquals(401, $response->getStatusCode(), 'Valid user without required permission has no access'); $this->assertEquals(401, $response->getStatusCode(), 'Valid user without required permission has no access');
$_SERVER['PHP_AUTH_USER'] = 'user-in-mygroup@test.com'; $_SERVER['PHP_AUTH_USER'] = 'user-in-mygroup@test.com';
$_SERVER['PHP_AUTH_PW'] = 'test'; $_SERVER['PHP_AUTH_PW'] = 'test';
$response = Director::test('BasicAuthTest_ControllerSecuredWithPermission'); $response = Director::test('BasicAuthTest_ControllerSecuredWithPermission', null, $_SESSION, null, null, $_SERVER);
$this->assertEquals(200, $response->getStatusCode(), 'Valid user with required permission has access'); $this->assertEquals(200, $response->getStatusCode(), 'Valid user with required permission has access');
$_SERVER['PHP_AUTH_USER'] = $origUser; $_SERVER['PHP_AUTH_USER'] = $origUser;
@ -101,17 +101,17 @@ class BasicAuthTest extends FunctionalTest
$_SERVER['PHP_AUTH_USER'] = 'user-without-groups@test.com'; $_SERVER['PHP_AUTH_USER'] = 'user-without-groups@test.com';
$_SERVER['PHP_AUTH_PW'] = 'wrongpassword'; $_SERVER['PHP_AUTH_PW'] = 'wrongpassword';
$response = Director::test('BasicAuthTest_ControllerSecuredWithoutPermission'); $response = Director::test('BasicAuthTest_ControllerSecuredWithoutPermission', null, $_SESSION, null, null, $_SERVER);
$this->assertEquals(401, $response->getStatusCode(), 'Invalid users dont have access'); $this->assertEquals(401, $response->getStatusCode(), 'Invalid users dont have access');
$_SERVER['PHP_AUTH_USER'] = 'user-without-groups@test.com'; $_SERVER['PHP_AUTH_USER'] = 'user-without-groups@test.com';
$_SERVER['PHP_AUTH_PW'] = 'test'; $_SERVER['PHP_AUTH_PW'] = 'test';
$response = Director::test('BasicAuthTest_ControllerSecuredWithoutPermission'); $response = Director::test('BasicAuthTest_ControllerSecuredWithoutPermission', null, $_SESSION, null, null, $_SERVER);
$this->assertEquals(200, $response->getStatusCode(), 'All valid users have access'); $this->assertEquals(200, $response->getStatusCode(), 'All valid users have access');
$_SERVER['PHP_AUTH_USER'] = 'user-in-mygroup@test.com'; $_SERVER['PHP_AUTH_USER'] = 'user-in-mygroup@test.com';
$_SERVER['PHP_AUTH_PW'] = 'test'; $_SERVER['PHP_AUTH_PW'] = 'test';
$response = Director::test('BasicAuthTest_ControllerSecuredWithoutPermission'); $response = Director::test('BasicAuthTest_ControllerSecuredWithoutPermission', null, $_SESSION, null, null, $_SERVER);
$this->assertEquals(200, $response->getStatusCode(), 'All valid users have access'); $this->assertEquals(200, $response->getStatusCode(), 'All valid users have access');
$_SERVER['PHP_AUTH_USER'] = $origUser; $_SERVER['PHP_AUTH_USER'] = $origUser;
@ -127,19 +127,19 @@ class BasicAuthTest extends FunctionalTest
// First failed attempt // First failed attempt
$_SERVER['PHP_AUTH_USER'] = 'failedlogin@test.com'; $_SERVER['PHP_AUTH_USER'] = 'failedlogin@test.com';
$_SERVER['PHP_AUTH_PW'] = 'test'; $_SERVER['PHP_AUTH_PW'] = 'test';
$response = Director::test('BasicAuthTest_ControllerSecuredWithoutPermission'); $response = Director::test('BasicAuthTest_ControllerSecuredWithoutPermission', null, $_SESSION, null, null, $_SERVER);
$check = Member::get()->filter('Email', 'failedlogin@test.com')->first(); $check = Member::get()->filter('Email', 'failedlogin@test.com')->first();
$this->assertEquals(1, $check->FailedLoginCount); $this->assertEquals(1, $check->FailedLoginCount);
// Second failed attempt // Second failed attempt
$_SERVER['PHP_AUTH_PW'] = 'testwrong'; $_SERVER['PHP_AUTH_PW'] = 'testwrong';
$response = Director::test('BasicAuthTest_ControllerSecuredWithoutPermission'); $response = Director::test('BasicAuthTest_ControllerSecuredWithoutPermission', null, $_SESSION, null, null, $_SERVER);
$check = Member::get()->filter('Email', 'failedlogin@test.com')->first(); $check = Member::get()->filter('Email', 'failedlogin@test.com')->first();
$this->assertEquals(2, $check->FailedLoginCount); $this->assertEquals(2, $check->FailedLoginCount);
// successful basic auth should reset failed login count // successful basic auth should reset failed login count
$_SERVER['PHP_AUTH_PW'] = 'Password'; $_SERVER['PHP_AUTH_PW'] = 'Password';
$response = Director::test('BasicAuthTest_ControllerSecuredWithoutPermission'); $response = Director::test('BasicAuthTest_ControllerSecuredWithoutPermission', null, $_SESSION, null, null, $_SERVER);
$check = Member::get()->filter('Email', 'failedlogin@test.com')->first(); $check = Member::get()->filter('Email', 'failedlogin@test.com')->first();
$this->assertEquals(0, $check->FailedLoginCount); $this->assertEquals(0, $check->FailedLoginCount);
} }

View File

@ -9,6 +9,7 @@ use SilverStripe\Security\InheritedPermissions;
use SilverStripe\Security\InheritedPermissionsExtension; use SilverStripe\Security\InheritedPermissionsExtension;
use SilverStripe\Security\Member; use SilverStripe\Security\Member;
use SilverStripe\Security\PermissionChecker; use SilverStripe\Security\PermissionChecker;
use SilverStripe\Security\Security;
use SilverStripe\Versioned\Versioned; use SilverStripe\Versioned\Versioned;
/** /**
@ -45,7 +46,7 @@ class TestPermissionNode extends DataObject implements TestOnly
public function canEdit($member = null) public function canEdit($member = null)
{ {
if (!$member) { if (!$member) {
$member = Member::currentUser(); $member = Security::getCurrentUser();
} }
return static::getInheritedPermissions()->canEdit($this->ID, $member); return static::getInheritedPermissions()->canEdit($this->ID, $member);
} }
@ -53,7 +54,7 @@ class TestPermissionNode extends DataObject implements TestOnly
public function canView($member = null) public function canView($member = null)
{ {
if (!$member) { if (!$member) {
$member = Member::currentUser(); $member = Security::getCurrentUser();
} }
return static::getInheritedPermissions()->canView($this->ID, $member); return static::getInheritedPermissions()->canView($this->ID, $member);
} }
@ -61,7 +62,7 @@ class TestPermissionNode extends DataObject implements TestOnly
public function canDelete($member = null) public function canDelete($member = null)
{ {
if (!$member) { if (!$member) {
$member = Member::currentUser(); $member = Security::getCurrentUser();
} }
return static::getInheritedPermissions()->canDelete($this->ID, $member); return static::getInheritedPermissions()->canDelete($this->ID, $member);
} }

View File

@ -2,20 +2,20 @@
namespace SilverStripe\Security\Tests; namespace SilverStripe\Security\Tests;
use SilverStripe\ORM\DataObject; use SilverStripe\Core\Injector\Injector;
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\PasswordEncryptor; use SilverStripe\Security\MemberAuthenticator\CMSMemberAuthenticator;
use SilverStripe\Security\PasswordEncryptor_PHPHash; use SilverStripe\Security\MemberAuthenticator\CMSMemberLoginForm;
use SilverStripe\Security\MemberAuthenticator\MemberAuthenticator;
use SilverStripe\Security\Security; use SilverStripe\Security\Security;
use SilverStripe\Security\Member; use SilverStripe\Security\Member;
use SilverStripe\Security\MemberAuthenticator; use SilverStripe\Security\MemberAuthenticator\MemberLoginForm;
use SilverStripe\Security\MemberLoginForm; use SilverStripe\Security\IdentityStore;
use SilverStripe\Security\CMSMemberLoginForm;
use SilverStripe\Core\Config\Config; use SilverStripe\Core\Config\Config;
use SilverStripe\Dev\SapphireTest; use SilverStripe\Dev\SapphireTest;
use SilverStripe\Forms\FieldList; use SilverStripe\Control\HTTPRequest;
use SilverStripe\Forms\Form;
class MemberAuthenticatorTest extends SapphireTest class MemberAuthenticatorTest extends SapphireTest
{ {
@ -41,59 +41,6 @@ class MemberAuthenticatorTest extends SapphireTest
parent::tearDown(); parent::tearDown();
} }
public function testLegacyPasswordHashMigrationUponLogin()
{
$member = new Member();
$field=Member::config()->unique_identifier_field;
$member->$field = 'test1@test.com';
$member->PasswordEncryption = "sha1";
$member->Password = "mypassword";
$member->write();
$data = array(
'Email' => $member->$field,
'Password' => 'mypassword'
);
MemberAuthenticator::authenticate($data);
/**
* @var Member $member
*/
$member = DataObject::get_by_id(Member::class, $member->ID);
$this->assertEquals($member->PasswordEncryption, "sha1_v2.4");
$result = $member->checkPassword('mypassword');
$this->assertTrue($result->isValid());
}
public function testNoLegacyPasswordHashMigrationOnIncompatibleAlgorithm()
{
Config::inst()->update(
PasswordEncryptor::class,
'encryptors',
array('crc32' => array(PasswordEncryptor_PHPHash::class => 'crc32'))
);
$field=Member::config()->unique_identifier_field;
$member = new Member();
$member->$field = 'test2@test.com';
$member->PasswordEncryption = "crc32";
$member->Password = "mypassword";
$member->write();
$data = array(
'Email' => $member->$field,
'Password' => 'mypassword'
);
MemberAuthenticator::authenticate($data);
$member = DataObject::get_by_id(Member::class, $member->ID);
$this->assertEquals($member->PasswordEncryption, "crc32");
$result = $member->checkPassword('mypassword');
$this->assertTrue($result->isValid());
}
public function testCustomIdentifierField() public function testCustomIdentifierField()
{ {
@ -109,75 +56,83 @@ class MemberAuthenticatorTest extends SapphireTest
public function testGenerateLoginForm() public function testGenerateLoginForm()
{ {
$authenticator = new MemberAuthenticator();
$controller = new Security(); $controller = new Security();
// Create basic login form // Create basic login form
$frontendForm = MemberAuthenticator::get_login_form($controller); $frontendResponse = $authenticator
$this->assertTrue($frontendForm instanceof MemberLoginForm); ->getLoginHandler($controller->link())
->handleRequest(new HTTPRequest('get', '/'), DataModel::inst());
$this->assertTrue(is_array($frontendResponse));
$this->assertTrue(isset($frontendResponse['Form']));
$this->assertTrue($frontendResponse['Form'] instanceof MemberLoginForm);
}
public function testGenerateCMSLoginForm()
{
/** @var CMSMemberAuthenticator $authenticator */
$authenticator = new CMSMemberAuthenticator();
// Supports cms login form // Supports cms login form
$this->assertTrue(MemberAuthenticator::supports_cms()); $this->assertGreaterThan(0, ($authenticator->supportedServices() & Authenticator::CMS_LOGIN));
$cmsForm = MemberAuthenticator::get_cms_login_form($controller); $cmsHandler = $authenticator->getLoginHandler('/');
$cmsForm = $cmsHandler->loginForm();
$this->assertTrue($cmsForm instanceof CMSMemberLoginForm); $this->assertTrue($cmsForm instanceof CMSMemberLoginForm);
} }
/** /**
* Test that a member can be authenticated via their temp id * Test that a member can be authenticated via their temp id
*/ */
public function testAuthenticateByTempID() public function testAuthenticateByTempID()
{ {
$authenticator = new CMSMemberAuthenticator();
$member = new Member(); $member = new Member();
$member->Email = 'test1@test.com'; $member->Email = 'test1@test.com';
$member->PasswordEncryption = "sha1"; $member->PasswordEncryption = "sha1";
$member->Password = "mypassword"; $member->Password = "mypassword";
$member->write(); $member->write();
// Make form
$controller = new Security();
/**
* @skipUpgrade
*/
$form = new Form($controller, 'Form', new FieldList(), new FieldList());
// If the user has never logged in, then the tempid should be empty // If the user has never logged in, then the tempid should be empty
$tempID = $member->TempIDHash; $tempID = $member->TempIDHash;
$this->assertEmpty($tempID); $this->assertEmpty($tempID);
// If the user logs in then they have a temp id // If the user logs in then they have a temp id
$member->logIn(true); Injector::inst()->get(IdentityStore::class)->logIn($member, true);
$tempID = $member->TempIDHash; $tempID = $member->TempIDHash;
$this->assertNotEmpty($tempID); $this->assertNotEmpty($tempID);
// Test correct login // Test correct login
$result = MemberAuthenticator::authenticate( $result = $authenticator->authenticate(
array( array(
'tempid' => $tempID, 'tempid' => $tempID,
'Password' => 'mypassword' 'Password' => 'mypassword'
), ),
$form $message
); );
$form->restoreFormState();
$this->assertNotEmpty($result); $this->assertNotEmpty($result);
$this->assertEquals($result->ID, $member->ID); $this->assertEquals($result->ID, $member->ID);
$this->assertEmpty($form->getMessage()); $this->assertTrue($message->isValid());
// Test incorrect login // Test incorrect login
$form->clearMessage(); $result = $authenticator->authenticate(
$result = MemberAuthenticator::authenticate(
array( array(
'tempid' => $tempID, 'tempid' => $tempID,
'Password' => 'notmypassword' 'Password' => 'notmypassword'
), ),
$form $message
); );
$form->restoreFormState();
$this->assertEmpty($result); $this->assertEmpty($result);
$messages = $message->getMessages();
$this->assertEquals( $this->assertEquals(
_t('SilverStripe\\Security\\Member.ERRORWRONGCRED', 'The provided details don\'t seem to be correct. Please try again.'), _t('SilverStripe\\Security\\Member.ERRORWRONGCRED', 'The provided details don\'t seem to be correct. Please try again.'),
$form->getMessage() $messages[0]['message']
); );
$this->assertEquals(ValidationResult::TYPE_ERROR, $form->getMessageType());
$this->assertEquals(ValidationResult::CAST_TEXT, $form->getMessageCast());
} }
/** /**
@ -185,64 +140,53 @@ class MemberAuthenticatorTest extends SapphireTest
*/ */
public function testDefaultAdmin() public function testDefaultAdmin()
{ {
// Make form $authenticator = new MemberAuthenticator();
$controller = new Security();
/**
* @skipUpgrade
*/
$form = new Form($controller, 'Form', new FieldList(), new FieldList());
// Test correct login // Test correct login
$result = MemberAuthenticator::authenticate( $result = $authenticator->authenticate(
array( array(
'Email' => 'admin', 'Email' => 'admin',
'Password' => 'password' 'Password' => 'password'
), ),
$form $message
); );
$form->restoreFormState();
$this->assertNotEmpty($result); $this->assertNotEmpty($result);
$this->assertEquals($result->Email, Security::default_admin_username()); $this->assertEquals($result->Email, Security::default_admin_username());
$this->assertEmpty($form->getMessage()); $this->assertTrue($message->isValid());
// Test incorrect login // Test incorrect login
$form->clearMessage(); $result = $authenticator->authenticate(
$result = MemberAuthenticator::authenticate(
array( array(
'Email' => 'admin', 'Email' => 'admin',
'Password' => 'notmypassword' 'Password' => 'notmypassword'
), ),
$form $message
); );
$form->restoreFormState(); $messages = $message->getMessages();
$this->assertEmpty($result); $this->assertEmpty($result);
$this->assertEquals( $this->assertEquals(
'The provided details don\'t seem to be correct. Please try again.', 'The provided details don\'t seem to be correct. Please try again.',
$form->getMessage() $messages[0]['message']
); );
$this->assertEquals(ValidationResult::TYPE_ERROR, $form->getMessageType());
$this->assertEquals(ValidationResult::CAST_TEXT, $form->getMessageCast());
} }
public function testDefaultAdminLockOut() public function testDefaultAdminLockOut()
{ {
$authenticator = new MemberAuthenticator();
Config::inst()->update(Member::class, 'lock_out_after_incorrect_logins', 1); Config::inst()->update(Member::class, 'lock_out_after_incorrect_logins', 1);
Config::inst()->update(Member::class, 'lock_out_delay_mins', 10); Config::inst()->update(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');
$controller = new Security();
/** @skipUpgrade */
$form = new Form($controller, 'Form', new FieldList(), new FieldList());
// Test correct login // Test correct login
MemberAuthenticator::authenticate( $authenticator->authenticate(
[ [
'Email' => 'admin', 'Email' => 'admin',
'Password' => 'wrongpassword' 'Password' => 'wrongpassword'
], ]
$form
); );
$this->assertTrue(Member::default_admin()->isLockedOut()); $this->assertFalse(Member::default_admin()->canLogin()->isValid());
$this->assertEquals('2016-04-18 00:10:00', Member::default_admin()->LockedOutUntil); $this->assertEquals('2016-04-18 00:10:00', Member::default_admin()->LockedOutUntil);
} }
} }

View File

@ -3,6 +3,7 @@
namespace SilverStripe\Security\Tests; namespace SilverStripe\Security\Tests;
use SilverStripe\Core\Convert; use SilverStripe\Core\Convert;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\Dev\FunctionalTest; use SilverStripe\Dev\FunctionalTest;
use SilverStripe\Control\Cookie; use SilverStripe\Control\Cookie;
use SilverStripe\i18n\i18n; use SilverStripe\i18n\i18n;
@ -10,15 +11,17 @@ 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\Security\Member;
use SilverStripe\Security\MemberAuthenticator; use SilverStripe\Security\MemberAuthenticator\SessionAuthenticationHandler;
use SilverStripe\Security\Security; use SilverStripe\Security\Security;
use SilverStripe\Security\MemberPassword; use SilverStripe\Security\MemberPassword;
use SilverStripe\Security\Group; use SilverStripe\Security\Group;
use SilverStripe\Security\Permission; use SilverStripe\Security\Permission;
use SilverStripe\Security\IdentityStore;
use SilverStripe\Security\PasswordEncryptor_Blowfish; use SilverStripe\Security\PasswordEncryptor_Blowfish;
use SilverStripe\Security\RememberLoginHash; use SilverStripe\Security\RememberLoginHash;
use SilverStripe\Security\Member_Validator; use SilverStripe\Security\Member_Validator;
use SilverStripe\Security\Tests\MemberTest\FieldsExtension; use SilverStripe\Security\Tests\MemberTest\FieldsExtension;
use SilverStripe\Control\HTTPRequest;
class MemberTest extends FunctionalTest class MemberTest extends FunctionalTest
{ {
@ -237,13 +240,13 @@ class MemberTest extends FunctionalTest
$this->assertNotNull($member); $this->assertNotNull($member);
// Initiate a password-reset // Initiate a password-reset
$response = $this->post('Security/LostPasswordForm', array('Email' => $member->Email)); $response = $this->post('Security/lostpassword/LostPasswordForm', array('Email' => $member->Email));
$this->assertEquals($response->getStatusCode(), 302); $this->assertEquals($response->getStatusCode(), 302);
// We should get redirected to Security/passwordsent // We should get redirected to Security/passwordsent
$this->assertContains( $this->assertContains(
'Security/passwordsent/testuser@example.com', 'Security/lostpassword/passwordsent/testuser@example.com',
urldecode($response->getHeader('Location')) urldecode($response->getHeader('Location'))
); );
@ -534,26 +537,24 @@ class MemberTest extends FunctionalTest
$member = $this->objFromFixture(Member::class, 'test'); $member = $this->objFromFixture(Member::class, 'test');
$member2 = $this->objFromFixture(Member::class, 'staffmember'); $member2 = $this->objFromFixture(Member::class, 'staffmember');
$this->session()->inst_set('loggedInAs', null);
/* Not logged in, you can't view, delete or edit the record */ /* Not logged in, you can't view, delete or edit the record */
$this->assertFalse($member->canView()); $this->assertFalse($member->canView());
$this->assertFalse($member->canDelete()); $this->assertFalse($member->canDelete());
$this->assertFalse($member->canEdit()); $this->assertFalse($member->canEdit());
/* Logged in users can edit their own record */ /* Logged in users can edit their own record */
$this->session()->inst_set('loggedInAs', $member->ID); $this->logInAs($member);
$this->assertTrue($member->canView()); $this->assertTrue($member->canView());
$this->assertFalse($member->canDelete()); $this->assertFalse($member->canDelete());
$this->assertTrue($member->canEdit()); $this->assertTrue($member->canEdit());
/* Other uses cannot view, delete or edit others records */ /* Other uses cannot view, delete or edit others records */
$this->session()->inst_set('loggedInAs', $member2->ID); $this->logInAs($member2);
$this->assertFalse($member->canView()); $this->assertFalse($member->canView());
$this->assertFalse($member->canDelete()); $this->assertFalse($member->canDelete());
$this->assertFalse($member->canEdit()); $this->assertFalse($member->canEdit());
$this->session()->inst_set('loggedInAs', null); $this->logOut();
} }
public function testAuthorisedMembersCanManipulateOthersRecords() public function testAuthorisedMembersCanManipulateOthersRecords()
@ -562,10 +563,12 @@ class MemberTest extends FunctionalTest
$member2 = $this->objFromFixture(Member::class, 'staffmember'); $member2 = $this->objFromFixture(Member::class, 'staffmember');
/* Group members with SecurityAdmin permissions can manipulate other records */ /* Group members with SecurityAdmin permissions can manipulate other records */
$this->session()->inst_set('loggedInAs', $member->ID); $this->logInAs($member);
$this->assertTrue($member2->canView()); $this->assertTrue($member2->canView());
$this->assertTrue($member2->canDelete()); $this->assertTrue($member2->canDelete());
$this->assertTrue($member2->canEdit()); $this->assertTrue($member2->canEdit());
$this->logOut();
} }
public function testExtendedCan() public function testExtendedCan()
@ -664,12 +667,12 @@ class MemberTest extends FunctionalTest
'Adding new admin group relation is not allowed for non-admin members' 'Adding new admin group relation is not allowed for non-admin members'
); );
$this->session()->inst_set('loggedInAs', $adminMember->ID); $this->logInAs($adminMember);
$this->assertTrue( $this->assertTrue(
$staffMember->onChangeGroups(array($newAdminGroup->ID)), $staffMember->onChangeGroups(array($newAdminGroup->ID)),
'Adding new admin group relation is allowed for normal users, when granter is logged in as admin' 'Adding new admin group relation is allowed for normal users, when granter is logged in as admin'
); );
$this->session()->inst_set('loggedInAs', null); $this->logOut();
$this->assertTrue( $this->assertTrue(
$adminMember->onChangeGroups(array($newAdminGroup->ID)), $adminMember->onChangeGroups(array($newAdminGroup->ID)),
@ -719,7 +722,7 @@ class MemberTest extends FunctionalTest
); );
// Test staff member can be added if they are already admin // Test staff member can be added if they are already admin
$this->session()->inst_set('loggedInAs', null); $this->logOut();
$this->assertFalse($adminMember->inGroup($newAdminGroup)); $this->assertFalse($adminMember->inGroup($newAdminGroup));
$adminMember->Groups()->add($newAdminGroup); $adminMember->Groups()->add($newAdminGroup);
$this->assertTrue( $this->assertTrue(
@ -872,7 +875,8 @@ class MemberTest extends FunctionalTest
{ {
$m1 = $this->objFromFixture(Member::class, 'grouplessmember'); $m1 = $this->objFromFixture(Member::class, 'grouplessmember');
$m1->login(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);
$firstHash = $hashes->first(); $firstHash = $hashes->first();
@ -887,7 +891,8 @@ class MemberTest extends FunctionalTest
*/ */
$m1 = $this->objFromFixture(Member::class, 'noexpiry'); $m1 = $this->objFromFixture(Member::class, 'noexpiry');
$m1->logIn(true); Injector::inst()->get(IdentityStore::class)->logIn($m1, true);
$firstHash = RememberLoginHash::get()->filter('MemberID', $m1->ID)->first(); $firstHash = RememberLoginHash::get()->filter('MemberID', $m1->ID)->first();
$this->assertNotNull($firstHash); $this->assertNotNull($firstHash);
@ -914,7 +919,7 @@ class MemberTest extends FunctionalTest
); );
$this->assertContains($message, $response->getBody()); $this->assertContains($message, $response->getBody());
$this->session()->inst_set('loggedInAs', null); $this->logOut();
// A wrong token or a wrong device ID should not let us autologin // A wrong token or a wrong device ID should not let us autologin
$response = $this->get( $response = $this->get(
@ -922,7 +927,7 @@ class MemberTest extends FunctionalTest
$this->session(), $this->session(),
null, null,
array( array(
'alc_enc' => $m1->ID.':'.str_rot13($token), 'alc_enc' => $m1->ID.':asdfasd'.str_rot13($token),
'alc_device' => $firstHash->DeviceID 'alc_device' => $firstHash->DeviceID
) )
); );
@ -942,12 +947,11 @@ class MemberTest extends FunctionalTest
// Re-logging (ie 'alc_enc' has expired), and not checking the "Remember Me" option // Re-logging (ie 'alc_enc' has expired), and not checking the "Remember Me" option
// should remove all previous hashes for this device // should remove all previous hashes for this device
$response = $this->post( $response = $this->post(
'Security/LoginForm', 'Security/login/default/LoginForm',
array( array(
'Email' => $m1->Email, 'Email' => $m1->Email,
'Password' => '1nitialPassword', 'Password' => '1nitialPassword',
'AuthenticationMethod' => MemberAuthenticator::class, 'action_doLogin' => 'action_doLogin'
'action_dologin' => 'action_dologin'
), ),
null, null,
$this->session(), $this->session(),
@ -966,7 +970,7 @@ class MemberTest extends FunctionalTest
* @var Member $m1 * @var Member $m1
*/ */
$m1 = $this->objFromFixture(Member::class, 'noexpiry'); $m1 = $this->objFromFixture(Member::class, 'noexpiry');
$m1->logIn(true); Injector::inst()->get(IdentityStore::class)->logIn($m1, true);
$firstHash = RememberLoginHash::get()->filter('MemberID', $m1->ID)->first(); $firstHash = RememberLoginHash::get()->filter('MemberID', $m1->ID)->first();
$this->assertNotNull($firstHash); $this->assertNotNull($firstHash);
@ -996,7 +1000,7 @@ class MemberTest extends FunctionalTest
); );
$this->assertContains($message, $response->getBody()); $this->assertContains($message, $response->getBody());
$this->session()->inst_set('loggedInAs', null); $this->logOut();
// re-generates the hash so we can get the token // re-generates the hash so we can get the token
$firstHash->Hash = $firstHash->getNewHash($m1); $firstHash->Hash = $firstHash->getNewHash($m1);
@ -1016,7 +1020,7 @@ class MemberTest extends FunctionalTest
) )
); );
$this->assertNotContains($message, $response->getBody()); $this->assertNotContains($message, $response->getBody());
$this->session()->inst_set('loggedInAs', null); $this->logOut();
DBDatetime::clear_mock_now(); DBDatetime::clear_mock_now();
} }
@ -1025,10 +1029,10 @@ class MemberTest extends FunctionalTest
$m1 = $this->objFromFixture(Member::class, 'noexpiry'); $m1 = $this->objFromFixture(Member::class, 'noexpiry');
// First device // First device
$m1->login(true); Injector::inst()->get(IdentityStore::class)->logIn($m1, true);
Cookie::set('alc_device', null); Cookie::set('alc_device', null);
// Second device // Second device
$m1->login(true); Injector::inst()->get(IdentityStore::class)->logIn($m1, true);
// Hash of first device // Hash of first device
$firstHash = RememberLoginHash::get()->filter('MemberID', $m1->ID)->first(); $firstHash = RememberLoginHash::get()->filter('MemberID', $m1->ID)->first();
@ -1069,7 +1073,11 @@ class MemberTest extends FunctionalTest
); );
$this->assertContains($message, $response->getBody()); $this->assertContains($message, $response->getBody());
$this->session()->inst_set('loggedInAs', null); // Test that removing session but not cookie keeps user
/** @var SessionAuthenticationHandler $sessionHandler */
$sessionHandler = Injector::inst()->get(SessionAuthenticationHandler::class);
$sessionHandler->logOut();
Security::setCurrentUser(null);
// Accessing the login page from the second device // Accessing the login page from the second device
$response = $this->get( $response = $this->get(
@ -1101,7 +1109,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);
$m1->login(true); Injector::inst()->get(IdentityStore::class)->logIn($m1, true);
$response = $this->get('Security/logout', $this->session()); $response = $this->get('Security/logout', $this->session());
$this->assertEquals( $this->assertEquals(
RememberLoginHash::get()->filter('MemberID', $m1->ID)->count(), RememberLoginHash::get()->filter('MemberID', $m1->ID)->count(),
@ -1156,8 +1164,8 @@ class MemberTest extends FunctionalTest
'Failed to increment $member->FailedLoginCount' 'Failed to increment $member->FailedLoginCount'
); );
$this->assertFalse( $this->assertTrue(
$member->isLockedOut(), $member->canLogin()->isValid(),
"Member has been locked out too early" "Member has been locked out too early"
); );
} }
@ -1362,12 +1370,12 @@ class MemberTest extends FunctionalTest
public function testCurrentUser() public function testCurrentUser()
{ {
$this->assertNull(Member::currentUser()); $this->assertNull(Security::getCurrentUser());
$adminMember = $this->objFromFixture(Member::class, 'admin'); $adminMember = $this->objFromFixture(Member::class, 'admin');
$this->logInAs($adminMember); $this->logInAs($adminMember);
$userFromSession = Member::currentUser(); $userFromSession = Security::getCurrentUser();
$this->assertEquals($adminMember->ID, $userFromSession->ID); $this->assertEquals($adminMember->ID, $userFromSession->ID);
} }
@ -1376,7 +1384,7 @@ class MemberTest extends FunctionalTest
*/ */
public function testActAsUserPermissions() public function testActAsUserPermissions()
{ {
$this->assertNull(Member::currentUser()); $this->assertNull(Security::getCurrentUser());
/** @var Member $adminMember */ /** @var Member $adminMember */
$adminMember = $this->objFromFixture(Member::class, 'admin'); $adminMember = $this->objFromFixture(Member::class, 'admin');
@ -1415,21 +1423,21 @@ class MemberTest extends FunctionalTest
*/ */
public function testActAsUser() public function testActAsUser()
{ {
$this->assertNull(Member::currentUser()); $this->assertNull(Security::getCurrentUser());
/** @var Member $adminMember */ /** @var Member $adminMember */
$adminMember = $this->objFromFixture(Member::class, 'admin'); $adminMember = $this->objFromFixture(Member::class, 'admin');
$memberID = Member::actAs($adminMember, function () { $member = Member::actAs($adminMember, function () {
return Member::currentUserID(); return Security::getCurrentUser();
}); });
$this->assertEquals($adminMember->ID, $memberID); $this->assertEquals($adminMember->ID, $member->ID);
// Check nesting // Check nesting
$memberID = Member::actAs($adminMember, function () { $member = Member::actAs($adminMember, function () {
return Member::actAs(null, function () { return Member::actAs(null, function () {
return Member::currentUserID(); return Security::getCurrentUser();
}); });
}); });
$this->assertEmpty($memberID); $this->assertEmpty($member);
} }
} }

View File

@ -2,18 +2,16 @@
namespace SilverStripe\Security\Tests; namespace SilverStripe\Security\Tests;
use PhpConsole\Auth; use SilverStripe\Dev\Debug;
use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\FieldType\DBDatetime; use SilverStripe\ORM\FieldType\DBDatetime;
use SilverStripe\ORM\FieldType\DBClassName; use SilverStripe\ORM\FieldType\DBClassName;
use SilverStripe\ORM\DB; use SilverStripe\ORM\DB;
use SilverStripe\ORM\ValidationResult; use SilverStripe\ORM\ValidationResult;
use SilverStripe\Security\Authenticator;
use SilverStripe\Security\LoginAttempt; use SilverStripe\Security\LoginAttempt;
use SilverStripe\Security\Member; use SilverStripe\Security\Member;
use SilverStripe\Security\MemberAuthenticator; use SilverStripe\Security\MemberAuthenticator\MemberAuthenticator;
use SilverStripe\Security\Security; use SilverStripe\Security\Security;
use SilverStripe\Security\Permission;
use SilverStripe\Core\Config\Config; use SilverStripe\Core\Config\Config;
use SilverStripe\Core\Convert; use SilverStripe\Core\Convert;
use SilverStripe\Dev\FunctionalTest; use SilverStripe\Dev\FunctionalTest;
@ -48,13 +46,9 @@ class SecurityTest extends FunctionalTest
protected function setUp() protected function setUp()
{ {
// This test assumes that MemberAuthenticator is present and the default
$this->priorAuthenticators = Authenticator::get_authenticators();
$this->priorDefaultAuthenticator = Authenticator::get_default_authenticator();
// Set to an empty array of authenticators to enable the default // Set to an empty array of authenticators to enable the default
Config::modify()->set(Authenticator::class, 'authenticators', []); Config::modify()->set(MemberAuthenticator::class, 'authenticators', []);
Config::modify()->set(Authenticator::class, 'default_authenticator', MemberAuthenticator::class); Config::modify()->set(MemberAuthenticator::class, 'default_authenticator', MemberAuthenticator::class);
// And that the unique identified field is 'Email' // And that the unique identified field is 'Email'
$this->priorUniqueIdentifierField = Member::config()->unique_identifier_field; $this->priorUniqueIdentifierField = Member::config()->unique_identifier_field;
@ -74,8 +68,8 @@ class SecurityTest extends FunctionalTest
// Restore selected authenticator // Restore selected authenticator
// MemberAuthenticator might not actually be present // MemberAuthenticator might not actually be present
Config::modify()->set(Authenticator::class, 'authenticators', $this->priorAuthenticators); // Config::modify()->set(Authenticator::class, 'authenticators', $this->priorAuthenticators);
Config::modify()->set(Authenticator::class, 'default_authenticator', $this->priorDefaultAuthenticator); // Config::modify()->set(Authenticator::class, 'default_authenticator', $this->priorDefaultAuthenticator);
// Restore unique identifier field // Restore unique identifier field
Member::config()->unique_identifier_field = $this->priorUniqueIdentifierField; Member::config()->unique_identifier_field = $this->priorUniqueIdentifierField;
@ -182,19 +176,19 @@ class SecurityTest extends FunctionalTest
public function testAutomaticRedirectionOnLogin() public function testAutomaticRedirectionOnLogin()
{ {
// BackURL with permission error (not authenticated) should not redirect // BackURL with permission error (not authenticated) should not redirect
if ($member = Member::currentUser()) { if ($member = Security::getCurrentUser()) {
$member->logOut(); Security::setCurrentUser(null);
} }
$response = $this->getRecursive('SecurityTest_SecuredController'); $response = $this->getRecursive('SecurityTest_SecuredController');
$this->assertContains(Convert::raw2xml("That page is secured."), $response->getBody()); $this->assertContains(Convert::raw2xml("That page is secured."), $response->getBody());
$this->assertContains('<input type="submit" name="action_dologin"', $response->getBody()); $this->assertContains('<input type="submit" name="action_doLogin"', $response->getBody());
// Non-logged in user should not be redirected, but instead shown the login form // Non-logged in user should not be redirected, but instead shown the login form
// No message/context is available as the user has not attempted to view the secured controller // No message/context is available as the user has not attempted to view the secured controller
$response = $this->getRecursive('Security/login?BackURL=SecurityTest_SecuredController/'); $response = $this->getRecursive('Security/login?BackURL=SecurityTest_SecuredController/');
$this->assertNotContains(Convert::raw2xml("That page is secured."), $response->getBody()); $this->assertNotContains(Convert::raw2xml("That page is secured."), $response->getBody());
$this->assertNotContains(Convert::raw2xml("You don't have access to this page"), $response->getBody()); $this->assertNotContains(Convert::raw2xml("You don't have access to this page"), $response->getBody());
$this->assertContains('<input type="submit" name="action_dologin"', $response->getBody()); $this->assertContains('<input type="submit" name="action_doLogin"', $response->getBody());
// BackURL with permission error (wrong permissions) should not redirect // BackURL with permission error (wrong permissions) should not redirect
$this->logInAs('grouplessmember'); $this->logInAs('grouplessmember');
@ -228,7 +222,7 @@ class SecurityTest extends FunctionalTest
$member = DataObject::get_one(Member::class); $member = DataObject::get_one(Member::class);
/* Log in with any user that we can find */ /* Log in with any user that we can find */
$this->session()->inst_set('loggedInAs', $member->ID); Security::setCurrentUser($member);
/* View the Security/login page */ /* View the Security/login page */
$response = $this->get(Config::inst()->get(Security::class, 'login_url')); $response = $this->get(Config::inst()->get(Security::class, 'login_url'));
@ -245,8 +239,7 @@ class SecurityTest extends FunctionalTest
'MemberLoginForm_LoginForm', 'MemberLoginForm_LoginForm',
null, null,
array( array(
'AuthenticationMethod' => MemberAuthenticator::class, 'action_logout' => 1,
'action_dologout' => 1,
) )
); );
@ -255,7 +248,7 @@ class SecurityTest extends FunctionalTest
$this->assertNotNull($response->getBody(), 'There is body content on the page'); $this->assertNotNull($response->getBody(), 'There is body content on the page');
/* Log the user out */ /* Log the user out */
$this->session()->inst_set('loggedInAs', null); Security::setCurrentUser(null);
} }
public function testMemberIDInSessionDoesntExistInDatabaseHasToLogin() public function testMemberIDInSessionDoesntExistInDatabaseHasToLogin()
@ -379,6 +372,8 @@ class SecurityTest extends FunctionalTest
); );
$this->assertEquals($this->idFromFixture(Member::class, 'test'), $this->session()->inst_get('loggedInAs')); $this->assertEquals($this->idFromFixture(Member::class, 'test'), $this->session()->inst_get('loggedInAs'));
$this->logOut();
/* EXPIRED PASSWORDS ARE SENT TO THE CHANGE PASSWORD FORM */ /* EXPIRED PASSWORDS ARE SENT TO THE CHANGE PASSWORD FORM */
$expiredResponse = $this->doTestLoginForm('expired@silverstripe.com', '1nitialPassword'); $expiredResponse = $this->doTestLoginForm('expired@silverstripe.com', '1nitialPassword');
$this->assertEquals(302, $expiredResponse->getStatusCode()); $this->assertEquals(302, $expiredResponse->getStatusCode());
@ -416,6 +411,7 @@ class SecurityTest extends FunctionalTest
$this->assertEquals($this->idFromFixture(Member::class, 'test'), $this->session()->inst_get('loggedInAs')); $this->assertEquals($this->idFromFixture(Member::class, 'test'), $this->session()->inst_get('loggedInAs'));
// Check if we can login with the new password // Check if we can login with the new password
$this->logOut();
$goodResponse = $this->doTestLoginForm('testuser@example.com', 'changedPassword'); $goodResponse = $this->doTestLoginForm('testuser@example.com', 'changedPassword');
$this->assertEquals(302, $goodResponse->getStatusCode()); $this->assertEquals(302, $goodResponse->getStatusCode());
$this->assertEquals( $this->assertEquals(
@ -436,7 +432,7 @@ class SecurityTest extends FunctionalTest
// Request new password by email // Request new password by email
$response = $this->get('Security/lostpassword'); $response = $this->get('Security/lostpassword');
$response = $this->post('Security/LostPasswordForm', array('Email' => 'testuser@example.com')); $response = $this->post('Security/lostpassword/LostPasswordForm', array('Email' => 'testuser@example.com'));
$this->assertEmailSent('testuser@example.com'); $this->assertEmailSent('testuser@example.com');
@ -461,6 +457,7 @@ class SecurityTest extends FunctionalTest
$this->assertEquals($this->idFromFixture(Member::class, 'test'), $this->session()->inst_get('loggedInAs')); $this->assertEquals($this->idFromFixture(Member::class, 'test'), $this->session()->inst_get('loggedInAs'));
// Check if we can login with the new password // Check if we can login with the new password
$this->logOut();
$goodResponse = $this->doTestLoginForm('testuser@example.com', 'changedPassword'); $goodResponse = $this->doTestLoginForm('testuser@example.com', 'changedPassword');
$this->assertEquals(302, $goodResponse->getStatusCode()); $this->assertEquals(302, $goodResponse->getStatusCode());
$this->assertEquals($this->idFromFixture(Member::class, 'test'), $this->session()->inst_get('loggedInAs')); $this->assertEquals($this->idFromFixture(Member::class, 'test'), $this->session()->inst_get('loggedInAs'));
@ -479,11 +476,11 @@ class SecurityTest extends FunctionalTest
Member::config()->lock_out_delay_mins = 15; Member::config()->lock_out_delay_mins = 15;
// Login with a wrong password for more than the defined threshold // Login with a wrong password for more than the defined threshold
for ($i = 1; $i <= Member::config()->lock_out_after_incorrect_logins+1; $i++) { for ($i = 1; $i <= (Member::config()->lock_out_after_incorrect_logins+1); $i++) {
$this->doTestLoginForm('testuser@example.com', 'incorrectpassword'); $this->doTestLoginForm('testuser@example.com', 'incorrectpassword');
$member = DataObject::get_by_id(Member::class, $this->idFromFixture(Member::class, 'test')); $member = DataObject::get_by_id(Member::class, $this->idFromFixture(Member::class, 'test'));
if ($i < Member::config()->lock_out_after_incorrect_logins) { if ($i < Member::config()->get('lock_out_after_incorrect_logins')) {
$this->assertNull( $this->assertNull(
$member->LockedOutUntil, $member->LockedOutUntil,
'User does not have a lockout time set if under threshold for failed attempts' 'User does not have a lockout time set if under threshold for failed attempts'
@ -502,18 +499,16 @@ class SecurityTest extends FunctionalTest
'User has a lockout time set after too many failed attempts' 'User has a lockout time set after too many failed attempts'
); );
} }
$msg = _t(
'SilverStripe\\Security\\Member.ERRORLOCKEDOUT2',
'Your account has been temporarily disabled because of too many failed attempts at ' .
'logging in. Please try again in {count} minutes.',
null,
array('count' => Member::config()->lock_out_delay_mins)
);
if ($i > Member::config()->lock_out_after_incorrect_logins) {
$this->assertHasMessage($msg);
}
} }
$msg = _t(
'SilverStripe\\Security\\Member.ERRORLOCKEDOUT2',
'Your account has been temporarily disabled because of too many failed attempts at ' .
'logging in. Please try again in {count} minutes.',
null,
array('count' => Member::config()->lock_out_delay_mins)
);
$this->assertHasMessage($msg);
$this->doTestLoginForm('testuser@example.com', '1nitialPassword'); $this->doTestLoginForm('testuser@example.com', '1nitialPassword');
$this->assertNull( $this->assertNull(
@ -533,7 +528,7 @@ class SecurityTest extends FunctionalTest
); );
// Log the user out // Log the user out
$this->session()->inst_set('loggedInAs', null); $this->logOut();
// Login again with wrong password, but less attempts than threshold // Login again with wrong password, but less attempts than threshold
for ($i = 1; $i < Member::config()->lock_out_after_incorrect_logins; $i++) { for ($i = 1; $i < Member::config()->lock_out_after_incorrect_logins; $i++) {
@ -594,14 +589,14 @@ class SecurityTest extends FunctionalTest
$attempt = DataObject::get_one( $attempt = DataObject::get_one(
LoginAttempt::class, LoginAttempt::class,
array( array(
'"LoginAttempt"."Email"' => 'testuser@example.com' '"LoginAttempt"."Email"' => 'testuser@example.com'
) )
); );
$this->assertTrue(is_object($attempt)); $this->assertTrue(is_object($attempt));
$member = DataObject::get_one( $member = DataObject::get_one(
Member::class, Member::class,
array( array(
'"Member"."Email"' => 'testuser@example.com' '"Member"."Email"' => 'testuser@example.com'
) )
); );
$this->assertEquals($attempt->Status, 'Failure'); $this->assertEquals($attempt->Status, 'Failure');
@ -648,9 +643,7 @@ class SecurityTest extends FunctionalTest
public function testDatabaseIsReadyWithInsufficientMemberColumns() public function testDatabaseIsReadyWithInsufficientMemberColumns()
{ {
$old = Security::$force_database_is_ready; Security::clear_database_is_ready();
Security::$force_database_is_ready = null;
Security::$database_is_ready = false;
DBClassName::clear_classname_cache(); DBClassName::clear_classname_cache();
// Assumption: The database has been built correctly by the test runner, // Assumption: The database has been built correctly by the test runner,
@ -666,8 +659,6 @@ class SecurityTest extends FunctionalTest
// Rebuild the database (which re-adds the Email column), and try again // Rebuild the database (which re-adds the Email column), and try again
static::resetDBSchema(true); static::resetDBSchema(true);
$this->assertTrue(Security::database_is_ready()); $this->assertTrue(Security::database_is_ready());
Security::$force_database_is_ready = $old;
} }
public function testSecurityControllerSendsRobotsTagHeader() public function testSecurityControllerSendsRobotsTagHeader()
@ -703,7 +694,7 @@ class SecurityTest extends FunctionalTest
'Email' => $email, 'Email' => $email,
'Password' => $password, 'Password' => $password,
'AuthenticationMethod' => MemberAuthenticator::class, 'AuthenticationMethod' => MemberAuthenticator::class,
'action_dologin' => 1, 'action_doLogin' => 1,
) )
); );
} }

View File

@ -16,6 +16,7 @@ use SilverStripe\ORM\FieldType\DBField;
use SilverStripe\ORM\ArrayList; use SilverStripe\ORM\ArrayList;
use SilverStripe\ORM\PaginatedList; use SilverStripe\ORM\PaginatedList;
use SilverStripe\Security\Member; use SilverStripe\Security\Member;
use SilverStripe\Security\Security;
use SilverStripe\Security\SecurityToken; use SilverStripe\Security\SecurityToken;
use SilverStripe\Security\Permission; use SilverStripe\Security\Permission;
use SilverStripe\View\ArrayData; use SilverStripe\View\ArrayData;
@ -406,22 +407,22 @@ SS;
); );
$this->assertEquals( $this->assertEquals(
(string)Member::currentUser(), (string)Security::getCurrentUser(),
$this->render('{$CurrentMember}'), $this->render('{$CurrentMember}'),
'Member template functions result correct result' 'Member template functions result correct result'
); );
$this->assertEquals( $this->assertEquals(
(string)Member::currentUser(), (string)Security::getCurrentUser(),
$this->render('{$CurrentUser}'), $this->render('{$CurrentUser}'),
'Member template functions result correct result' 'Member template functions result correct result'
); );
$this->assertEquals( $this->assertEquals(
(string)Member::currentUser(), (string)Security::getCurrentUser(),
$this->render('{$currentMember}'), $this->render('{$currentMember}'),
'Member template functions result correct result' 'Member template functions result correct result'
); );
$this->assertEquals( $this->assertEquals(
(string)Member::currentUser(), (string)Security::getCurrentUser(),
$this->render('{$currentUser}'), $this->render('{$currentUser}'),
'Member template functions result correct result' 'Member template functions result correct result'
); );