mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 14:05:37 +02:00
Merge pull request #6829 from sminnee/authenticator-refactor
Refactor Authenticators
This commit is contained in:
commit
c7f7233c4d
@ -12,12 +12,6 @@ use SilverStripe\View\Parsers\ShortcodeParser;
|
||||
* Here you can make different settings for the Framework module (the core
|
||||
* 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')
|
||||
|
@ -1,4 +1,35 @@
|
||||
SilverStripe\Security\MemberLoginForm:
|
||||
required_fields:
|
||||
- Email
|
||||
- Password
|
||||
---
|
||||
Name: coreauthentication
|
||||
---
|
||||
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
|
||||
|
@ -30,7 +30,7 @@ Example: Disallow creation of new players if the currently logged-in player is n
|
||||
public function onBeforeWrite() {
|
||||
// check on first write action, aka "database row creation" (ID-property is not set)
|
||||
if(!$this->isInDb()) {
|
||||
$currentPlayer = Member::currentUser();
|
||||
$currentPlayer = Security::getCurrentUser();
|
||||
|
||||
if(!$currentPlayer->IsTeamManager()) {
|
||||
user_error('Player-creation not allowed', E_USER_ERROR);
|
||||
|
@ -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()`.
|
||||
|
||||
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">
|
||||
By default, all `DataObject` subclasses can only be edited, created and viewed by users with the 'ADMIN' permission
|
||||
|
@ -40,7 +40,7 @@ includes [api:Controller], [api:FormField] and [api:DataObject] instances.
|
||||
```php
|
||||
$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
|
||||
|
@ -109,7 +109,7 @@ we added a `SayHi` method which is unique to our extension.
|
||||
|
||||
**mysite/code/Page.php**
|
||||
:::php
|
||||
$member = Member::currentUser();
|
||||
$member = Security::getCurrentUser();
|
||||
echo $member->SayHi;
|
||||
|
||||
// "Hi Sam"
|
||||
@ -220,7 +220,7 @@ To see what extensions are currently enabled on an object, use [api:Object::getE
|
||||
|
||||
|
||||
:::php
|
||||
$member = Member::currentUser();
|
||||
$member = Security::getCurrentUser();
|
||||
|
||||
print_r($member->getExtensionInstances());
|
||||
|
||||
|
@ -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.
|
||||
|
||||
:::php
|
||||
if( $member = Member::currentUser() ) {
|
||||
if( $member = Security::getCurrentUser() ) {
|
||||
// Work with $member
|
||||
} else {
|
||||
// Do non-member stuff
|
||||
|
@ -60,7 +60,7 @@ The PHP Logic..
|
||||
$email = SilverStripe\Control\Email\Email::create()
|
||||
->setHTMLTemplate('Email\\MyCustomEmail')
|
||||
->setData(array(
|
||||
'Member' => Member::currentUser(),
|
||||
'Member' => Security::getCurrentUser(),
|
||||
'Link'=> $link,
|
||||
))
|
||||
->setFrom($from)
|
||||
|
@ -9,6 +9,7 @@ use SilverStripe\ORM\DataModel;
|
||||
use SilverStripe\ORM\FieldType\DBHTMLText;
|
||||
use SilverStripe\Security\BasicAuth;
|
||||
use SilverStripe\Security\Member;
|
||||
use SilverStripe\Security\Security;
|
||||
use SilverStripe\View\SSViewer;
|
||||
use SilverStripe\View\TemplateGlobalProvider;
|
||||
|
||||
@ -575,7 +576,7 @@ class Controller extends RequestHandler implements TemplateGlobalProvider
|
||||
public function can($perm, $member = null)
|
||||
{
|
||||
if (!$member) {
|
||||
$member = Member::currentUser();
|
||||
$member = Security::getCurrentUser();
|
||||
}
|
||||
if (is_array($perm)) {
|
||||
$perm = array_map(array($this, 'can'), $perm, array_fill(0, count($perm), $member));
|
||||
|
@ -296,6 +296,20 @@ class RequestHandler extends ViewableData
|
||||
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
|
||||
*
|
||||
|
@ -5,8 +5,10 @@ namespace SilverStripe\Dev;
|
||||
use SilverStripe\Control\Session;
|
||||
use SilverStripe\Control\HTTPResponse;
|
||||
use SilverStripe\Core\Config\Config;
|
||||
use SilverStripe\ORM\DataObject;
|
||||
use SilverStripe\Security\BasicAuth;
|
||||
use SilverStripe\Security\Member;
|
||||
use SilverStripe\Security\Security;
|
||||
use SilverStripe\Security\SecurityToken;
|
||||
use SilverStripe\View\SSViewer;
|
||||
use PHPUnit_Framework_AssertionFailedError;
|
||||
@ -104,6 +106,8 @@ class FunctionalTest extends SapphireTest
|
||||
// basis.
|
||||
BasicAuth::protect_entire_site(false);
|
||||
|
||||
$this->logOut();
|
||||
|
||||
SecurityToken::disable();
|
||||
}
|
||||
|
||||
@ -394,24 +398,6 @@ class FunctionalTest extends SapphireTest
|
||||
$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.
|
||||
* This is helpful if you're not testing publication functionality and don't want "stage management" cluttering
|
||||
|
@ -25,6 +25,7 @@ use SilverStripe\Core\Resettable;
|
||||
use SilverStripe\i18n\i18n;
|
||||
use SilverStripe\ORM\DataExtension;
|
||||
use SilverStripe\ORM\SS_List;
|
||||
use SilverStripe\Security\IdentityStore;
|
||||
use SilverStripe\Versioned\Versioned;
|
||||
use SilverStripe\ORM\DataObject;
|
||||
use SilverStripe\ORM\DataModel;
|
||||
@ -276,7 +277,7 @@ class SapphireTest extends PHPUnit_Framework_TestCase
|
||||
if (Controller::has_curr()) {
|
||||
Controller::curr()->setSession(Session::create(array()));
|
||||
}
|
||||
Security::$database_is_ready = null;
|
||||
Security::clear_database_is_ready();
|
||||
|
||||
// Set up test routes
|
||||
$this->setUpRoutes();
|
||||
@ -1250,10 +1251,33 @@ class SapphireTest extends PHPUnit_Framework_TestCase
|
||||
|
||||
$this->cache_generatedMembers[$permCode] = $member;
|
||||
}
|
||||
$member->logIn();
|
||||
$this->logInAs($member);
|
||||
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()
|
||||
*/
|
||||
|
@ -38,7 +38,7 @@ class TestSession
|
||||
/**
|
||||
* Necessary to use the mock session
|
||||
* 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
|
||||
*/
|
||||
|
@ -5,6 +5,7 @@ namespace SilverStripe\Forms;
|
||||
use SilverStripe\ORM\DataObject;
|
||||
use SilverStripe\ORM\DataObjectInterface;
|
||||
use SilverStripe\Security\Member;
|
||||
use SilverStripe\Security\Security;
|
||||
use SilverStripe\View\Requirements;
|
||||
|
||||
/**
|
||||
@ -504,7 +505,7 @@ class ConfirmedPasswordField extends FormField
|
||||
}
|
||||
|
||||
// Check this password is valid for the current user
|
||||
$member = Member::currentUser();
|
||||
$member = Security::getCurrentUser();
|
||||
if (!$member) {
|
||||
$validator->validationError(
|
||||
$name,
|
||||
|
@ -228,23 +228,23 @@ class FormRequestHandler extends RequestHandler
|
||||
// First, try a handler method on the controller (has been checked for allowed_actions above already)
|
||||
$controller = $this->form->getController();
|
||||
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.
|
||||
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
|
||||
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
|
||||
$field = $this->checkFieldsForAction($this->form->Fields(), $funcName);
|
||||
if ($field) {
|
||||
return $field->$funcName($vars, $this->form, $request);
|
||||
return $field->$funcName($vars, $this->form, $request, $this);
|
||||
}
|
||||
} catch (ValidationException $e) {
|
||||
// The ValdiationResult contains all the relevant metadata
|
||||
|
@ -10,6 +10,7 @@ use SilverStripe\ORM\DataObject;
|
||||
use SilverStripe\ORM\FieldType\DBDatetime;
|
||||
use SilverStripe\ORM\FieldType\DBHTMLText;
|
||||
use SilverStripe\Security\Member;
|
||||
use SilverStripe\Security\Security;
|
||||
use SilverStripe\View\Requirements;
|
||||
use SilverStripe\View\ArrayData;
|
||||
|
||||
@ -249,7 +250,7 @@ class GridFieldPrintButton implements GridField_HTMLProvider, GridField_ActionPr
|
||||
"Header" => $header,
|
||||
"ItemRows" => $itemRows,
|
||||
"Datetime" => DBDatetime::now(),
|
||||
"Member" => Member::currentUser(),
|
||||
"Member" => Security::getCurrentUser(),
|
||||
));
|
||||
|
||||
return $ret;
|
||||
|
@ -490,6 +490,17 @@ abstract class Database
|
||||
*/
|
||||
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
|
||||
*
|
||||
|
@ -24,6 +24,7 @@ use SilverStripe\ORM\FieldType\DBComposite;
|
||||
use SilverStripe\ORM\FieldType\DBClassName;
|
||||
use SilverStripe\Security\Member;
|
||||
use SilverStripe\Security\Permission;
|
||||
use SilverStripe\Security\Security;
|
||||
use SilverStripe\View\ViewableData;
|
||||
use LogicException;
|
||||
use InvalidArgumentException;
|
||||
@ -76,11 +77,11 @@ use stdClass;
|
||||
* static $api_access = true;
|
||||
*
|
||||
* function canView($member = false) {
|
||||
* if(!$member) $member = Member::currentUser();
|
||||
* if(!$member) $member = Security::getCurrentUser();
|
||||
* return $member->inGroup('Subscribers');
|
||||
* }
|
||||
* function canEdit($member = false) {
|
||||
* if(!$member) $member = Member::currentUser();
|
||||
* if(!$member) $member = Security::getCurrentUser();
|
||||
* return $member->inGroup('Editors');
|
||||
* }
|
||||
*
|
||||
@ -2498,7 +2499,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
public function can($perm, $member = null, $context = array())
|
||||
{
|
||||
if (!$member) {
|
||||
$member = Member::currentUser();
|
||||
$member = Security::getCurrentUser();
|
||||
}
|
||||
|
||||
if ($member && Permission::checkMember($member, "ADMIN")) {
|
||||
|
@ -9,6 +9,7 @@ use SilverStripe\Forms\DateField;
|
||||
use SilverStripe\i18n\i18n;
|
||||
use SilverStripe\ORM\DB;
|
||||
use SilverStripe\Security\Member;
|
||||
use SilverStripe\Security\Security;
|
||||
|
||||
/**
|
||||
* Represents a date field.
|
||||
@ -250,7 +251,7 @@ class DBDate extends DBField
|
||||
public function FormatFromSettings($member = null)
|
||||
{
|
||||
if (!$member) {
|
||||
$member = Member::currentUser();
|
||||
$member = Security::getCurrentUser();
|
||||
}
|
||||
|
||||
// Fall back to nice
|
||||
|
@ -7,6 +7,7 @@ use SilverStripe\Forms\DatetimeField;
|
||||
use SilverStripe\i18n\i18n;
|
||||
use SilverStripe\ORM\DB;
|
||||
use SilverStripe\Security\Member;
|
||||
use SilverStripe\Security\Security;
|
||||
use SilverStripe\View\TemplateGlobalProvider;
|
||||
use Exception;
|
||||
use InvalidArgumentException;
|
||||
@ -97,7 +98,7 @@ class DBDatetime extends DBDate implements TemplateGlobalProvider
|
||||
public function FormatFromSettings($member = null)
|
||||
{
|
||||
if (!$member) {
|
||||
$member = Member::currentUser();
|
||||
$member = Security::getCurrentUser();
|
||||
}
|
||||
|
||||
// Fall back to nice
|
||||
|
@ -8,6 +8,7 @@ use SilverStripe\Forms\TimeField;
|
||||
use SilverStripe\i18n\i18n;
|
||||
use SilverStripe\ORM\DB;
|
||||
use SilverStripe\Security\Member;
|
||||
use SilverStripe\Security\Security;
|
||||
|
||||
/**
|
||||
* Represents a column in the database with the type 'Time'.
|
||||
@ -153,7 +154,7 @@ class DBTime extends DBField
|
||||
public function FormatFromSettings($member = null)
|
||||
{
|
||||
if (!$member) {
|
||||
$member = Member::currentUser();
|
||||
$member = Security::getCurrentUser();
|
||||
}
|
||||
|
||||
// Fall back to nice
|
||||
|
40
src/Security/AuthenticationHandler.php
Normal file
40
src/Security/AuthenticationHandler.php
Normal 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);
|
||||
}
|
75
src/Security/AuthenticationRequestFilter.php
Normal file
75
src/Security/AuthenticationRequestFilter.php
Normal 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)
|
||||
{
|
||||
}
|
||||
}
|
@ -2,11 +2,9 @@
|
||||
|
||||
namespace SilverStripe\Security;
|
||||
|
||||
use SilverStripe\Core\Config\Configurable;
|
||||
use SilverStripe\Core\Extensible;
|
||||
use SilverStripe\Core\Injector\Injectable;
|
||||
use SilverStripe\Control\Controller;
|
||||
use SilverStripe\Forms\Form;
|
||||
use SilverStripe\ORM\ValidationResult;
|
||||
use SilverStripe\Security\MemberAuthenticator\LoginHandler;
|
||||
use SilverStripe\Security\MemberAuthenticator\LogoutHandler;
|
||||
|
||||
/**
|
||||
* Abstract base class for an authentication method
|
||||
@ -16,125 +14,76 @@ use SilverStripe\Forms\Form;
|
||||
*
|
||||
* @author Markus Lanthaler <markus@silverstripe.com>
|
||||
*/
|
||||
abstract class Authenticator
|
||||
interface Authenticator
|
||||
{
|
||||
use Injectable;
|
||||
use Configurable;
|
||||
use Extensible;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->constructExtensions();
|
||||
}
|
||||
const LOGIN = 1;
|
||||
const LOGOUT = 2;
|
||||
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
|
||||
*/
|
||||
private static $authenticators = [];
|
||||
|
||||
/**
|
||||
* Used to influence the order of authenticators on the login-screen
|
||||
* (default shows first).
|
||||
* The number should be a bitwise-OR of 1 or more of the following constants:
|
||||
* Authenticator::LOGIN, Authenticator::LOGOUT, Authenticator::CHANGE_PASSWORD,
|
||||
* Authenticator::RESET_PASSWORD, or Authenticator::CMS_LOGIN
|
||||
*
|
||||
* @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
|
||||
* @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
|
||||
*/
|
||||
public static function authenticate($RAW_data, Form $form = null)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Method that creates the login form for this authentication method
|
||||
* The default URL of the RequestHandler should return the initial log-in form, any other
|
||||
* URL may be added for other steps & processing.
|
||||
*
|
||||
* @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)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Method that creates the re-authentication form for the in-CMS view
|
||||
* URL-handling methods may return an array [ "Form" => (form-object) ] which can then
|
||||
* be merged into a default controller.
|
||||
*
|
||||
* @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
|
||||
*/
|
||||
public static function supports_cms()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a given authenticator is registered
|
||||
* The default URL of the RequestHandler should log the user out immediately and destroy the session.
|
||||
*
|
||||
* @param string $authenticator Name of the authenticator class to check
|
||||
* @return bool Returns TRUE if the authenticator is registered, FALSE
|
||||
* otherwise.
|
||||
* @param string $link The base link to use for this RequestHandler
|
||||
* @return LogoutHandler
|
||||
*/
|
||||
public static function is_registered($authenticator)
|
||||
{
|
||||
$authenticators = self::config()->get('authenticators');
|
||||
if (count($authenticators) === 0) {
|
||||
$authenticators = [self::config()->get('default_authenticator')];
|
||||
}
|
||||
|
||||
return in_array($authenticator, $authenticators, true);
|
||||
}
|
||||
|
||||
public function getLogOutHandler($link);
|
||||
|
||||
/**
|
||||
* Get all registered authenticators
|
||||
* Return RequestHandler to manage the change-password process.
|
||||
*
|
||||
* @return array Returns an array with the class names of all registered
|
||||
* authenticators.
|
||||
* The default URL of the RequetHandler should return the initial change-password form,
|
||||
* 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()
|
||||
{
|
||||
$authenticators = self::config()->get('authenticators');
|
||||
$default = self::config()->get('default_authenticator');
|
||||
public function getChangePasswordHandler($link);
|
||||
|
||||
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()
|
||||
{
|
||||
return self::config()->get('default_authenticator');
|
||||
}
|
||||
public function getLostPasswordHandler($link);
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
|
@ -2,12 +2,14 @@
|
||||
|
||||
namespace SilverStripe\Security;
|
||||
|
||||
use SilverStripe\Control\Controller;
|
||||
use SilverStripe\Control\Director;
|
||||
use SilverStripe\Control\HTTPRequest;
|
||||
use SilverStripe\Control\HTTPResponse;
|
||||
use SilverStripe\Control\HTTPResponse_Exception;
|
||||
use SilverStripe\Core\Config\Config;
|
||||
use SilverStripe\Core\Config\Configurable;
|
||||
use SilverStripe\Dev\SapphireTest;
|
||||
use SilverStripe\Security\MemberAuthenticator\MemberAuthenticator;
|
||||
|
||||
/**
|
||||
* Provides an interface to HTTP basic authentication.
|
||||
@ -41,24 +43,29 @@ class BasicAuth
|
||||
* @var String Message that shows in the authentication box.
|
||||
* 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.
|
||||
*
|
||||
* Used by {@link Controller::init()}.
|
||||
*
|
||||
* @throws HTTPResponse_Exception
|
||||
*
|
||||
* @param HTTPRequest $request
|
||||
* @param string $realm
|
||||
* @param string|array $permissionCode Optional
|
||||
* @param boolean $tryUsingSessionLogin If true, then the method with authenticate against the
|
||||
* 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)
|
||||
{
|
||||
$isRunningTests = (class_exists('SilverStripe\\Dev\\SapphireTest', false) && SapphireTest::is_running_test());
|
||||
public static function requireLogin(
|
||||
HTTPRequest $request,
|
||||
$realm,
|
||||
$permissionCode = null,
|
||||
$tryUsingSessionLogin = true
|
||||
) {
|
||||
$isRunningTests = (class_exists(SapphireTest::class, false) && SapphireTest::is_running_test());
|
||||
if (!Security::database_is_ready() || (Director::is_cli() && !$isRunningTests)) {
|
||||
return true;
|
||||
}
|
||||
@ -71,25 +78,37 @@ class BasicAuth
|
||||
* The follow rewrite rule must be in the sites .htaccess file to enable this workaround
|
||||
* RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
|
||||
*/
|
||||
$authHeader = (isset($_SERVER['HTTP_AUTHORIZATION']) ? $_SERVER['HTTP_AUTHORIZATION'] :
|
||||
(isset($_SERVER['REDIRECT_HTTP_AUTHORIZATION']) ? $_SERVER['REDIRECT_HTTP_AUTHORIZATION'] : null));
|
||||
$authHeader = $request->getHeader('Authorization');
|
||||
$matches = array();
|
||||
if ($authHeader && preg_match('/Basic\s+(.*)$/i', $authHeader, $matches)) {
|
||||
list($name, $password) = explode(':', base64_decode($matches[1]));
|
||||
$_SERVER['PHP_AUTH_USER'] = strip_tags($name);
|
||||
$_SERVER['PHP_AUTH_PW'] = strip_tags($password);
|
||||
$request->addHeader('PHP_AUTH_USER', strip_tags($name));
|
||||
$request->addHeader('PHP_AUTH_PW', strip_tags($password));
|
||||
}
|
||||
|
||||
$member = null;
|
||||
if (isset($_SERVER['PHP_AUTH_USER']) && isset($_SERVER['PHP_AUTH_PW'])) {
|
||||
$member = MemberAuthenticator::authenticate(array(
|
||||
'Email' => $_SERVER['PHP_AUTH_USER'],
|
||||
'Password' => $_SERVER['PHP_AUTH_PW'],
|
||||
), null);
|
||||
|
||||
if ($request->getHeader('PHP_AUTH_USER') && $request->getHeader('PHP_AUTH_PW')) {
|
||||
/** @var MemberAuthenticator $authenticator */
|
||||
$authenticators = Security::singleton()->getApplicableAuthenticators(Authenticator::LOGIN);
|
||||
|
||||
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) {
|
||||
$member = Member::currentUser();
|
||||
$member = Security::getCurrentUser();
|
||||
}
|
||||
|
||||
// If we've failed the authentication mechanism, then show the login form
|
||||
@ -97,10 +116,20 @@ class BasicAuth
|
||||
$response = new HTTPResponse(null, 401);
|
||||
$response->addHeader('WWW-Authenticate', "Basic realm=\"$realm\"");
|
||||
|
||||
if (isset($_SERVER['PHP_AUTH_USER'])) {
|
||||
$response->setBody(_t('SilverStripe\\Security\\BasicAuth.ERRORNOTREC', "That username / password isn't recognised"));
|
||||
if ($request->getHeader('PHP_AUTH_USER')) {
|
||||
$response->setBody(
|
||||
_t(
|
||||
'SilverStripe\\Security\\BasicAuth.ERRORNOTREC',
|
||||
"That username / password isn't recognised"
|
||||
)
|
||||
);
|
||||
} 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
|
||||
@ -113,8 +142,13 @@ class BasicAuth
|
||||
$response = new HTTPResponse(null, 401);
|
||||
$response->addHeader('WWW-Authenticate', "Basic realm=\"$realm\"");
|
||||
|
||||
if (isset($_SERVER['PHP_AUTH_USER'])) {
|
||||
$response->setBody(_t('SilverStripe\\Security\\BasicAuth.ERRORNOTADMIN', "That user is not an administrator."));
|
||||
if ($request->getHeader('PHP_AUTH_USER')) {
|
||||
$response->setBody(
|
||||
_t(
|
||||
'SilverStripe\\Security\\BasicAuth.ERRORNOTADMIN',
|
||||
'That user is not an administrator.'
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// 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)
|
||||
{
|
||||
Config::inst()->update('SilverStripe\\Security\\BasicAuth', 'entire_site_protected', $protect);
|
||||
Config::inst()->update('SilverStripe\\Security\\BasicAuth', 'entire_site_protected_code', $code);
|
||||
Config::inst()->update('SilverStripe\\Security\\BasicAuth', 'entire_site_protected_message', $message);
|
||||
static::config()->set('entire_site_protected', $protect);
|
||||
static::config()->set('entire_site_protected_code', $code);
|
||||
static::config()->set('entire_site_protected_message', $message);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -160,9 +194,16 @@ class BasicAuth
|
||||
*/
|
||||
public static function protect_site_if_necessary()
|
||||
{
|
||||
$config = Config::forClass('SilverStripe\\Security\\BasicAuth');
|
||||
if ($config->entire_site_protected) {
|
||||
self::requireLogin($config->entire_site_protected_message, $config->entire_site_protected_code, false);
|
||||
$config = static::config();
|
||||
$request = Controller::curr()->getRequest();
|
||||
if ($config->get('entire_site_protected')) {
|
||||
/** @noinspection ExceptionsAnnotatingAndHandlingInspection */
|
||||
static::requireLogin(
|
||||
$request,
|
||||
$config->get('entire_site_protected_message'),
|
||||
$config->get('entire_site_protected_code'),
|
||||
false
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,38 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace SilverStripe\Security;
|
||||
namespace SilverStripe\Security\MemberAuthenticator;
|
||||
|
||||
use SilverStripe\Control\Controller;
|
||||
use SilverStripe\Control\RequestHandler;
|
||||
use SilverStripe\Forms\CheckboxField;
|
||||
use SilverStripe\Forms\FieldList;
|
||||
use SilverStripe\Forms\FormAction;
|
||||
use SilverStripe\Forms\HiddenField;
|
||||
use SilverStripe\Forms\LiteralField;
|
||||
use SilverStripe\Forms\PasswordField;
|
||||
use SilverStripe\Security\Security;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* @param Controller $controller
|
||||
* @param RequestHandler $controller
|
||||
* @param string $authenticatorClass
|
||||
* @param FieldList $name
|
||||
*/
|
||||
public function __construct(Controller $controller, $authenticatorClass, $name)
|
||||
public function __construct(RequestHandler $controller, $authenticatorClass, $name)
|
||||
{
|
||||
$this->controller = $controller;
|
||||
|
||||
@ -42,7 +34,7 @@ class CMSMemberLoginForm extends LoginForm
|
||||
|
||||
$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()
|
||||
{
|
||||
// Set default fields
|
||||
$fields = new FieldList(
|
||||
$fields = FieldList::create([
|
||||
HiddenField::create("AuthenticationMethod", null, $this->authenticator_class, $this),
|
||||
HiddenField::create('tempid', null, $this->controller->getRequest()->requestVar('tempid')),
|
||||
PasswordField::create("Password", _t('SilverStripe\\Security\\Member.PASSWORD', 'Password')),
|
||||
@ -63,9 +55,9 @@ class CMSMemberLoginForm extends LoginForm
|
||||
_t('SilverStripe\\Security\\CMSMemberLoginForm.BUTTONFORGOTPASSWORD', "Forgot password?")
|
||||
)
|
||||
)
|
||||
);
|
||||
]);
|
||||
|
||||
if (Security::config()->autologin_enabled) {
|
||||
if (Security::config()->get('autologin_enabled')) {
|
||||
$fields->push(CheckboxField::create(
|
||||
"Remember",
|
||||
_t('SilverStripe\\Security\\Member.REMEMBERME', "Remember me next time?")
|
||||
@ -88,8 +80,8 @@ class CMSMemberLoginForm extends LoginForm
|
||||
}
|
||||
|
||||
// Make actions
|
||||
$actions = new FieldList(
|
||||
FormAction::create('dologin', _t('SilverStripe\\Security\\CMSMemberLoginForm.BUTTONLOGIN', "Log back in")),
|
||||
$actions = FieldList::create([
|
||||
FormAction::create('doLogin', _t('SilverStripe\\Security\\CMSMemberLoginForm.BUTTONLOGIN', "Log back in")),
|
||||
LiteralField::create(
|
||||
'doLogout',
|
||||
sprintf(
|
||||
@ -98,14 +90,20 @@ class CMSMemberLoginForm extends LoginForm
|
||||
_t('SilverStripe\\Security\\CMSMemberLoginForm.BUTTONLOGOUT', "Log out")
|
||||
)
|
||||
)
|
||||
);
|
||||
]);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -3,11 +3,11 @@
|
||||
namespace SilverStripe\Security;
|
||||
|
||||
use SilverStripe\Admin\AdminRootController;
|
||||
use SilverStripe\Control\HTTPResponse;
|
||||
use SilverStripe\Core\Convert;
|
||||
use SilverStripe\Control\Director;
|
||||
use SilverStripe\Control\Controller;
|
||||
use SilverStripe\Control\Director;
|
||||
use SilverStripe\Control\HTTPResponse;
|
||||
use SilverStripe\Control\Session;
|
||||
use SilverStripe\Core\Convert;
|
||||
use SilverStripe\ORM\FieldType\DBField;
|
||||
use SilverStripe\View\Requirements;
|
||||
|
||||
@ -22,6 +22,7 @@ class CMSSecurity extends Security
|
||||
);
|
||||
|
||||
private static $allowed_actions = array(
|
||||
'login',
|
||||
'LoginForm',
|
||||
'success'
|
||||
);
|
||||
@ -41,12 +42,27 @@ class CMSSecurity extends Security
|
||||
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)
|
||||
{
|
||||
/** @skipUpgrade */
|
||||
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
|
||||
*
|
||||
@ -57,6 +73,7 @@ class CMSSecurity extends Security
|
||||
if ($tempid = $this->getRequest()->requestVar('tempid')) {
|
||||
return Member::member_from_tempid($tempid);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -78,7 +95,7 @@ class CMSSecurity extends Security
|
||||
public function getTitle()
|
||||
{
|
||||
// Check if logged in already
|
||||
if (Member::currentUserID()) {
|
||||
if (Security::getCurrentUser()) {
|
||||
return _t('SilverStripe\\Security\\CMSSecurity.SUCCESS', 'Success');
|
||||
}
|
||||
|
||||
@ -129,6 +146,7 @@ setTimeout(function(){top.location.href = "$loginURLJS";}, 0);
|
||||
PHP
|
||||
);
|
||||
$this->setResponse($response);
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
@ -142,19 +160,6 @@ PHP
|
||||
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
|
||||
*
|
||||
@ -163,28 +168,11 @@ PHP
|
||||
public static function enabled()
|
||||
{
|
||||
// Disable shortcut
|
||||
if (!static::config()->reauth_enabled) {
|
||||
if (!static::config()->get('reauth_enabled')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Count all cms-supported methods
|
||||
$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);
|
||||
return count(Security::singleton()->getApplicableAuthenticators(Authenticator::CMS_LOGIN)) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -195,7 +183,7 @@ PHP
|
||||
public function success()
|
||||
{
|
||||
// Ensure member is properly logged in
|
||||
if (!Member::currentUserID() || !class_exists(AdminRootController::class)) {
|
||||
if (!Security::getCurrentUser() || !class_exists(AdminRootController::class)) {
|
||||
return $this->redirectToExternalLogin();
|
||||
}
|
||||
|
||||
@ -204,7 +192,7 @@ PHP
|
||||
$backURLs = array(
|
||||
$this->getRequest()->requestVar('BackURL'),
|
||||
Session::get('BackURL'),
|
||||
Director::absoluteURL(AdminRootController::config()->url_base, true),
|
||||
Director::absoluteURL(AdminRootController::config()->get('url_base'), true),
|
||||
);
|
||||
$backURL = null;
|
||||
foreach ($backURLs as $backURL) {
|
||||
@ -217,7 +205,7 @@ PHP
|
||||
$controller = $controller->customise(array(
|
||||
'Content' => _t(
|
||||
'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>',
|
||||
'Login message displayed in the cms popup once a user has re-authenticated themselves',
|
||||
array('link' => Convert::raw2att($backURL))
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -4,24 +4,24 @@ namespace SilverStripe\Security;
|
||||
|
||||
use SilverStripe\Admin\SecurityAdmin;
|
||||
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\TextareaField;
|
||||
use SilverStripe\Forms\Tab;
|
||||
use SilverStripe\Forms\TabSet;
|
||||
use SilverStripe\Forms\FieldList;
|
||||
use SilverStripe\Forms\LiteralField;
|
||||
use SilverStripe\Forms\ListboxField;
|
||||
use SilverStripe\Forms\HiddenField;
|
||||
use SilverStripe\Forms\HTMLEditor\HTMLEditorConfig;
|
||||
use SilverStripe\Forms\GridField\GridFieldConfig_RelationEditor;
|
||||
use SilverStripe\Forms\Form;
|
||||
use SilverStripe\Forms\GridField\GridField;
|
||||
use SilverStripe\Forms\GridField\GridFieldAddExistingAutocompleter;
|
||||
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\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\DataObject;
|
||||
use SilverStripe\ORM\DataQuery;
|
||||
@ -29,7 +29,6 @@ use SilverStripe\ORM\HasManyList;
|
||||
use SilverStripe\ORM\Hierarchy\Hierarchy;
|
||||
use SilverStripe\ORM\ManyManyList;
|
||||
use SilverStripe\ORM\UnsavedRelationList;
|
||||
use SilverStripe\View\Requirements;
|
||||
|
||||
/**
|
||||
* A security group.
|
||||
@ -95,6 +94,7 @@ class Group extends DataObject
|
||||
$doSet = new ArrayList();
|
||||
|
||||
$children = Group::get()->filter("ParentID", $this->ID);
|
||||
/** @var Group $child */
|
||||
foreach ($children as $child) {
|
||||
$doSet->push($child);
|
||||
$doSet->merge($child->getAllChildren());
|
||||
@ -159,7 +159,7 @@ class Group extends DataObject
|
||||
$detailForm = $config->getComponentByType(GridFieldDetailForm::class);
|
||||
$detailForm
|
||||
->setValidator(Member_Validator::create())
|
||||
->setItemEditFormCallback(function ($form, $component) use ($group) {
|
||||
->setItemEditFormCallback(function ($form) use ($group) {
|
||||
/** @var Form $form */
|
||||
$record = $form->getRecord();
|
||||
$groupsField = $form->Fields()->dataFieldByName('DirectGroups');
|
||||
@ -369,9 +369,9 @@ class Group extends DataObject
|
||||
{
|
||||
$parent = $this;
|
||||
$items = [];
|
||||
while (isset($parent) && $parent instanceof Group) {
|
||||
while ($parent instanceof Group) {
|
||||
$items[] = $parent->ID;
|
||||
$parent = $parent->Parent;
|
||||
$parent = $parent->getParent();
|
||||
}
|
||||
return $items;
|
||||
}
|
||||
@ -395,12 +395,14 @@ class Group extends DataObject
|
||||
->sort('"Sort"');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getTreeTitle()
|
||||
{
|
||||
if ($this->hasMethod('alternateTreeTitle')) {
|
||||
return $this->alternateTreeTitle();
|
||||
}
|
||||
return htmlspecialchars($this->Title, ENT_QUOTES);
|
||||
$title = htmlspecialchars($this->Title, ENT_QUOTES);
|
||||
$this->extend('updateTreeTitle', $title);
|
||||
return $title;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -476,7 +478,7 @@ class Group extends DataObject
|
||||
public function canEdit($member = null)
|
||||
{
|
||||
if (!$member) {
|
||||
$member = Member::currentUser();
|
||||
$member = Security::getCurrentUser();
|
||||
}
|
||||
|
||||
// extended access checks
|
||||
@ -512,7 +514,7 @@ class Group extends DataObject
|
||||
public function canView($member = null)
|
||||
{
|
||||
if (!$member) {
|
||||
$member = Member::currentUser();
|
||||
$member = Security::getCurrentUser();
|
||||
}
|
||||
|
||||
// extended access checks
|
||||
@ -534,7 +536,7 @@ class Group extends DataObject
|
||||
public function canDelete($member = null)
|
||||
{
|
||||
if (!$member) {
|
||||
$member = Member::currentUser();
|
||||
$member = Security::getCurrentUser();
|
||||
}
|
||||
|
||||
// extended access checks
|
||||
|
@ -2,8 +2,8 @@
|
||||
|
||||
namespace SilverStripe\Security;
|
||||
|
||||
use SilverStripe\ORM\DataObject;
|
||||
use SilverStripe\Dev\CsvBulkLoader;
|
||||
use SilverStripe\ORM\DataObject;
|
||||
|
||||
/**
|
||||
* @todo Migrate Permission->Arg and Permission->Type values
|
||||
@ -15,12 +15,8 @@ class GroupCsvBulkLoader extends CsvBulkLoader
|
||||
'Code' => 'Code',
|
||||
);
|
||||
|
||||
public function __construct($objectClass = null)
|
||||
public function __construct($objectClass = Group::class)
|
||||
{
|
||||
if (!$objectClass) {
|
||||
$objectClass = 'SilverStripe\\Security\\Group';
|
||||
}
|
||||
|
||||
parent::__construct($objectClass);
|
||||
}
|
||||
|
||||
|
30
src/Security/IdentityStore.php
Normal file
30
src/Security/IdentityStore.php
Normal 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);
|
||||
}
|
@ -158,13 +158,13 @@ class InheritedPermissions implements PermissionChecker
|
||||
{
|
||||
switch ($permission) {
|
||||
case self::EDIT:
|
||||
$this->canEditMultiple($ids, Member::currentUser(), false);
|
||||
$this->canEditMultiple($ids, Security::getCurrentUser(), false);
|
||||
break;
|
||||
case self::VIEW:
|
||||
$this->canViewMultiple($ids, Member::currentUser(), false);
|
||||
$this->canViewMultiple($ids, Security::getCurrentUser(), false);
|
||||
break;
|
||||
case self::DELETE:
|
||||
$this->canDeleteMultiple($ids, Member::currentUser(), false);
|
||||
$this->canDeleteMultiple($ids, Security::getCurrentUser(), false);
|
||||
break;
|
||||
default:
|
||||
throw new InvalidArgumentException("Invalid permission type $permission");
|
||||
|
@ -2,7 +2,6 @@
|
||||
|
||||
namespace SilverStripe\Security;
|
||||
|
||||
use SilverStripe\Core\Injector\Injector;
|
||||
use SilverStripe\Forms\FieldList;
|
||||
use SilverStripe\Forms\Form;
|
||||
|
||||
|
@ -3,16 +3,16 @@
|
||||
namespace SilverStripe\Security;
|
||||
|
||||
use IntlDateFormatter;
|
||||
use InvalidArgumentException;
|
||||
use SilverStripe\Admin\LeftAndMain;
|
||||
use SilverStripe\CMS\Controllers\CMSMain;
|
||||
use SilverStripe\Control\Cookie;
|
||||
use SilverStripe\Control\Controller;
|
||||
use SilverStripe\Control\Director;
|
||||
use SilverStripe\Control\Email\Email;
|
||||
use SilverStripe\Control\Email\Mailer;
|
||||
use SilverStripe\Control\Session;
|
||||
use SilverStripe\Core\Convert;
|
||||
use SilverStripe\Core\Injector\Injector;
|
||||
use SilverStripe\Dev\SapphireTest;
|
||||
use SilverStripe\Dev\Deprecation;
|
||||
use SilverStripe\Dev\TestMailer;
|
||||
use SilverStripe\Forms\ConfirmedPasswordField;
|
||||
use SilverStripe\Forms\DropdownField;
|
||||
@ -20,20 +20,17 @@ use SilverStripe\Forms\FieldList;
|
||||
use SilverStripe\Forms\HTMLEditor\HTMLEditorConfig;
|
||||
use SilverStripe\Forms\ListboxField;
|
||||
use SilverStripe\i18n\i18n;
|
||||
use SilverStripe\MSSQL\MSSQLDatabase;
|
||||
use SilverStripe\ORM\ArrayList;
|
||||
use SilverStripe\ORM\DataList;
|
||||
use SilverStripe\ORM\DataObject;
|
||||
use SilverStripe\ORM\DB;
|
||||
use SilverStripe\ORM\FieldType\DBDatetime;
|
||||
use SilverStripe\ORM\HasManyList;
|
||||
use SilverStripe\ORM\ManyManyList;
|
||||
use SilverStripe\ORM\SS_List;
|
||||
use SilverStripe\ORM\Map;
|
||||
use SilverStripe\ORM\SS_List;
|
||||
use SilverStripe\ORM\ValidationException;
|
||||
use SilverStripe\ORM\ValidationResult;
|
||||
use SilverStripe\View\SSViewer;
|
||||
use SilverStripe\View\TemplateGlobalProvider;
|
||||
use DateTime;
|
||||
|
||||
/**
|
||||
* The member class which represents the users of the system
|
||||
@ -56,30 +53,31 @@ use DateTime;
|
||||
* @property int $FailedLoginCount
|
||||
* @property string $DateFormat
|
||||
* @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(
|
||||
'FirstName' => 'Varchar',
|
||||
'Surname' => 'Varchar',
|
||||
'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
|
||||
'TempIDExpired' => 'Datetime', // Expiry of temp login
|
||||
'Password' => 'Varchar(160)',
|
||||
'AutoLoginHash' => 'Varchar(160)', // Used to auto-login the user on password reset
|
||||
'AutoLoginExpired' => 'Datetime',
|
||||
'FirstName' => 'Varchar',
|
||||
'Surname' => 'Varchar',
|
||||
'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
|
||||
'TempIDExpired' => 'Datetime', // Expiry of temp login
|
||||
'Password' => 'Varchar(160)',
|
||||
'AutoLoginHash' => 'Varchar(160)', // Used to auto-login the user on password reset
|
||||
'AutoLoginExpired' => 'Datetime',
|
||||
// This is an arbitrary code pointing to a PasswordEncryptor instance,
|
||||
// not an actual encryption algorithm.
|
||||
// Warning: Never change this field after its the first password hashing without
|
||||
// providing a new cleartext password as well.
|
||||
'PasswordEncryption' => "Varchar(50)",
|
||||
'Salt' => 'Varchar(50)',
|
||||
'PasswordExpiry' => 'Date',
|
||||
'LockedOutUntil' => 'Datetime',
|
||||
'Locale' => 'Varchar(6)',
|
||||
'Salt' => 'Varchar(50)',
|
||||
'PasswordExpiry' => 'Date',
|
||||
'LockedOutUntil' => 'Datetime',
|
||||
'Locale' => 'Varchar(6)',
|
||||
// handled in registerFailedLogin(), only used if $lock_out_after_incorrect_logins is set
|
||||
'FailedLoginCount' => 'Int',
|
||||
'FailedLoginCount' => 'Int',
|
||||
);
|
||||
|
||||
private static $belongs_many_many = array(
|
||||
@ -87,7 +85,7 @@ class Member extends DataObject implements TemplateGlobalProvider
|
||||
);
|
||||
|
||||
private static $has_many = array(
|
||||
'LoggedPasswords' => MemberPassword::class,
|
||||
'LoggedPasswords' => MemberPassword::class,
|
||||
'RememberLoginHashes' => RememberLoginHash::class,
|
||||
);
|
||||
|
||||
@ -191,6 +189,12 @@ class Member extends DataObject implements TemplateGlobalProvider
|
||||
*/
|
||||
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
|
||||
* @var Int Number of incorrect logins after which
|
||||
@ -276,7 +280,7 @@ class Member extends DataObject implements TemplateGlobalProvider
|
||||
|
||||
// Find member
|
||||
/** @skipUpgrade */
|
||||
$admin = Member::get()
|
||||
$admin = static::get()
|
||||
->filter('Email', Security::default_admin_username())
|
||||
->first();
|
||||
if (!$admin) {
|
||||
@ -284,7 +288,7 @@ class Member extends DataObject implements TemplateGlobalProvider
|
||||
// persistent logins in the database. See Security::setDefaultAdmin().
|
||||
// Set 'Email' to identify this as the default admin
|
||||
$admin = Member::create();
|
||||
$admin->FirstName = _t(__CLASS__.'.DefaultAdminFirstname', 'Default Admin');
|
||||
$admin->FirstName = _t(__CLASS__ . '.DefaultAdminFirstname', 'Default Admin');
|
||||
$admin->Email = Security::default_admin_username();
|
||||
$admin->write();
|
||||
}
|
||||
@ -323,14 +327,15 @@ class Member extends DataObject implements TemplateGlobalProvider
|
||||
|
||||
// Check a password is set on this member
|
||||
if (empty($this->Password) && $this->exists()) {
|
||||
$result->addError(_t(__CLASS__.'.NoPassword', 'There is no password on this member.'));
|
||||
$result->addError(_t(__CLASS__ . '.NoPassword', 'There is no password on this member.'));
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
$e = PasswordEncryptor::create_for_algorithm($this->PasswordEncryption);
|
||||
if (!$e->check($this->Password, $password, $this->Salt, $this)) {
|
||||
$result->addError(_t(
|
||||
__CLASS__.'.ERRORWRONGCRED',
|
||||
__CLASS__ . '.ERRORWRONGCRED',
|
||||
'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()) {
|
||||
$result->addError(
|
||||
_t(
|
||||
__CLASS__.'.ERRORLOCKEDOUT2',
|
||||
__CLASS__ . '.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' => $this->config()->lock_out_delay_mins)
|
||||
array('count' => static::config()->get('lock_out_delay_mins'))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$this->extend('canLogIn', $result);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
@ -387,36 +393,10 @@ class Member extends DataObject implements TemplateGlobalProvider
|
||||
if (!$this->LockedOutUntil) {
|
||||
return false;
|
||||
}
|
||||
|
||||
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.
|
||||
*
|
||||
@ -443,63 +423,48 @@ class Member extends DataObject implements TemplateGlobalProvider
|
||||
if (!$this->PasswordExpiry) {
|
||||
return false;
|
||||
}
|
||||
|
||||
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');
|
||||
}
|
||||
|
||||
self::session_regenerate_id();
|
||||
|
||||
Session::set("loggedInAs", $this->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);
|
||||
}
|
||||
|
||||
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');
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Called after a member is logged in via session/cookie/etc
|
||||
*/
|
||||
public function afterMemberLoggedIn()
|
||||
{
|
||||
// Clear the incorrect log-in count
|
||||
$this->registerSuccessfulLogin();
|
||||
|
||||
$this->LockedOutUntil = null;
|
||||
$this->LockedOutUntil = null;
|
||||
|
||||
$this->regenerateTempID();
|
||||
|
||||
$this->write();
|
||||
|
||||
// Audit logging hook
|
||||
$this->extend('memberLoggedIn');
|
||||
$this->extend('afterMemberLoggedIn');
|
||||
}
|
||||
|
||||
/**
|
||||
@ -511,9 +476,10 @@ class Member extends DataObject implements TemplateGlobalProvider
|
||||
public function regenerateTempID()
|
||||
{
|
||||
$generator = new RandomGenerator();
|
||||
$lifetime = self::config()->get('temp_id_lifetime');
|
||||
$this->TempIDHash = $generator->randomToken('sha1');
|
||||
$this->TempIDExpired = self::config()->temp_id_lifetime
|
||||
? date('Y-m-d H:i:s', strtotime(DBDatetime::now()->getValue()) + self::config()->temp_id_lifetime)
|
||||
$this->TempIDExpired = $lifetime
|
||||
? date('Y-m-d H:i:s', strtotime(DBDatetime::now()->getValue()) + $lifetime)
|
||||
: null;
|
||||
$this->write();
|
||||
}
|
||||
@ -523,15 +489,20 @@ class Member extends DataObject implements TemplateGlobalProvider
|
||||
* has a database record of the same ID. If there is
|
||||
* 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
|
||||
*/
|
||||
public static function logged_in_session_exists()
|
||||
{
|
||||
if ($id = Member::currentUserID()) {
|
||||
if ($member = DataObject::get_by_id(Member::class, $id)) {
|
||||
if ($member->exists()) {
|
||||
return true;
|
||||
}
|
||||
Deprecation::notice(
|
||||
'5.0.0',
|
||||
'This method is deprecated and now does not add value. Please use Security::getCurrentUser()'
|
||||
);
|
||||
|
||||
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
|
||||
*
|
||||
* 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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use Security::setCurrentUser(null) or an IdentityStore
|
||||
* Logs this member out.
|
||||
*/
|
||||
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');
|
||||
|
||||
Session::clear("loggedInAs");
|
||||
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();
|
||||
|
||||
Injector::inst()->get(IdentityStore::class)->logOut(Controller::curr()->getRequest());
|
||||
// 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.
|
||||
$e = PasswordEncryptor::create_for_algorithm($this->PasswordEncryption);
|
||||
|
||||
return $e->encrypt($string, $this->Salt);
|
||||
}
|
||||
|
||||
@ -723,6 +591,7 @@ class Member extends DataObject implements TemplateGlobalProvider
|
||||
{
|
||||
$hash = $this->encryptWithUserSettings($autologinToken);
|
||||
$member = self::member_from_autologinhash($hash, false);
|
||||
|
||||
return (bool)$member;
|
||||
}
|
||||
|
||||
@ -738,13 +607,13 @@ class Member extends DataObject implements TemplateGlobalProvider
|
||||
public static function member_from_autologinhash($hash, $login = false)
|
||||
{
|
||||
/** @var Member $member */
|
||||
$member = Member::get()->filter([
|
||||
'AutoLoginHash' => $hash,
|
||||
$member = static::get()->filter([
|
||||
'AutoLoginHash' => $hash,
|
||||
'AutoLoginExpired:GreaterThan' => DBDatetime::now()->getValue(),
|
||||
])->first();
|
||||
|
||||
if ($login && $member) {
|
||||
$member->logIn();
|
||||
Injector::inst()->get(IdentityStore::class)->logIn($member);
|
||||
}
|
||||
|
||||
return $member;
|
||||
@ -758,11 +627,12 @@ class Member extends DataObject implements TemplateGlobalProvider
|
||||
*/
|
||||
public static function member_from_tempid($tempid)
|
||||
{
|
||||
$members = Member::get()
|
||||
$members = static::get()
|
||||
->filter('TempIDHash', $tempid);
|
||||
|
||||
// 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());
|
||||
}
|
||||
|
||||
@ -773,6 +643,8 @@ class Member extends DataObject implements TemplateGlobalProvider
|
||||
* 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.
|
||||
*
|
||||
* @todo possibly move this to an extension
|
||||
*
|
||||
* @return FieldList Returns a {@link FieldList} containing the fields for
|
||||
* the member form.
|
||||
*/
|
||||
@ -788,11 +660,12 @@ class Member extends DataObject implements TemplateGlobalProvider
|
||||
i18n::getSources()->getKnownLocales()
|
||||
));
|
||||
|
||||
$fields->removeByName(static::config()->hidden_fields);
|
||||
$fields->removeByName(static::config()->get('hidden_fields'));
|
||||
$fields->removeByName('FailedLoginCount');
|
||||
|
||||
|
||||
$this->extend('updateMemberFormFields', $fields);
|
||||
|
||||
return $fields;
|
||||
}
|
||||
|
||||
@ -805,7 +678,7 @@ class Member extends DataObject implements TemplateGlobalProvider
|
||||
{
|
||||
$editingPassword = $this->isInDB();
|
||||
$label = $editingPassword
|
||||
? _t(__CLASS__.'.EDIT_PASSWORD', 'New Password')
|
||||
? _t(__CLASS__ . '.EDIT_PASSWORD', 'New Password')
|
||||
: $this->fieldLabel('Password');
|
||||
/** @var ConfirmedPasswordField $password */
|
||||
$password = ConfirmedPasswordField::create(
|
||||
@ -817,12 +690,13 @@ class Member extends DataObject implements TemplateGlobalProvider
|
||||
);
|
||||
|
||||
// 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->setCanBeEmpty(true);
|
||||
$this->extend('updateMemberPasswordField', $password);
|
||||
|
||||
return $password;
|
||||
}
|
||||
|
||||
@ -850,24 +724,20 @@ class Member extends DataObject implements TemplateGlobalProvider
|
||||
/**
|
||||
* Returns the current logged in user
|
||||
*
|
||||
* @deprecated 5.0.0 use Security::getCurrentUser()
|
||||
*
|
||||
* @return Member
|
||||
*/
|
||||
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 DataObject::get_by_id(Member::class, $id);
|
||||
}
|
||||
return Security::getCurrentUser();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* 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.
|
||||
* Set to null or 0 to act as a logged out user.
|
||||
* @param $callback
|
||||
* @param callable $callback
|
||||
*/
|
||||
public static function actAs($member, $callback)
|
||||
{
|
||||
$id = ($member instanceof Member ? $member->ID : $member) ?: 0;
|
||||
$previousID = static::$overrideID;
|
||||
static::$overrideID = $id;
|
||||
$previousUser = Security::getCurrentUser();
|
||||
|
||||
// Transform ID to member
|
||||
if (is_numeric($member)) {
|
||||
$member = DataObject::get_by_id(Member::class, $member);
|
||||
}
|
||||
Security::setCurrentUser($member);
|
||||
|
||||
try {
|
||||
return $callback();
|
||||
} finally {
|
||||
static::$overrideID = $previousID;
|
||||
Security::setCurrentUser($previousUser);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
public static function currentUserID()
|
||||
{
|
||||
if (isset(static::$overrideID)) {
|
||||
return static::$overrideID;
|
||||
}
|
||||
Deprecation::notice(
|
||||
'5.0.0',
|
||||
'This method is deprecated. Please use Security::getCurrentUser() or an IdentityStore'
|
||||
);
|
||||
|
||||
$id = Session::get("loggedInAs");
|
||||
if (!$id && !self::$_already_tried_to_auto_log_in) {
|
||||
self::autoLogin();
|
||||
$id = Session::get("loggedInAs");
|
||||
if ($member = Security::getCurrentUser()) {
|
||||
return $member->ID;
|
||||
} else {
|
||||
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.
|
||||
*
|
||||
* @return string Returns a random password.
|
||||
*/
|
||||
/**
|
||||
* Generate a random password, with randomiser to kick in if there's no words file on the
|
||||
* filesystem.
|
||||
*
|
||||
* @return string Returns a random password.
|
||||
*/
|
||||
public static function create_new_password()
|
||||
{
|
||||
$words = Security::config()->uninherited('word_list');
|
||||
@ -932,16 +805,17 @@ class Member extends DataObject implements TemplateGlobalProvider
|
||||
$words = file($words);
|
||||
|
||||
list($usec, $sec) = explode(' ', microtime());
|
||||
srand($sec + ((float) $usec * 100000));
|
||||
mt_srand($sec + ((float)$usec * 100000));
|
||||
|
||||
$word = trim($words[rand(0, sizeof($words)-1)]);
|
||||
$number = rand(10, 999);
|
||||
$word = trim($words[random_int(0, count($words) - 1)]);
|
||||
$number = random_int(10, 999);
|
||||
|
||||
return $word . $number;
|
||||
} else {
|
||||
$random = rand();
|
||||
$random = mt_rand();
|
||||
$string = md5($random);
|
||||
$output = substr($string, 0, 8);
|
||||
|
||||
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.
|
||||
// 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.
|
||||
$identifierField = Member::config()->unique_identifier_field;
|
||||
$identifierField = Member::config()->get('unique_identifier_field');
|
||||
if ($this->$identifierField) {
|
||||
// Note: Same logic as Member_Validator class
|
||||
$filter = [
|
||||
@ -971,12 +845,12 @@ class Member extends DataObject implements TemplateGlobalProvider
|
||||
|
||||
if ($existingRecord) {
|
||||
throw new ValidationException(_t(
|
||||
__CLASS__.'.ValidationIdentifierFailed',
|
||||
__CLASS__ . '.ValidationIdentifierFailed',
|
||||
'Can\'t overwrite existing member #{id} with identical identifier ({name} = {value}))',
|
||||
'Values in brackets show "fieldname = value", usually denoting an existing email address',
|
||||
array(
|
||||
'id' => $existingRecord->ID,
|
||||
'name' => $identifierField,
|
||||
'id' => $existingRecord->ID,
|
||||
'name' => $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.
|
||||
// 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)
|
||||
&& $this->isChanged('Password')
|
||||
&& $this->record['Password']
|
||||
&& $this->config()->notify_password_change
|
||||
&& static::config()->get('notify_password_change')
|
||||
) {
|
||||
Email::create()
|
||||
->setHTMLTemplate('SilverStripe\\Control\\Email\\ChangePasswordEmail')
|
||||
->setData($this)
|
||||
->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();
|
||||
}
|
||||
|
||||
@ -1002,7 +877,7 @@ class Member extends DataObject implements TemplateGlobalProvider
|
||||
// Note that this only works with cleartext passwords, as we can't rehash
|
||||
// existing passwords.
|
||||
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)
|
||||
$this->Salt = '';
|
||||
// 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->Salt,
|
||||
($this->PasswordEncryption) ?
|
||||
$this->PasswordEncryption : Security::config()->password_encryption_algorithm,
|
||||
$this->PasswordEncryption : Security::config()->get('password_encryption_algorithm'),
|
||||
$this
|
||||
);
|
||||
|
||||
@ -1022,8 +897,8 @@ class Member extends DataObject implements TemplateGlobalProvider
|
||||
// If we haven't manually set a password expiry
|
||||
if (!$this->isChanged('PasswordExpiry')) {
|
||||
// then set it for us
|
||||
if (self::config()->password_expiry_days) {
|
||||
$this->PasswordExpiry = date('Y-m-d', time() + 86400 * self::config()->password_expiry_days);
|
||||
if (static::config()->get('password_expiry_days')) {
|
||||
$this->PasswordExpiry = date('Y-m-d', time() + 86400 * static::config()->get('password_expiry_days'));
|
||||
} else {
|
||||
$this->PasswordExpiry = null;
|
||||
}
|
||||
@ -1044,7 +919,7 @@ class Member extends DataObject implements TemplateGlobalProvider
|
||||
|
||||
Permission::reset();
|
||||
|
||||
if ($this->isChanged('Password')) {
|
||||
if ($this->isChanged('Password') && static::config()->get('password_logging_enabled')) {
|
||||
MemberPassword::log($this);
|
||||
}
|
||||
}
|
||||
@ -1068,6 +943,7 @@ class Member extends DataObject implements TemplateGlobalProvider
|
||||
$password->delete();
|
||||
$password->destroy();
|
||||
}
|
||||
|
||||
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
|
||||
$adminGroups = Permission::get_groups_by_permission('ADMIN');
|
||||
$adminGroupIDs = ($adminGroups) ? $adminGroups->column('ID') : array();
|
||||
return count(array_intersect($ids, $adminGroupIDs)) == 0;
|
||||
$adminGroups = Permission::get_groups_by_permission('ADMIN');
|
||||
$adminGroupIDs = ($adminGroups) ? $adminGroups->column('ID') : array();
|
||||
|
||||
return count(array_intersect($ids, $adminGroupIDs)) == 0;
|
||||
}
|
||||
|
||||
|
||||
@ -1131,7 +1008,7 @@ class Member extends DataObject implements TemplateGlobalProvider
|
||||
} elseif ($group instanceof Group) {
|
||||
$groupCheckObj = $group;
|
||||
} 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) {
|
||||
@ -1199,10 +1076,17 @@ class Member extends DataObject implements TemplateGlobalProvider
|
||||
*/
|
||||
public static function set_title_columns($columns, $sep = ' ')
|
||||
{
|
||||
Deprecation::notice('5.0', 'Use Member.title_format config instead');
|
||||
if (!is_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 -----------------------------------//
|
||||
@ -1219,13 +1103,14 @@ class Member extends DataObject implements TemplateGlobalProvider
|
||||
*/
|
||||
public function getTitle()
|
||||
{
|
||||
$format = $this->config()->title_format;
|
||||
$format = static::config()->get('title_format');
|
||||
if ($format) {
|
||||
$values = array();
|
||||
foreach ($format['columns'] as $col) {
|
||||
$values[] = $this->getField($col);
|
||||
}
|
||||
return join($format['sep'], $values);
|
||||
|
||||
return implode($format['sep'], $values);
|
||||
}
|
||||
if ($this->getField('ID') === 0) {
|
||||
return $this->getField('Surname');
|
||||
@ -1250,25 +1135,24 @@ class Member extends DataObject implements TemplateGlobalProvider
|
||||
*/
|
||||
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
|
||||
$format = static::config()->title_format;
|
||||
$format = static::config()->get('title_format');
|
||||
if (!$format) {
|
||||
$format = [
|
||||
'columns' => ['Surname', 'FirstName'],
|
||||
'sep' => ' ',
|
||||
'sep' => ' ',
|
||||
];
|
||||
}
|
||||
|
||||
$columnsWithTablename = array();
|
||||
$columnsWithTablename = array();
|
||||
foreach ($format['columns'] as $column) {
|
||||
$columnsWithTablename[] = static::getSchema()->sqlColumnForField(__CLASS__, $column);
|
||||
}
|
||||
|
||||
$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) {
|
||||
return $locale;
|
||||
}
|
||||
|
||||
return i18n::get_locale();
|
||||
}
|
||||
|
||||
@ -1415,16 +1300,18 @@ class Member extends DataObject implements TemplateGlobalProvider
|
||||
|
||||
// No groups, return all Members
|
||||
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();
|
||||
// This is a bit ineffective, but follow the ORM style
|
||||
/** @var Group $group */
|
||||
foreach (Group::get()->byIDs($groupIDList) as $group) {
|
||||
$membersList->merge($group->Members());
|
||||
}
|
||||
|
||||
$membersList->removeDuplicates('ID');
|
||||
|
||||
return $membersList->map();
|
||||
}
|
||||
|
||||
@ -1446,7 +1333,7 @@ class Member extends DataObject implements TemplateGlobalProvider
|
||||
return ArrayList::create()->map();
|
||||
}
|
||||
|
||||
if (!$groups || $groups->Count() == 0) {
|
||||
if (count($groups) == 0) {
|
||||
$perms = array('ADMIN', 'CMS_ACCESS_AssetAdmin');
|
||||
|
||||
if (class_exists(CMSMain::class)) {
|
||||
@ -1479,7 +1366,7 @@ class Member extends DataObject implements TemplateGlobalProvider
|
||||
}
|
||||
|
||||
/** @skipUpgrade */
|
||||
$members = Member::get()
|
||||
$members = static::get()
|
||||
->innerJoin("Group_Members", '"Group_Members"."MemberID" = "Member"."ID"')
|
||||
->innerJoin("Group", '"Group"."ID" = "Group_Members"."GroupID"');
|
||||
if ($groupIDList) {
|
||||
@ -1539,12 +1426,12 @@ class Member extends DataObject implements TemplateGlobalProvider
|
||||
|
||||
$mainFields->replaceField('Locale', new DropdownField(
|
||||
"Locale",
|
||||
_t(__CLASS__.'.INTERFACELANG', "Interface Language", 'Language of the CMS'),
|
||||
_t(__CLASS__ . '.INTERFACELANG', "Interface Language", 'Language of the CMS'),
|
||||
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');
|
||||
}
|
||||
|
||||
@ -1570,7 +1457,7 @@ class Member extends DataObject implements TemplateGlobalProvider
|
||||
->setSource($groupsMap)
|
||||
->setAttribute(
|
||||
'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['FirstName'] = _t(__CLASS__.'.FIRSTNAME', 'First Name');
|
||||
$labels['Surname'] = _t(__CLASS__.'.SURNAME', 'Surname');
|
||||
$labels['FirstName'] = _t(__CLASS__ . '.FIRSTNAME', 'First Name');
|
||||
$labels['Surname'] = _t(__CLASS__ . '.SURNAME', 'Surname');
|
||||
/** @skipUpgrade */
|
||||
$labels['Email'] = _t(__CLASS__.'.EMAIL', 'Email');
|
||||
$labels['Password'] = _t(__CLASS__.'.db_Password', 'Password');
|
||||
$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['Locale'] = _t(__CLASS__.'.db_Locale', 'Interface Locale');
|
||||
$labels['Email'] = _t(__CLASS__ . '.EMAIL', 'Email');
|
||||
$labels['Password'] = _t(__CLASS__ . '.db_Password', 'Password');
|
||||
$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['Locale'] = _t(__CLASS__ . '.db_Locale', 'Interface Locale');
|
||||
if ($includerelations) {
|
||||
$labels['Groups'] = _t(
|
||||
__CLASS__.'.belongs_many_many_Groups',
|
||||
__CLASS__ . '.belongs_many_many_Groups',
|
||||
'Groups',
|
||||
'Security Groups this member belongs to'
|
||||
);
|
||||
}
|
||||
|
||||
return $labels;
|
||||
}
|
||||
|
||||
@ -1639,7 +1527,7 @@ class Member extends DataObject implements TemplateGlobalProvider
|
||||
{
|
||||
//get member
|
||||
if (!$member) {
|
||||
$member = Member::currentUser();
|
||||
$member = Security::getCurrentUser();
|
||||
}
|
||||
//check for extensions, we do this first as they can overrule everything
|
||||
$extended = $this->extendedCan(__FUNCTION__, $member);
|
||||
@ -1655,6 +1543,7 @@ class Member extends DataObject implements TemplateGlobalProvider
|
||||
if ($this->ID == $member->ID) {
|
||||
return true;
|
||||
}
|
||||
|
||||
//standard check
|
||||
return Permission::checkMember($member, 'CMS_ACCESS_SecurityAdmin');
|
||||
}
|
||||
@ -1670,7 +1559,7 @@ class Member extends DataObject implements TemplateGlobalProvider
|
||||
{
|
||||
//get member
|
||||
if (!$member) {
|
||||
$member = Member::currentUser();
|
||||
$member = Security::getCurrentUser();
|
||||
}
|
||||
//check for extensions, we do this first as they can overrule everything
|
||||
$extended = $this->extendedCan(__FUNCTION__, $member);
|
||||
@ -1691,9 +1580,11 @@ class Member extends DataObject implements TemplateGlobalProvider
|
||||
if ($this->ID == $member->ID) {
|
||||
return true;
|
||||
}
|
||||
|
||||
//standard check
|
||||
return Permission::checkMember($member, 'CMS_ACCESS_SecurityAdmin');
|
||||
}
|
||||
|
||||
/**
|
||||
* Users can edit their own record.
|
||||
* 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)
|
||||
{
|
||||
if (!$member) {
|
||||
$member = Member::currentUser();
|
||||
$member = Security::getCurrentUser();
|
||||
}
|
||||
//check for extensions, we do this first as they can overrule everything
|
||||
$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
|
||||
// deleting a member who has more privileges (e.g. a non-Admin deleting an Admin)
|
||||
if (Permission::checkMember($this, 'ADMIN')) {
|
||||
if (! Permission::checkMember($member, 'ADMIN')) {
|
||||
if (!Permission::checkMember($member, 'ADMIN')) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
//standard check
|
||||
return Permission::checkMember($member, 'CMS_ACCESS_SecurityAdmin');
|
||||
}
|
||||
@ -1782,13 +1674,14 @@ class Member extends DataObject implements TemplateGlobalProvider
|
||||
*/
|
||||
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
|
||||
$this->FailedLoginCount = $this->FailedLoginCount + 1;
|
||||
|
||||
if ($this->FailedLoginCount >= self::config()->lock_out_after_incorrect_logins) {
|
||||
$lockoutMins = self::config()->lock_out_delay_mins;
|
||||
$this->LockedOutUntil = date('Y-m-d H:i:s', DBDatetime::now()->getTimestamp() + $lockoutMins*60);
|
||||
if ($this->FailedLoginCount >= $lockOutAfterCount) {
|
||||
$lockoutMins = self::config()->get('lock_out_delay_mins');
|
||||
$this->LockedOutUntil = date('Y-m-d H:i:s', DBDatetime::now()->getTimestamp() + $lockoutMins * 60);
|
||||
$this->FailedLoginCount = 0;
|
||||
}
|
||||
}
|
||||
@ -1801,7 +1694,7 @@ class Member extends DataObject implements TemplateGlobalProvider
|
||||
*/
|
||||
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
|
||||
$this->FailedLoginCount = 0;
|
||||
$this->write();
|
||||
@ -1834,12 +1727,4 @@ class Member extends DataObject implements TemplateGlobalProvider
|
||||
// If can't find a suitable editor, just default to cms
|
||||
return $currentName ? $currentName : 'cms';
|
||||
}
|
||||
|
||||
public static function get_template_global_variables()
|
||||
{
|
||||
return array(
|
||||
'CurrentMember' => 'currentUser',
|
||||
'currentUser',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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__;
|
||||
}
|
||||
}
|
@ -1,27 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace SilverStripe\Security;
|
||||
namespace SilverStripe\Security\MemberAuthenticator;
|
||||
|
||||
use SilverStripe\Control\HTTPResponse;
|
||||
use SilverStripe\Core\Convert;
|
||||
use SilverStripe\Security\CMSSecurity;
|
||||
use SilverStripe\Security\Security;
|
||||
|
||||
class CMSMemberLoginHandler extends MemberLoginHandler
|
||||
class CMSLoginHandler extends LoginHandler
|
||||
{
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
private static $allowed_actions = [
|
||||
'LoginForm'
|
||||
];
|
||||
|
||||
return $this->redirectBackToForm();
|
||||
/**
|
||||
* Return the CMSMemberLoginForm form
|
||||
*/
|
||||
public function loginForm()
|
||||
{
|
||||
return CMSMemberLoginForm::create(
|
||||
$this,
|
||||
get_class($this->authenticator),
|
||||
'LoginForm'
|
||||
);
|
||||
}
|
||||
|
||||
public function redirectBackToForm()
|
||||
@ -75,13 +76,12 @@ PHP
|
||||
/**
|
||||
* Send user to the right location after login
|
||||
*
|
||||
* @param array $data
|
||||
* @return HTTPResponse
|
||||
*/
|
||||
protected function logInUserAndRedirect($data)
|
||||
protected function redirectAfterSuccessfulLogin()
|
||||
{
|
||||
// Check password expiry
|
||||
if (Member::currentUser()->isPasswordExpired()) {
|
||||
if (Security::getCurrentUser()->isPasswordExpired()) {
|
||||
// Redirect the user to the external password change form if necessary
|
||||
return $this->redirectToChangePassword();
|
||||
}
|
45
src/Security/MemberAuthenticator/CMSMemberAuthenticator.php
Normal file
45
src/Security/MemberAuthenticator/CMSMemberAuthenticator.php
Normal 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);
|
||||
}
|
||||
}
|
81
src/Security/MemberAuthenticator/ChangePasswordForm.php
Normal file
81
src/Security/MemberAuthenticator/ChangePasswordForm.php
Normal 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;
|
||||
}
|
||||
}
|
308
src/Security/MemberAuthenticator/ChangePasswordHandler.php
Normal file
308
src/Security/MemberAuthenticator/ChangePasswordHandler.php
Normal 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);
|
||||
}
|
||||
}
|
245
src/Security/MemberAuthenticator/CookieAuthenticationHandler.php
Normal file
245
src/Security/MemberAuthenticator/CookieAuthenticationHandler.php
Normal 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());
|
||||
}
|
||||
}
|
257
src/Security/MemberAuthenticator/LoginHandler.php
Normal file
257
src/Security/MemberAuthenticator/LoginHandler.php
Normal 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));
|
||||
}
|
||||
}
|
64
src/Security/MemberAuthenticator/LogoutHandler.php
Normal file
64
src/Security/MemberAuthenticator/LogoutHandler.php
Normal 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;
|
||||
}
|
||||
}
|
45
src/Security/MemberAuthenticator/LostPasswordForm.php
Normal file
45
src/Security/MemberAuthenticator/LostPasswordForm.php
Normal 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')
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
230
src/Security/MemberAuthenticator/LostPasswordHandler.php
Normal file
230
src/Security/MemberAuthenticator/LostPasswordHandler.php
Normal 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();
|
||||
}
|
||||
}
|
199
src/Security/MemberAuthenticator/MemberAuthenticator.php
Normal file
199
src/Security/MemberAuthenticator/MemberAuthenticator.php
Normal 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);
|
||||
}
|
||||
}
|
@ -1,19 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace SilverStripe\Security;
|
||||
namespace SilverStripe\Security\MemberAuthenticator;
|
||||
|
||||
use SilverStripe\Control\Director;
|
||||
use SilverStripe\Control\RequestHandler;
|
||||
use SilverStripe\Control\Session;
|
||||
use SilverStripe\Control\Controller;
|
||||
use SilverStripe\Forms\HiddenField;
|
||||
use SilverStripe\Forms\CheckboxField;
|
||||
use SilverStripe\Forms\FieldList;
|
||||
use SilverStripe\Forms\FormAction;
|
||||
use SilverStripe\Forms\TextField;
|
||||
use SilverStripe\Forms\PasswordField;
|
||||
use SilverStripe\Forms\CheckboxField;
|
||||
use SilverStripe\Forms\HiddenField;
|
||||
use SilverStripe\Forms\LiteralField;
|
||||
use SilverStripe\Forms\PasswordField;
|
||||
use SilverStripe\Forms\RequiredFields;
|
||||
use SilverStripe\Forms\TextField;
|
||||
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;
|
||||
|
||||
/**
|
||||
@ -26,7 +30,7 @@ use SilverStripe\View\Requirements;
|
||||
* allowing extensions to "veto" execution by returning FALSE.
|
||||
* 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
|
||||
*
|
||||
* @config
|
||||
* @var array
|
||||
*/
|
||||
private static $required_fields;
|
||||
private static $required_fields = [
|
||||
'Email',
|
||||
'Password',
|
||||
];
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @skipUpgrade
|
||||
* @param Controller $controller The parent controller, necessary to
|
||||
* @param RequestHandler $controller The parent controller, necessary to
|
||||
* create the appropriate form action tag.
|
||||
* @param string $authenticatorClass Authenticator for this LoginForm
|
||||
* @param string $name The method on the controller that will return this
|
||||
@ -69,6 +78,7 @@ class MemberLoginForm extends LoginForm
|
||||
$checkCurrentUser = true
|
||||
) {
|
||||
|
||||
$this->controller = $controller;
|
||||
$this->authenticator_class = $authenticatorClass;
|
||||
|
||||
$customCSS = project() . '/css/member_login.css';
|
||||
@ -76,18 +86,17 @@ class MemberLoginForm extends LoginForm
|
||||
Requirements::css($customCSS);
|
||||
}
|
||||
|
||||
if ($controller->request->getVar('BackURL')) {
|
||||
$backURL = $controller->request->getVar('BackURL');
|
||||
} else {
|
||||
$backURL = Session::get('BackURL');
|
||||
}
|
||||
|
||||
if ($checkCurrentUser && Member::currentUser() && Member::logged_in_session_exists()) {
|
||||
if ($checkCurrentUser && Security::getCurrentUser()) {
|
||||
// @todo find a more elegant way to handle this
|
||||
$logoutAction = Security::logout_url();
|
||||
$fields = FieldList::create(
|
||||
HiddenField::create("AuthenticationMethod", null, $this->authenticator_class, $this)
|
||||
HiddenField::create('AuthenticationMethod', null, $this->authenticator_class, $this)
|
||||
);
|
||||
$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 {
|
||||
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
|
||||
$this->setFormMethod('POST', true);
|
||||
|
||||
parent::__construct($controller, $name, $fields, $actions);
|
||||
|
||||
if (isset($logoutAction)) {
|
||||
$this->setFormAction($logoutAction);
|
||||
}
|
||||
$this->setValidator(RequiredFields::create(self::config()->get('required_fields')));
|
||||
}
|
||||
|
||||
@ -117,6 +125,12 @@ class MemberLoginForm extends LoginForm
|
||||
*/
|
||||
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);
|
||||
$fields = FieldList::create(
|
||||
HiddenField::create("AuthenticationMethod", null, $this->authenticator_class, $this),
|
||||
@ -128,14 +142,14 @@ class MemberLoginForm extends LoginForm
|
||||
);
|
||||
$emailField->setAttribute('autofocus', 'true');
|
||||
|
||||
if (Security::config()->remember_username) {
|
||||
if (Security::config()->get('remember_username')) {
|
||||
$emailField->setValue(Session::get('SessionForms.MemberLoginForm.Email'));
|
||||
} else {
|
||||
// Some browsers won't respect this attribute unless it's added to the form
|
||||
$this->setAttribute('autocomplete', 'off');
|
||||
$emailField->setAttribute('autocomplete', 'off');
|
||||
}
|
||||
if (Security::config()->autologin_enabled) {
|
||||
if (Security::config()->get('autologin_enabled')) {
|
||||
$fields->push(
|
||||
CheckboxField::create(
|
||||
"Remember",
|
||||
@ -150,6 +164,10 @@ class MemberLoginForm extends LoginForm
|
||||
);
|
||||
}
|
||||
|
||||
if (isset($backURL)) {
|
||||
$fields->push(HiddenField::create('BackURL', 'BackURL', $backURL));
|
||||
}
|
||||
|
||||
return $fields;
|
||||
}
|
||||
|
||||
@ -161,7 +179,7 @@ class MemberLoginForm extends LoginForm
|
||||
protected function getFormActions()
|
||||
{
|
||||
$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(
|
||||
'forgotPassword',
|
||||
'<p id="ForgotPassword"><a href="' . Security::lost_password_url() . '">'
|
||||
@ -177,7 +195,7 @@ class MemberLoginForm extends LoginForm
|
||||
parent::restoreFormState();
|
||||
|
||||
$forceMessage = Session::get('MemberLoginForm.force_message');
|
||||
if (($member = Member::currentUser()) && !$forceMessage) {
|
||||
if (($member = Security::getCurrentUser()) && !$forceMessage) {
|
||||
$message = _t(
|
||||
'SilverStripe\\Security\\Member.LOGGEDINAS',
|
||||
"You're logged in as {name}.",
|
||||
@ -194,14 +212,6 @@ class MemberLoginForm extends LoginForm
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return MemberLoginHandler
|
||||
*/
|
||||
protected function buildRequestHandler()
|
||||
{
|
||||
return MemberLoginHandler::create($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* The name of this login form, to display in the frontend
|
||||
* Replaces Authenticator::get_name()
|
@ -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());
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
@ -109,7 +109,7 @@ class Member_GroupSet extends ManyManyList
|
||||
{
|
||||
$id = $this->getForeignID();
|
||||
if ($id) {
|
||||
return DataObject::get_by_id('SilverStripe\\Security\\Member', $id);
|
||||
return DataObject::get_by_id(Member::class, $id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,8 +2,8 @@
|
||||
|
||||
namespace SilverStripe\Security;
|
||||
|
||||
use SilverStripe\Forms\RequiredFields;
|
||||
use SilverStripe\Forms\GridField\GridFieldDetailForm_ItemRequest;
|
||||
use SilverStripe\Forms\RequiredFields;
|
||||
|
||||
/**
|
||||
* Member Validator
|
||||
|
@ -2,8 +2,8 @@
|
||||
|
||||
namespace SilverStripe\Security;
|
||||
|
||||
use SilverStripe\Core\Config\Config;
|
||||
use ReflectionClass;
|
||||
use SilverStripe\Core\Config\Config;
|
||||
|
||||
/**
|
||||
* Allows pluggable password encryption.
|
||||
|
@ -6,9 +6,9 @@ use SilverStripe\Core\ClassInfo;
|
||||
use SilverStripe\Core\Resettable;
|
||||
use SilverStripe\Dev\TestOnly;
|
||||
use SilverStripe\i18n\i18nEntityProvider;
|
||||
use SilverStripe\ORM\DB;
|
||||
use SilverStripe\ORM\DataObject;
|
||||
use SilverStripe\ORM\ArrayList;
|
||||
use SilverStripe\ORM\DataObject;
|
||||
use SilverStripe\ORM\DB;
|
||||
use SilverStripe\ORM\SS_List;
|
||||
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)
|
||||
{
|
||||
if (!$member) {
|
||||
if (!Member::currentUserID()) {
|
||||
if (!Security::getCurrentUser()) {
|
||||
return false;
|
||||
}
|
||||
$member = Member::currentUserID();
|
||||
$member = Security::getCurrentUser();
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
if (!$member) {
|
||||
$memberID = $member = Member::currentUserID();
|
||||
} else {
|
||||
$memberID = (is_object($member)) ? $member->ID : $member;
|
||||
$member = Security::getCurrentUser();
|
||||
}
|
||||
$memberID = ($member instanceof Member) ? $member->ID : $member;
|
||||
|
||||
if (!$memberID) {
|
||||
return false;
|
||||
@ -347,7 +346,7 @@ class Permission extends DataObject implements TemplateGlobalProvider, Resettabl
|
||||
{
|
||||
// Default to current member, with session-caching
|
||||
if (!$memberID) {
|
||||
$member = Member::currentUser();
|
||||
$member = Security::getCurrentUser();
|
||||
if ($member && isset($_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.
|
||||
*
|
||||
* @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
|
||||
* permission.
|
||||
*/
|
||||
|
@ -2,14 +2,13 @@
|
||||
|
||||
namespace SilverStripe\Security;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use SilverStripe\Core\Config\Config;
|
||||
use SilverStripe\Forms\FormField;
|
||||
use SilverStripe\ORM\DataObject;
|
||||
use SilverStripe\ORM\SS_List;
|
||||
use SilverStripe\ORM\ArrayList;
|
||||
use SilverStripe\ORM\DataObject;
|
||||
use SilverStripe\ORM\DataObjectInterface;
|
||||
use SilverStripe\View\Requirements;
|
||||
use InvalidArgumentException;
|
||||
use SilverStripe\ORM\SS_List;
|
||||
|
||||
/**
|
||||
* Shows a categorized list of available permissions (through {@link Permission::get_codes()}).
|
||||
|
@ -2,10 +2,10 @@
|
||||
|
||||
namespace SilverStripe\Security;
|
||||
|
||||
use SilverStripe\ORM\FieldType\DBDatetime;
|
||||
use SilverStripe\ORM\DataObject;
|
||||
use DateTime;
|
||||
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"
|
||||
@ -16,7 +16,8 @@ use DateInterval;
|
||||
* is discarded as well.
|
||||
*
|
||||
* @property string $DeviceID
|
||||
* @property string $RememberLoginHash
|
||||
* @property string $ExpiryDate
|
||||
* @property string $Hash
|
||||
* @method Member Member()
|
||||
*/
|
||||
class RememberLoginHash extends DataObject
|
||||
|
83
src/Security/RequestAuthenticationHandler.php
Normal file
83
src/Security/RequestAuthenticationHandler.php
Normal 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);
|
||||
}
|
||||
}
|
@ -2,33 +2,32 @@
|
||||
|
||||
namespace SilverStripe\Security;
|
||||
|
||||
use Page;
|
||||
use LogicException;
|
||||
use SilverStripe\CMS\Controllers\ContentController;
|
||||
use Page;
|
||||
use SilverStripe\CMS\Controllers\ModelAsController;
|
||||
use SilverStripe\Control\Controller;
|
||||
use SilverStripe\Control\Director;
|
||||
use SilverStripe\Control\HTTPRequest;
|
||||
use SilverStripe\Control\HTTPResponse;
|
||||
use SilverStripe\Control\HTTPResponse_Exception;
|
||||
use SilverStripe\Control\RequestHandler;
|
||||
use SilverStripe\Control\Session;
|
||||
use SilverStripe\Core\ClassInfo;
|
||||
use SilverStripe\Core\Config\Config;
|
||||
use SilverStripe\Core\Convert;
|
||||
use SilverStripe\Core\Injector\Injector;
|
||||
use SilverStripe\Dev\Deprecation;
|
||||
use SilverStripe\Dev\TestOnly;
|
||||
use SilverStripe\Forms\EmailField;
|
||||
use SilverStripe\Forms\FieldList;
|
||||
use SilverStripe\Forms\Form;
|
||||
use SilverStripe\Forms\FormAction;
|
||||
use SilverStripe\ORM\ArrayList;
|
||||
use SilverStripe\ORM\DB;
|
||||
use SilverStripe\ORM\DataModel;
|
||||
use SilverStripe\ORM\DataObject;
|
||||
use SilverStripe\ORM\DB;
|
||||
use SilverStripe\ORM\FieldType\DBField;
|
||||
use SilverStripe\ORM\FieldType\DBHTMLText;
|
||||
use SilverStripe\ORM\ValidationResult;
|
||||
use SilverStripe\View\ArrayData;
|
||||
use SilverStripe\View\SSViewer;
|
||||
use SilverStripe\View\TemplateGlobalProvider;
|
||||
use Exception;
|
||||
use SilverStripe\View\ViewableData_Customised;
|
||||
use Subsite;
|
||||
|
||||
/**
|
||||
@ -46,13 +45,10 @@ class Security extends Controller implements TemplateGlobalProvider
|
||||
'passwordsent',
|
||||
'changepassword',
|
||||
'ping',
|
||||
'LoginForm',
|
||||
'ChangePasswordForm',
|
||||
'LostPasswordForm',
|
||||
);
|
||||
|
||||
/**
|
||||
* Default user name. Only used in dev-mode by {@link setDefaultAdmin()}
|
||||
* Default user name. {@link setDefaultAdmin()}
|
||||
*
|
||||
* @var string
|
||||
* @see setDefaultAdmin()
|
||||
@ -60,7 +56,7 @@ class Security extends Controller implements TemplateGlobalProvider
|
||||
protected static $default_username;
|
||||
|
||||
/**
|
||||
* Default password. Only used in dev-mode by {@link setDefaultAdmin()}
|
||||
* Default password. {@link setDefaultAdmin()}
|
||||
*
|
||||
* @var string
|
||||
* @see setDefaultAdmin()
|
||||
@ -74,7 +70,7 @@ class Security extends Controller implements TemplateGlobalProvider
|
||||
* @config
|
||||
* @var bool
|
||||
*/
|
||||
protected static $strict_path_checking = false;
|
||||
private static $strict_path_checking = false;
|
||||
|
||||
/**
|
||||
* 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
|
||||
* 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
|
||||
* @var bool
|
||||
@ -118,7 +114,7 @@ class Security extends Controller implements TemplateGlobalProvider
|
||||
private static $template = 'BlankPage';
|
||||
|
||||
/**
|
||||
* Template thats used to render the pages.
|
||||
* Template that is used to render the pages.
|
||||
*
|
||||
* @var string
|
||||
* @config
|
||||
@ -157,7 +153,7 @@ class Security extends Controller implements TemplateGlobalProvider
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private static $login_url = "Security/login";
|
||||
private static $login_url = 'Security/login';
|
||||
|
||||
/**
|
||||
* The default logout URL
|
||||
@ -166,7 +162,7 @@ class Security extends Controller implements TemplateGlobalProvider
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private static $logout_url = "Security/logout";
|
||||
private static $logout_url = 'Security/logout';
|
||||
|
||||
/**
|
||||
* The default lost password URL
|
||||
@ -175,7 +171,7 @@ class Security extends Controller implements TemplateGlobalProvider
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private static $lost_password_url = "Security/lostpassword";
|
||||
private static $lost_password_url = 'Security/lostpassword';
|
||||
|
||||
/**
|
||||
* 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()}
|
||||
* 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
|
||||
@ -214,7 +210,107 @@ class Security extends Controller implements TemplateGlobalProvider
|
||||
*
|
||||
* @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
|
||||
@ -252,14 +348,19 @@ class Security extends Controller implements TemplateGlobalProvider
|
||||
if (Director::is_ajax()) {
|
||||
$response = ($controller) ? $controller->getResponse() : new HTTPResponse();
|
||||
$response->setStatusCode(403);
|
||||
if (!Member::currentUser()) {
|
||||
$response->setBody(_t('SilverStripe\\CMS\\Controllers\\ContentController.NOTLOGGEDIN', 'Not logged in'));
|
||||
$response->setStatusDescription(_t('SilverStripe\\CMS\\Controllers\\ContentController.NOTLOGGEDIN', 'Not logged in'));
|
||||
// Tell the CMS to allow re-aunthentication
|
||||
if (!static::getCurrentUser()) {
|
||||
$response->setBody(
|
||||
_t('SilverStripe\\CMS\\Controllers\\ContentController.NOTLOGGEDIN', 'Not logged in')
|
||||
);
|
||||
$response->setStatusDescription(
|
||||
_t('SilverStripe\\CMS\\Controllers\\ContentController.NOTLOGGEDIN', 'Not logged in')
|
||||
);
|
||||
// Tell the CMS to allow re-authentication
|
||||
if (CMSSecurity::enabled()) {
|
||||
$response->addHeader('X-Reauthenticate', '1');
|
||||
}
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
@ -269,15 +370,15 @@ class Security extends Controller implements TemplateGlobalProvider
|
||||
$messageSet = $configMessageSet;
|
||||
} else {
|
||||
$messageSet = array(
|
||||
'default' => _t(
|
||||
'default' => _t(
|
||||
'SilverStripe\\Security\\Security.NOTEPAGESECURED',
|
||||
"That page is secured. Enter your credentials below and we will send "
|
||||
. "you right along."
|
||||
. "you right along."
|
||||
),
|
||||
'alreadyLoggedIn' => _t(
|
||||
'SilverStripe\\Security\\Security.ALREADYLOGGEDIN',
|
||||
"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."
|
||||
)
|
||||
);
|
||||
@ -288,7 +389,7 @@ class Security extends Controller implements TemplateGlobalProvider
|
||||
$messageSet = array('default' => $messageSet);
|
||||
}
|
||||
|
||||
$member = Member::currentUser();
|
||||
$member = static::getCurrentUser();
|
||||
|
||||
// Work out the right message to show
|
||||
if ($member && $member->exists()) {
|
||||
@ -303,12 +404,8 @@ class Security extends Controller implements TemplateGlobalProvider
|
||||
$message = $messageSet['default'];
|
||||
}
|
||||
|
||||
// Somewhat hackish way to render a login form with an error message.
|
||||
$me = new Security();
|
||||
$form = $me->LoginForm();
|
||||
$form->sessionMessage($message, ValidationResult::TYPE_WARNING);
|
||||
Session::set('MemberLoginForm.force_message', 1);
|
||||
$loginResponse = $me->login();
|
||||
static::singleton()->setLoginMessage($message, ValidationResult::TYPE_WARNING);
|
||||
$loginResponse = static::singleton()->login();
|
||||
if ($loginResponse instanceof HTTPResponse) {
|
||||
return $loginResponse;
|
||||
}
|
||||
@ -322,7 +419,7 @@ class Security extends Controller implements TemplateGlobalProvider
|
||||
$message = $messageSet['default'];
|
||||
}
|
||||
|
||||
static::setLoginMessage($message, ValidationResult::TYPE_WARNING);
|
||||
static::singleton()->setLoginMessage($message, ValidationResult::TYPE_WARNING);
|
||||
|
||||
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();
|
||||
|
||||
// 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
|
||||
self::$currentUser = $currentUser;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the selected authenticator for this request
|
||||
*
|
||||
* @return string Class name of Authenticator
|
||||
* @throws LogicException
|
||||
* @return null|Member
|
||||
*/
|
||||
protected function getAuthenticator()
|
||||
public static function getCurrentUser()
|
||||
{
|
||||
$authenticator = $this->getRequest()->requestVar('AuthenticationMethod');
|
||||
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');
|
||||
return self::$currentUser;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* 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();
|
||||
foreach ($authenticators as $authenticator) {
|
||||
$forms[] = $authenticator::get_login_form($this);
|
||||
}
|
||||
|
||||
return $forms;
|
||||
return array_map(
|
||||
function (Authenticator $authenticator) {
|
||||
return [
|
||||
$authenticator->getLoginHandler($this->Link())->loginForm()
|
||||
];
|
||||
},
|
||||
$this->getApplicableAuthenticators()
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -436,6 +497,12 @@ class Security extends Controller implements TemplateGlobalProvider
|
||||
/**
|
||||
* 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.
|
||||
* - If it's false, the code calling logout() is
|
||||
* responsible for sending the user where-ever
|
||||
@ -444,14 +511,34 @@ class Security extends Controller implements TemplateGlobalProvider
|
||||
*/
|
||||
public function logout($redirect = true)
|
||||
{
|
||||
$member = Member::currentUser();
|
||||
if ($member) {
|
||||
$member->logOut();
|
||||
$this->extend('beforeMemberLoggedOut');
|
||||
$member = static::getCurrentUser();
|
||||
|
||||
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())) {
|
||||
return $this->redirectBack();
|
||||
}
|
||||
|
||||
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
|
||||
// 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.
|
||||
if ($this->getRequest()->requestVar('BackURL')
|
||||
&& !$this->getLoginMessage()
|
||||
&& ($member = Member::currentUser())
|
||||
if (!$this->getLoginMessage()
|
||||
&& ($member = static::getCurrentUser())
|
||||
&& $member->exists()
|
||||
&& $this->getRequest()->requestVar('BackURL')
|
||||
) {
|
||||
return $this->redirectBack();
|
||||
}
|
||||
@ -511,25 +598,24 @@ class Security extends Controller implements TemplateGlobalProvider
|
||||
|
||||
// Create new instance of page holder
|
||||
/** @var Page $holderPage */
|
||||
$holderPage = new $pageClass;
|
||||
$holderPage = Injector::inst()->create($pageClass);
|
||||
$holderPage->Title = $title;
|
||||
/** @skipUpgrade */
|
||||
$holderPage->URLSegment = 'Security';
|
||||
// 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();
|
||||
/** @var ContentController $controller */
|
||||
$controller = $controllerClass::create($holderPage);
|
||||
$controller = ModelAsController::controller_for($holderPage);
|
||||
$controller->setDataModel($this->model);
|
||||
$controller->doInit();
|
||||
|
||||
return $controller;
|
||||
}
|
||||
|
||||
/**
|
||||
* Combine the given forms into a formset with a tabbed interface
|
||||
*
|
||||
* @param array $forms List of LoginForm instances
|
||||
* @param array|Form[] $forms
|
||||
* @return string
|
||||
*/
|
||||
protected function generateLoginFormSet($forms)
|
||||
@ -537,6 +623,7 @@ class Security extends Controller implements TemplateGlobalProvider
|
||||
$viewData = new ArrayData(array(
|
||||
'Forms' => new ArrayList($forms),
|
||||
));
|
||||
|
||||
return $viewData->renderWith(
|
||||
$this->getTemplatesFor('MultiAuthenticatorLogin')
|
||||
);
|
||||
@ -561,6 +648,7 @@ class Security extends Controller implements TemplateGlobalProvider
|
||||
if ($messageCast !== ValidationResult::CAST_HTML) {
|
||||
$message = Convert::raw2xml($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 $messageCast Message cast. One of ValidationResult::CAST_*
|
||||
*/
|
||||
public static function setLoginMessage(
|
||||
public function setLoginMessage(
|
||||
$message,
|
||||
$messageType = ValidationResult::TYPE_WARNING,
|
||||
$messageCast = ValidationResult::CAST_TEXT
|
||||
) {
|
||||
Session::set("Security.Message.message", $message);
|
||||
Session::set("Security.Message.type", $messageType);
|
||||
Session::set("Security.Message.cast", $messageCast);
|
||||
Session::set('Security.Message.message', $message);
|
||||
Session::set('Security.Message.type', $messageType);
|
||||
Session::set('Security.Message.cast', $messageCast);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -586,7 +674,7 @@ class Security extends Controller implements TemplateGlobalProvider
|
||||
*/
|
||||
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.
|
||||
* 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
|
||||
if ($response = $this->preLogin()) {
|
||||
return $response;
|
||||
}
|
||||
$authName = null;
|
||||
|
||||
// Get response handler
|
||||
$controller = $this->getResponseController(_t('SilverStripe\\Security\\Security.LOGIN', 'Log in'));
|
||||
if (!$request) {
|
||||
$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 (($response = $controller->getResponse()) && $response->isFinished()) {
|
||||
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.
|
||||
$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.
|
||||
static::clearLoginMessage();
|
||||
|
||||
// only display tabs when more than one authenticator is provided
|
||||
// to save bandwidth and reduce the amount of custom styling needed
|
||||
if (count($forms) > 1) {
|
||||
$content = $this->generateLoginFormSet($forms);
|
||||
} else {
|
||||
$content = $forms[0]->forTemplate();
|
||||
if ($message) {
|
||||
$messageResult = [
|
||||
'Content' => DBField::create_field('HTMLFragment', $message),
|
||||
'Message' => DBField::create_field('HTMLFragment', $message),
|
||||
'MessageType' => $messageType
|
||||
];
|
||||
$fragments = array_merge($fragments, $messageResult);
|
||||
}
|
||||
|
||||
// Finally, customise the controller to add any form messages and the form.
|
||||
$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')
|
||||
);
|
||||
return $controller->customise($fragments)->renderWith($templates);
|
||||
}
|
||||
|
||||
public function basicauthlogin()
|
||||
{
|
||||
$member = BasicAuth::requireLogin("SilverStripe login", 'ADMIN');
|
||||
$member->logIn();
|
||||
$member = BasicAuth::requireLogin($this->getRequest(), 'SilverStripe login', 'ADMIN');
|
||||
static::setCurrentUser($member);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -663,113 +861,20 @@ class Security extends Controller implements TemplateGlobalProvider
|
||||
*/
|
||||
public function lostpassword()
|
||||
{
|
||||
$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;
|
||||
$handlers = [];
|
||||
$authenticators = $this->getApplicableAuthenticators(Authenticator::RESET_PASSWORD);
|
||||
/** @var Authenticator $authenticator */
|
||||
foreach ($authenticators as $authenticator) {
|
||||
$handlers[] = $authenticator->getLostPasswordHandler(
|
||||
Controller::join_links($this->Link(), '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 $this->delegateToMultipleHandlers(
|
||||
$handlers,
|
||||
_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()
|
||||
{
|
||||
$controller = $this->getResponseController(_t('SilverStripe\\Security\\Security.CHANGEPASSWORDHEADER', 'Change your password'));
|
||||
|
||||
// if the controller calls Director::redirect(), this will break early
|
||||
if (($response = $controller->getResponse()) && $response->isFinished()) {
|
||||
return $response;
|
||||
/** @var array|Authenticator[] $authenticators */
|
||||
$authenticators = $this->getApplicableAuthenticators(Authenticator::CHANGE_PASSWORD);
|
||||
$handlers = [];
|
||||
foreach ($authenticators as $authenticator) {
|
||||
$handlers[] = $authenticator->getChangePasswordHandler($this->Link('changepassword'));
|
||||
}
|
||||
|
||||
// Extract the member from the URL.
|
||||
/** @var Member $member */
|
||||
$member = null;
|
||||
if (isset($_REQUEST['m'])) {
|
||||
$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'));
|
||||
return $this->delegateToMultipleHandlers(
|
||||
$handlers,
|
||||
_t('SilverStripe\\Security\\Security.CHANGEPASSWORDHEADER', 'Change your password'),
|
||||
$this->getTemplatesFor('changepassword')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory method for the lost password form
|
||||
* Create a link to the password reset form.
|
||||
*
|
||||
* @skipUpgrade
|
||||
* @return ChangePasswordForm Returns the lost password 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 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)
|
||||
{
|
||||
$templates = SSViewer::get_templates_by_class(static::class, "_{$action}", __CLASS__);
|
||||
|
||||
return array_merge(
|
||||
$templates,
|
||||
[
|
||||
@ -906,12 +960,7 @@ class Security extends Controller implements TemplateGlobalProvider
|
||||
*/
|
||||
public static function findAnAdministrator()
|
||||
{
|
||||
// coupling to subsites module
|
||||
$origSubsite = null;
|
||||
if (is_callable('Subsite::changeSubsite')) {
|
||||
$origSubsite = Subsite::currentSubsiteID();
|
||||
Subsite::changeSubsite(0);
|
||||
}
|
||||
static::singleton()->extend('beforeFindAdministrator');
|
||||
|
||||
/** @var Member $member */
|
||||
$member = null;
|
||||
@ -919,19 +968,13 @@ class Security extends Controller implements TemplateGlobalProvider
|
||||
// find a group with ADMIN permission
|
||||
$adminGroup = Permission::get_groups_by_permission('ADMIN')->first();
|
||||
|
||||
if (is_callable('Subsite::changeSubsite')) {
|
||||
Subsite::changeSubsite($origSubsite);
|
||||
}
|
||||
|
||||
if ($adminGroup) {
|
||||
$member = $adminGroup->Members()->First();
|
||||
}
|
||||
|
||||
if (!$adminGroup) {
|
||||
Group::singleton()->requireDefaultRecords();
|
||||
$adminGroup = Permission::get_groups_by_permission('ADMIN')->first();
|
||||
}
|
||||
|
||||
$member = $adminGroup->Members()->First();
|
||||
|
||||
if (!$member) {
|
||||
Member::singleton()->requireDefaultRecords();
|
||||
$member = Permission::get_members_by_permission('ADMIN')->first();
|
||||
@ -953,6 +996,8 @@ class Security extends Controller implements TemplateGlobalProvider
|
||||
->add($member);
|
||||
}
|
||||
|
||||
static::singleton()->extend('afterFindAdministrator');
|
||||
|
||||
return $member;
|
||||
}
|
||||
|
||||
@ -987,6 +1032,7 @@ class Security extends Controller implements TemplateGlobalProvider
|
||||
|
||||
self::$default_username = $username;
|
||||
self::$default_password = $password;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -1075,8 +1121,8 @@ class Security extends Controller implements TemplateGlobalProvider
|
||||
$salt = ($salt) ? $salt : $e->salt($password);
|
||||
|
||||
return array(
|
||||
'password' => $e->encrypt($password, $salt, $member),
|
||||
'salt' => $salt,
|
||||
'password' => $e->encrypt($password, $salt, $member),
|
||||
'salt' => $salt,
|
||||
'algorithm' => $algorithm,
|
||||
'encryptor' => $e
|
||||
);
|
||||
@ -1137,6 +1183,25 @@ class Security extends Controller implements TemplateGlobalProvider
|
||||
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
|
||||
* @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
|
||||
* 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)
|
||||
{
|
||||
@ -1208,9 +1273,11 @@ class Security extends Controller implements TemplateGlobalProvider
|
||||
public static function get_template_global_variables()
|
||||
{
|
||||
return array(
|
||||
"LoginURL" => "login_url",
|
||||
"LogoutURL" => "logout_url",
|
||||
"LoginURL" => "login_url",
|
||||
"LogoutURL" => "logout_url",
|
||||
"LostPasswordURL" => "lost_password_url",
|
||||
"CurrentMember" => "getCurrentUser",
|
||||
"currentUser" => "getCurrentUser"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -2,11 +2,11 @@
|
||||
|
||||
namespace SilverStripe\Security;
|
||||
|
||||
use SilverStripe\Control\Controller;
|
||||
use SilverStripe\Control\HTTPRequest;
|
||||
use SilverStripe\Control\Session;
|
||||
use SilverStripe\Core\Config\Configurable;
|
||||
use SilverStripe\Core\Injector\Injectable;
|
||||
use SilverStripe\Control\Session;
|
||||
use SilverStripe\Control\Controller;
|
||||
use SilverStripe\Forms\FieldList;
|
||||
use SilverStripe\Forms\HiddenField;
|
||||
use SilverStripe\View\TemplateGlobalProvider;
|
||||
@ -61,11 +61,11 @@ class SecurityToken implements TemplateGlobalProvider
|
||||
protected $name = null;
|
||||
|
||||
/**
|
||||
* @param $name
|
||||
* @param string $name
|
||||
*/
|
||||
public function __construct($name = null)
|
||||
{
|
||||
$this->name = ($name) ? $name : self::get_default_name();
|
||||
$this->name = $name ?: self::get_default_name();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -312,7 +312,7 @@ class ViewableData implements IteratorAggregate
|
||||
*/
|
||||
public function castingHelper($field)
|
||||
{
|
||||
$specs = $this->config()->get('casting');
|
||||
$specs = static::config()->get('casting');
|
||||
if (isset($specs[$field])) {
|
||||
return $specs[$field];
|
||||
}
|
||||
|
@ -6,7 +6,7 @@ Feature: Log in
|
||||
|
||||
Scenario: Bad login
|
||||
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
|
||||
Given I am logged in with "ADMIN" permissions
|
||||
|
@ -23,6 +23,7 @@ use SilverStripe\Dev\Deprecation;
|
||||
use SilverStripe\Dev\FunctionalTest;
|
||||
use SilverStripe\ORM\DataModel;
|
||||
use SilverStripe\Security\Member;
|
||||
use SilverStripe\Security\Security;
|
||||
use SilverStripe\View\SSViewer;
|
||||
|
||||
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'
|
||||
);
|
||||
|
||||
$this->session()->inst_set('loggedInAs', $adminUser->ID);
|
||||
Security::setCurrentUser($adminUser);
|
||||
$response = $this->get("AccessSecuredController/templateaction");
|
||||
$this->assertEquals(
|
||||
200,
|
||||
@ -211,8 +212,8 @@ class ControllerTest extends FunctionalTest
|
||||
'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'
|
||||
);
|
||||
$this->session()->inst_set('loggedInAs', null);
|
||||
|
||||
Security::setCurrentUser(null);
|
||||
$response = $this->get("AccessSecuredController/adminonly");
|
||||
$this->assertEquals(
|
||||
403,
|
||||
@ -236,15 +237,15 @@ class ControllerTest extends FunctionalTest
|
||||
"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");
|
||||
$this->assertEquals(
|
||||
200,
|
||||
$response->getStatusCode(),
|
||||
"Permission codes are respected when set in \$allowed_actions"
|
||||
);
|
||||
$this->session()->inst_set('loggedInAs', null);
|
||||
|
||||
Security::setCurrentUser(null);
|
||||
$response = $this->get('AccessBaseController/extensionmethod1');
|
||||
$this->assertEquals(
|
||||
200,
|
||||
@ -285,7 +286,7 @@ class ControllerTest extends FunctionalTest
|
||||
"and doesn't satisfy checks"
|
||||
);
|
||||
|
||||
$this->session()->inst_set('loggedInAs', $adminUser->ID);
|
||||
Security::setCurrentUser($adminUser);
|
||||
$response = $this->get('IndexSecuredController/');
|
||||
$this->assertEquals(
|
||||
200,
|
||||
@ -293,7 +294,7 @@ class ControllerTest extends FunctionalTest
|
||||
"Access granted when index action is limited through allowed_actions, " .
|
||||
"and does satisfy checks"
|
||||
);
|
||||
$this->session()->inst_set('loggedInAs', null);
|
||||
Security::setCurrentUser(null);
|
||||
}
|
||||
|
||||
public function testWildcardAllowedActions()
|
||||
|
@ -10,11 +10,10 @@ use SilverStripe\Forms\Tests\GridField\GridFieldTest\Team;
|
||||
use SilverStripe\ORM\ArrayList;
|
||||
use SilverStripe\ORM\DataList;
|
||||
use SilverStripe\ORM\ValidationException;
|
||||
use SilverStripe\Security\Member;
|
||||
use SilverStripe\Security\Security;
|
||||
use SilverStripe\Security\SecurityToken;
|
||||
use SilverStripe\Dev\CSSContentParser;
|
||||
use SilverStripe\Dev\SapphireTest;
|
||||
use SilverStripe\Control\Controller;
|
||||
use SilverStripe\Control\HTTPRequest;
|
||||
use SilverStripe\Control\Session;
|
||||
use SilverStripe\Forms\FieldList;
|
||||
@ -67,8 +66,8 @@ class GridFieldDeleteActionTest extends SapphireTest
|
||||
|
||||
public function testDontShowDeleteButtons()
|
||||
{
|
||||
if (Member::currentUser()) {
|
||||
Member::currentUser()->logOut();
|
||||
if (Security::getCurrentUser()) {
|
||||
Security::setCurrentUser(null);
|
||||
}
|
||||
$content = new CSSContentParser($this->gridField->FieldHolder());
|
||||
// Check that there are content
|
||||
@ -116,8 +115,8 @@ class GridFieldDeleteActionTest extends SapphireTest
|
||||
|
||||
public function testDeleteActionWithoutCorrectPermission()
|
||||
{
|
||||
if (Member::currentUser()) {
|
||||
Member::currentUser()->logOut();
|
||||
if (Security::getCurrentUser()) {
|
||||
Security::setCurrentUser(null);
|
||||
}
|
||||
$this->setExpectedException(ValidationException::class);
|
||||
|
||||
|
@ -17,6 +17,7 @@ use SilverStripe\Forms\Form;
|
||||
use SilverStripe\Forms\GridField\GridFieldConfig;
|
||||
use SilverStripe\Forms\GridField\GridFieldEditButton;
|
||||
use SilverStripe\Forms\GridField\GridField;
|
||||
use SilverStripe\Security\Security;
|
||||
|
||||
class GridFieldEditButtonTest extends SapphireTest
|
||||
{
|
||||
@ -62,8 +63,8 @@ class GridFieldEditButtonTest extends SapphireTest
|
||||
|
||||
public function testShowEditLinks()
|
||||
{
|
||||
if (Member::currentUser()) {
|
||||
Member::currentUser()->logOut();
|
||||
if (Security::getCurrentUser()) {
|
||||
Security::setCurrentUser(null);
|
||||
}
|
||||
|
||||
$content = new CSSContentParser($this->gridField->FieldHolder());
|
||||
|
@ -15,7 +15,7 @@ use SilverStripe\Security\Tests\BasicAuthTest\ControllerSecuredWithPermission;
|
||||
class BasicAuthTest extends FunctionalTest
|
||||
{
|
||||
|
||||
static $original_unique_identifier_field;
|
||||
protected static $original_unique_identifier_field;
|
||||
|
||||
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
|
||||
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;
|
||||
}
|
||||
|
||||
@ -42,7 +42,7 @@ class BasicAuthTest extends FunctionalTest
|
||||
unset($_SERVER['PHP_AUTH_USER']);
|
||||
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());
|
||||
|
||||
$_SERVER['PHP_AUTH_USER'] = $origUser;
|
||||
@ -56,13 +56,13 @@ class BasicAuthTest extends FunctionalTest
|
||||
|
||||
unset($_SERVER['PHP_AUTH_USER']);
|
||||
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::$post_init_called);
|
||||
|
||||
$_SERVER['PHP_AUTH_USER'] = 'user-in-mygroup@test.com';
|
||||
$_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::$post_init_called);
|
||||
|
||||
@ -77,17 +77,17 @@ class BasicAuthTest extends FunctionalTest
|
||||
|
||||
$_SERVER['PHP_AUTH_USER'] = 'user-in-mygroup@test.com';
|
||||
$_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');
|
||||
|
||||
$_SERVER['PHP_AUTH_USER'] = 'user-without-groups@test.com';
|
||||
$_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');
|
||||
|
||||
$_SERVER['PHP_AUTH_USER'] = 'user-in-mygroup@test.com';
|
||||
$_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');
|
||||
|
||||
$_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_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');
|
||||
|
||||
$_SERVER['PHP_AUTH_USER'] = 'user-without-groups@test.com';
|
||||
$_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');
|
||||
|
||||
$_SERVER['PHP_AUTH_USER'] = 'user-in-mygroup@test.com';
|
||||
$_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');
|
||||
|
||||
$_SERVER['PHP_AUTH_USER'] = $origUser;
|
||||
@ -127,19 +127,19 @@ class BasicAuthTest extends FunctionalTest
|
||||
// First failed attempt
|
||||
$_SERVER['PHP_AUTH_USER'] = 'failedlogin@test.com';
|
||||
$_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();
|
||||
$this->assertEquals(1, $check->FailedLoginCount);
|
||||
|
||||
// Second failed attempt
|
||||
$_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();
|
||||
$this->assertEquals(2, $check->FailedLoginCount);
|
||||
|
||||
// successful basic auth should reset failed login count
|
||||
$_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();
|
||||
$this->assertEquals(0, $check->FailedLoginCount);
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ use SilverStripe\Security\InheritedPermissions;
|
||||
use SilverStripe\Security\InheritedPermissionsExtension;
|
||||
use SilverStripe\Security\Member;
|
||||
use SilverStripe\Security\PermissionChecker;
|
||||
use SilverStripe\Security\Security;
|
||||
use SilverStripe\Versioned\Versioned;
|
||||
|
||||
/**
|
||||
@ -45,7 +46,7 @@ class TestPermissionNode extends DataObject implements TestOnly
|
||||
public function canEdit($member = null)
|
||||
{
|
||||
if (!$member) {
|
||||
$member = Member::currentUser();
|
||||
$member = Security::getCurrentUser();
|
||||
}
|
||||
return static::getInheritedPermissions()->canEdit($this->ID, $member);
|
||||
}
|
||||
@ -53,7 +54,7 @@ class TestPermissionNode extends DataObject implements TestOnly
|
||||
public function canView($member = null)
|
||||
{
|
||||
if (!$member) {
|
||||
$member = Member::currentUser();
|
||||
$member = Security::getCurrentUser();
|
||||
}
|
||||
return static::getInheritedPermissions()->canView($this->ID, $member);
|
||||
}
|
||||
@ -61,7 +62,7 @@ class TestPermissionNode extends DataObject implements TestOnly
|
||||
public function canDelete($member = null)
|
||||
{
|
||||
if (!$member) {
|
||||
$member = Member::currentUser();
|
||||
$member = Security::getCurrentUser();
|
||||
}
|
||||
return static::getInheritedPermissions()->canDelete($this->ID, $member);
|
||||
}
|
||||
|
@ -2,20 +2,20 @@
|
||||
|
||||
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\ValidationResult;
|
||||
use SilverStripe\Security\PasswordEncryptor;
|
||||
use SilverStripe\Security\PasswordEncryptor_PHPHash;
|
||||
use SilverStripe\Security\Authenticator;
|
||||
use SilverStripe\Security\MemberAuthenticator\CMSMemberAuthenticator;
|
||||
use SilverStripe\Security\MemberAuthenticator\CMSMemberLoginForm;
|
||||
use SilverStripe\Security\MemberAuthenticator\MemberAuthenticator;
|
||||
use SilverStripe\Security\Security;
|
||||
use SilverStripe\Security\Member;
|
||||
use SilverStripe\Security\MemberAuthenticator;
|
||||
use SilverStripe\Security\MemberLoginForm;
|
||||
use SilverStripe\Security\CMSMemberLoginForm;
|
||||
use SilverStripe\Security\MemberAuthenticator\MemberLoginForm;
|
||||
use SilverStripe\Security\IdentityStore;
|
||||
use SilverStripe\Core\Config\Config;
|
||||
use SilverStripe\Dev\SapphireTest;
|
||||
use SilverStripe\Forms\FieldList;
|
||||
use SilverStripe\Forms\Form;
|
||||
use SilverStripe\Control\HTTPRequest;
|
||||
|
||||
class MemberAuthenticatorTest extends SapphireTest
|
||||
{
|
||||
@ -41,59 +41,6 @@ class MemberAuthenticatorTest extends SapphireTest
|
||||
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()
|
||||
{
|
||||
|
||||
@ -109,75 +56,83 @@ class MemberAuthenticatorTest extends SapphireTest
|
||||
|
||||
public function testGenerateLoginForm()
|
||||
{
|
||||
$authenticator = new MemberAuthenticator();
|
||||
|
||||
$controller = new Security();
|
||||
|
||||
// Create basic login form
|
||||
$frontendForm = MemberAuthenticator::get_login_form($controller);
|
||||
$this->assertTrue($frontendForm instanceof MemberLoginForm);
|
||||
$frontendResponse = $authenticator
|
||||
->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
|
||||
$this->assertTrue(MemberAuthenticator::supports_cms());
|
||||
$cmsForm = MemberAuthenticator::get_cms_login_form($controller);
|
||||
$this->assertGreaterThan(0, ($authenticator->supportedServices() & Authenticator::CMS_LOGIN));
|
||||
$cmsHandler = $authenticator->getLoginHandler('/');
|
||||
$cmsForm = $cmsHandler->loginForm();
|
||||
$this->assertTrue($cmsForm instanceof CMSMemberLoginForm);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Test that a member can be authenticated via their temp id
|
||||
*/
|
||||
public function testAuthenticateByTempID()
|
||||
{
|
||||
$authenticator = new CMSMemberAuthenticator();
|
||||
|
||||
$member = new Member();
|
||||
$member->Email = 'test1@test.com';
|
||||
$member->PasswordEncryption = "sha1";
|
||||
$member->Password = "mypassword";
|
||||
$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
|
||||
$tempID = $member->TempIDHash;
|
||||
$this->assertEmpty($tempID);
|
||||
|
||||
// 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;
|
||||
$this->assertNotEmpty($tempID);
|
||||
|
||||
// Test correct login
|
||||
$result = MemberAuthenticator::authenticate(
|
||||
$result = $authenticator->authenticate(
|
||||
array(
|
||||
'tempid' => $tempID,
|
||||
'Password' => 'mypassword'
|
||||
),
|
||||
$form
|
||||
$message
|
||||
);
|
||||
$form->restoreFormState();
|
||||
|
||||
$this->assertNotEmpty($result);
|
||||
$this->assertEquals($result->ID, $member->ID);
|
||||
$this->assertEmpty($form->getMessage());
|
||||
$this->assertTrue($message->isValid());
|
||||
|
||||
// Test incorrect login
|
||||
$form->clearMessage();
|
||||
$result = MemberAuthenticator::authenticate(
|
||||
$result = $authenticator->authenticate(
|
||||
array(
|
||||
'tempid' => $tempID,
|
||||
'Password' => 'notmypassword'
|
||||
),
|
||||
$form
|
||||
$message
|
||||
);
|
||||
$form->restoreFormState();
|
||||
|
||||
$this->assertEmpty($result);
|
||||
$messages = $message->getMessages();
|
||||
$this->assertEquals(
|
||||
_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()
|
||||
{
|
||||
// Make form
|
||||
$controller = new Security();
|
||||
/**
|
||||
* @skipUpgrade
|
||||
*/
|
||||
$form = new Form($controller, 'Form', new FieldList(), new FieldList());
|
||||
$authenticator = new MemberAuthenticator();
|
||||
|
||||
// Test correct login
|
||||
$result = MemberAuthenticator::authenticate(
|
||||
$result = $authenticator->authenticate(
|
||||
array(
|
||||
'Email' => 'admin',
|
||||
'Password' => 'password'
|
||||
),
|
||||
$form
|
||||
$message
|
||||
);
|
||||
$form->restoreFormState();
|
||||
$this->assertNotEmpty($result);
|
||||
$this->assertEquals($result->Email, Security::default_admin_username());
|
||||
$this->assertEmpty($form->getMessage());
|
||||
$this->assertTrue($message->isValid());
|
||||
|
||||
// Test incorrect login
|
||||
$form->clearMessage();
|
||||
$result = MemberAuthenticator::authenticate(
|
||||
$result = $authenticator->authenticate(
|
||||
array(
|
||||
'Email' => 'admin',
|
||||
'Password' => 'notmypassword'
|
||||
),
|
||||
$form
|
||||
$message
|
||||
);
|
||||
$form->restoreFormState();
|
||||
$messages = $message->getMessages();
|
||||
$this->assertEmpty($result);
|
||||
$this->assertEquals(
|
||||
'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()
|
||||
{
|
||||
$authenticator = new MemberAuthenticator();
|
||||
|
||||
Config::inst()->update(Member::class, 'lock_out_after_incorrect_logins', 1);
|
||||
Config::inst()->update(Member::class, 'lock_out_delay_mins', 10);
|
||||
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
|
||||
MemberAuthenticator::authenticate(
|
||||
$authenticator->authenticate(
|
||||
[
|
||||
'Email' => 'admin',
|
||||
'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);
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,7 @@
|
||||
namespace SilverStripe\Security\Tests;
|
||||
|
||||
use SilverStripe\Core\Convert;
|
||||
use SilverStripe\Core\Injector\Injector;
|
||||
use SilverStripe\Dev\FunctionalTest;
|
||||
use SilverStripe\Control\Cookie;
|
||||
use SilverStripe\i18n\i18n;
|
||||
@ -10,15 +11,17 @@ use SilverStripe\ORM\DataObject;
|
||||
use SilverStripe\ORM\DB;
|
||||
use SilverStripe\ORM\FieldType\DBDatetime;
|
||||
use SilverStripe\Security\Member;
|
||||
use SilverStripe\Security\MemberAuthenticator;
|
||||
use SilverStripe\Security\MemberAuthenticator\SessionAuthenticationHandler;
|
||||
use SilverStripe\Security\Security;
|
||||
use SilverStripe\Security\MemberPassword;
|
||||
use SilverStripe\Security\Group;
|
||||
use SilverStripe\Security\Permission;
|
||||
use SilverStripe\Security\IdentityStore;
|
||||
use SilverStripe\Security\PasswordEncryptor_Blowfish;
|
||||
use SilverStripe\Security\RememberLoginHash;
|
||||
use SilverStripe\Security\Member_Validator;
|
||||
use SilverStripe\Security\Tests\MemberTest\FieldsExtension;
|
||||
use SilverStripe\Control\HTTPRequest;
|
||||
|
||||
class MemberTest extends FunctionalTest
|
||||
{
|
||||
@ -237,13 +240,13 @@ class MemberTest extends FunctionalTest
|
||||
$this->assertNotNull($member);
|
||||
|
||||
// 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);
|
||||
|
||||
// We should get redirected to Security/passwordsent
|
||||
$this->assertContains(
|
||||
'Security/passwordsent/testuser@example.com',
|
||||
'Security/lostpassword/passwordsent/testuser@example.com',
|
||||
urldecode($response->getHeader('Location'))
|
||||
);
|
||||
|
||||
@ -534,26 +537,24 @@ class MemberTest extends FunctionalTest
|
||||
$member = $this->objFromFixture(Member::class, 'test');
|
||||
$member2 = $this->objFromFixture(Member::class, 'staffmember');
|
||||
|
||||
$this->session()->inst_set('loggedInAs', null);
|
||||
|
||||
/* Not logged in, you can't view, delete or edit the record */
|
||||
$this->assertFalse($member->canView());
|
||||
$this->assertFalse($member->canDelete());
|
||||
$this->assertFalse($member->canEdit());
|
||||
|
||||
/* Logged in users can edit their own record */
|
||||
$this->session()->inst_set('loggedInAs', $member->ID);
|
||||
$this->logInAs($member);
|
||||
$this->assertTrue($member->canView());
|
||||
$this->assertFalse($member->canDelete());
|
||||
$this->assertTrue($member->canEdit());
|
||||
|
||||
/* Other uses cannot view, delete or edit others records */
|
||||
$this->session()->inst_set('loggedInAs', $member2->ID);
|
||||
$this->logInAs($member2);
|
||||
$this->assertFalse($member->canView());
|
||||
$this->assertFalse($member->canDelete());
|
||||
$this->assertFalse($member->canEdit());
|
||||
|
||||
$this->session()->inst_set('loggedInAs', null);
|
||||
$this->logOut();
|
||||
}
|
||||
|
||||
public function testAuthorisedMembersCanManipulateOthersRecords()
|
||||
@ -562,10 +563,12 @@ class MemberTest extends FunctionalTest
|
||||
$member2 = $this->objFromFixture(Member::class, 'staffmember');
|
||||
|
||||
/* 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->canDelete());
|
||||
$this->assertTrue($member2->canEdit());
|
||||
|
||||
$this->logOut();
|
||||
}
|
||||
|
||||
public function testExtendedCan()
|
||||
@ -664,12 +667,12 @@ class MemberTest extends FunctionalTest
|
||||
'Adding new admin group relation is not allowed for non-admin members'
|
||||
);
|
||||
|
||||
$this->session()->inst_set('loggedInAs', $adminMember->ID);
|
||||
$this->logInAs($adminMember);
|
||||
$this->assertTrue(
|
||||
$staffMember->onChangeGroups(array($newAdminGroup->ID)),
|
||||
'Adding new admin group relation is allowed for normal users, when granter is logged in as admin'
|
||||
);
|
||||
$this->session()->inst_set('loggedInAs', null);
|
||||
$this->logOut();
|
||||
|
||||
$this->assertTrue(
|
||||
$adminMember->onChangeGroups(array($newAdminGroup->ID)),
|
||||
@ -719,7 +722,7 @@ class MemberTest extends FunctionalTest
|
||||
);
|
||||
|
||||
// Test staff member can be added if they are already admin
|
||||
$this->session()->inst_set('loggedInAs', null);
|
||||
$this->logOut();
|
||||
$this->assertFalse($adminMember->inGroup($newAdminGroup));
|
||||
$adminMember->Groups()->add($newAdminGroup);
|
||||
$this->assertTrue(
|
||||
@ -872,7 +875,8 @@ class MemberTest extends FunctionalTest
|
||||
{
|
||||
$m1 = $this->objFromFixture(Member::class, 'grouplessmember');
|
||||
|
||||
$m1->login(true);
|
||||
Injector::inst()->get(IdentityStore::class)->logIn($m1, true);
|
||||
|
||||
$hashes = RememberLoginHash::get()->filter('MemberID', $m1->ID);
|
||||
$this->assertEquals($hashes->count(), 1);
|
||||
$firstHash = $hashes->first();
|
||||
@ -887,7 +891,8 @@ class MemberTest extends FunctionalTest
|
||||
*/
|
||||
$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();
|
||||
$this->assertNotNull($firstHash);
|
||||
|
||||
@ -914,7 +919,7 @@ class MemberTest extends FunctionalTest
|
||||
);
|
||||
$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
|
||||
$response = $this->get(
|
||||
@ -922,7 +927,7 @@ class MemberTest extends FunctionalTest
|
||||
$this->session(),
|
||||
null,
|
||||
array(
|
||||
'alc_enc' => $m1->ID.':'.str_rot13($token),
|
||||
'alc_enc' => $m1->ID.':asdfasd'.str_rot13($token),
|
||||
'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
|
||||
// should remove all previous hashes for this device
|
||||
$response = $this->post(
|
||||
'Security/LoginForm',
|
||||
'Security/login/default/LoginForm',
|
||||
array(
|
||||
'Email' => $m1->Email,
|
||||
'Password' => '1nitialPassword',
|
||||
'AuthenticationMethod' => MemberAuthenticator::class,
|
||||
'action_dologin' => 'action_dologin'
|
||||
'action_doLogin' => 'action_doLogin'
|
||||
),
|
||||
null,
|
||||
$this->session(),
|
||||
@ -966,7 +970,7 @@ class MemberTest extends FunctionalTest
|
||||
* @var Member $m1
|
||||
*/
|
||||
$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();
|
||||
$this->assertNotNull($firstHash);
|
||||
|
||||
@ -996,7 +1000,7 @@ class MemberTest extends FunctionalTest
|
||||
);
|
||||
$this->assertContains($message, $response->getBody());
|
||||
|
||||
$this->session()->inst_set('loggedInAs', null);
|
||||
$this->logOut();
|
||||
|
||||
// re-generates the hash so we can get the token
|
||||
$firstHash->Hash = $firstHash->getNewHash($m1);
|
||||
@ -1016,7 +1020,7 @@ class MemberTest extends FunctionalTest
|
||||
)
|
||||
);
|
||||
$this->assertNotContains($message, $response->getBody());
|
||||
$this->session()->inst_set('loggedInAs', null);
|
||||
$this->logOut();
|
||||
DBDatetime::clear_mock_now();
|
||||
}
|
||||
|
||||
@ -1025,10 +1029,10 @@ class MemberTest extends FunctionalTest
|
||||
$m1 = $this->objFromFixture(Member::class, 'noexpiry');
|
||||
|
||||
// First device
|
||||
$m1->login(true);
|
||||
Injector::inst()->get(IdentityStore::class)->logIn($m1, true);
|
||||
Cookie::set('alc_device', null);
|
||||
// Second device
|
||||
$m1->login(true);
|
||||
Injector::inst()->get(IdentityStore::class)->logIn($m1, true);
|
||||
|
||||
// Hash of first device
|
||||
$firstHash = RememberLoginHash::get()->filter('MemberID', $m1->ID)->first();
|
||||
@ -1069,7 +1073,11 @@ class MemberTest extends FunctionalTest
|
||||
);
|
||||
$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
|
||||
$response = $this->get(
|
||||
@ -1101,7 +1109,7 @@ class MemberTest extends FunctionalTest
|
||||
|
||||
// Logging out from any device when all login hashes should be removed
|
||||
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());
|
||||
$this->assertEquals(
|
||||
RememberLoginHash::get()->filter('MemberID', $m1->ID)->count(),
|
||||
@ -1156,8 +1164,8 @@ class MemberTest extends FunctionalTest
|
||||
'Failed to increment $member->FailedLoginCount'
|
||||
);
|
||||
|
||||
$this->assertFalse(
|
||||
$member->isLockedOut(),
|
||||
$this->assertTrue(
|
||||
$member->canLogin()->isValid(),
|
||||
"Member has been locked out too early"
|
||||
);
|
||||
}
|
||||
@ -1362,12 +1370,12 @@ class MemberTest extends FunctionalTest
|
||||
|
||||
public function testCurrentUser()
|
||||
{
|
||||
$this->assertNull(Member::currentUser());
|
||||
$this->assertNull(Security::getCurrentUser());
|
||||
|
||||
$adminMember = $this->objFromFixture(Member::class, 'admin');
|
||||
$this->logInAs($adminMember);
|
||||
|
||||
$userFromSession = Member::currentUser();
|
||||
$userFromSession = Security::getCurrentUser();
|
||||
$this->assertEquals($adminMember->ID, $userFromSession->ID);
|
||||
}
|
||||
|
||||
@ -1376,7 +1384,7 @@ class MemberTest extends FunctionalTest
|
||||
*/
|
||||
public function testActAsUserPermissions()
|
||||
{
|
||||
$this->assertNull(Member::currentUser());
|
||||
$this->assertNull(Security::getCurrentUser());
|
||||
|
||||
/** @var Member $adminMember */
|
||||
$adminMember = $this->objFromFixture(Member::class, 'admin');
|
||||
@ -1415,21 +1423,21 @@ class MemberTest extends FunctionalTest
|
||||
*/
|
||||
public function testActAsUser()
|
||||
{
|
||||
$this->assertNull(Member::currentUser());
|
||||
$this->assertNull(Security::getCurrentUser());
|
||||
|
||||
/** @var Member $adminMember */
|
||||
$adminMember = $this->objFromFixture(Member::class, 'admin');
|
||||
$memberID = Member::actAs($adminMember, function () {
|
||||
return Member::currentUserID();
|
||||
$member = Member::actAs($adminMember, function () {
|
||||
return Security::getCurrentUser();
|
||||
});
|
||||
$this->assertEquals($adminMember->ID, $memberID);
|
||||
$this->assertEquals($adminMember->ID, $member->ID);
|
||||
|
||||
// Check nesting
|
||||
$memberID = Member::actAs($adminMember, function () {
|
||||
$member = Member::actAs($adminMember, function () {
|
||||
return Member::actAs(null, function () {
|
||||
return Member::currentUserID();
|
||||
return Security::getCurrentUser();
|
||||
});
|
||||
});
|
||||
$this->assertEmpty($memberID);
|
||||
$this->assertEmpty($member);
|
||||
}
|
||||
}
|
||||
|
@ -2,18 +2,16 @@
|
||||
|
||||
namespace SilverStripe\Security\Tests;
|
||||
|
||||
use PhpConsole\Auth;
|
||||
use SilverStripe\Dev\Debug;
|
||||
use SilverStripe\ORM\DataObject;
|
||||
use SilverStripe\ORM\FieldType\DBDatetime;
|
||||
use SilverStripe\ORM\FieldType\DBClassName;
|
||||
use SilverStripe\ORM\DB;
|
||||
use SilverStripe\ORM\ValidationResult;
|
||||
use SilverStripe\Security\Authenticator;
|
||||
use SilverStripe\Security\LoginAttempt;
|
||||
use SilverStripe\Security\Member;
|
||||
use SilverStripe\Security\MemberAuthenticator;
|
||||
use SilverStripe\Security\MemberAuthenticator\MemberAuthenticator;
|
||||
use SilverStripe\Security\Security;
|
||||
use SilverStripe\Security\Permission;
|
||||
use SilverStripe\Core\Config\Config;
|
||||
use SilverStripe\Core\Convert;
|
||||
use SilverStripe\Dev\FunctionalTest;
|
||||
@ -48,13 +46,9 @@ class SecurityTest extends FunctionalTest
|
||||
|
||||
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
|
||||
Config::modify()->set(Authenticator::class, 'authenticators', []);
|
||||
Config::modify()->set(Authenticator::class, 'default_authenticator', MemberAuthenticator::class);
|
||||
Config::modify()->set(MemberAuthenticator::class, 'authenticators', []);
|
||||
Config::modify()->set(MemberAuthenticator::class, 'default_authenticator', MemberAuthenticator::class);
|
||||
|
||||
// And that the unique identified field is 'Email'
|
||||
$this->priorUniqueIdentifierField = Member::config()->unique_identifier_field;
|
||||
@ -74,8 +68,8 @@ class SecurityTest extends FunctionalTest
|
||||
// Restore selected authenticator
|
||||
|
||||
// MemberAuthenticator might not actually be present
|
||||
Config::modify()->set(Authenticator::class, 'authenticators', $this->priorAuthenticators);
|
||||
Config::modify()->set(Authenticator::class, 'default_authenticator', $this->priorDefaultAuthenticator);
|
||||
// Config::modify()->set(Authenticator::class, 'authenticators', $this->priorAuthenticators);
|
||||
// Config::modify()->set(Authenticator::class, 'default_authenticator', $this->priorDefaultAuthenticator);
|
||||
|
||||
// Restore unique identifier field
|
||||
Member::config()->unique_identifier_field = $this->priorUniqueIdentifierField;
|
||||
@ -182,19 +176,19 @@ class SecurityTest extends FunctionalTest
|
||||
public function testAutomaticRedirectionOnLogin()
|
||||
{
|
||||
// BackURL with permission error (not authenticated) should not redirect
|
||||
if ($member = Member::currentUser()) {
|
||||
$member->logOut();
|
||||
if ($member = Security::getCurrentUser()) {
|
||||
Security::setCurrentUser(null);
|
||||
}
|
||||
$response = $this->getRecursive('SecurityTest_SecuredController');
|
||||
$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
|
||||
// No message/context is available as the user has not attempted to view the secured controller
|
||||
$response = $this->getRecursive('Security/login?BackURL=SecurityTest_SecuredController/');
|
||||
$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->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
|
||||
$this->logInAs('grouplessmember');
|
||||
@ -228,7 +222,7 @@ class SecurityTest extends FunctionalTest
|
||||
$member = DataObject::get_one(Member::class);
|
||||
|
||||
/* Log in with any user that we can find */
|
||||
$this->session()->inst_set('loggedInAs', $member->ID);
|
||||
Security::setCurrentUser($member);
|
||||
|
||||
/* View the Security/login page */
|
||||
$response = $this->get(Config::inst()->get(Security::class, 'login_url'));
|
||||
@ -245,8 +239,7 @@ class SecurityTest extends FunctionalTest
|
||||
'MemberLoginForm_LoginForm',
|
||||
null,
|
||||
array(
|
||||
'AuthenticationMethod' => MemberAuthenticator::class,
|
||||
'action_dologout' => 1,
|
||||
'action_logout' => 1,
|
||||
)
|
||||
);
|
||||
|
||||
@ -255,7 +248,7 @@ class SecurityTest extends FunctionalTest
|
||||
$this->assertNotNull($response->getBody(), 'There is body content on the page');
|
||||
|
||||
/* Log the user out */
|
||||
$this->session()->inst_set('loggedInAs', null);
|
||||
Security::setCurrentUser(null);
|
||||
}
|
||||
|
||||
public function testMemberIDInSessionDoesntExistInDatabaseHasToLogin()
|
||||
@ -379,6 +372,8 @@ class SecurityTest extends FunctionalTest
|
||||
);
|
||||
$this->assertEquals($this->idFromFixture(Member::class, 'test'), $this->session()->inst_get('loggedInAs'));
|
||||
|
||||
$this->logOut();
|
||||
|
||||
/* EXPIRED PASSWORDS ARE SENT TO THE CHANGE PASSWORD FORM */
|
||||
$expiredResponse = $this->doTestLoginForm('expired@silverstripe.com', '1nitialPassword');
|
||||
$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'));
|
||||
|
||||
// Check if we can login with the new password
|
||||
$this->logOut();
|
||||
$goodResponse = $this->doTestLoginForm('testuser@example.com', 'changedPassword');
|
||||
$this->assertEquals(302, $goodResponse->getStatusCode());
|
||||
$this->assertEquals(
|
||||
@ -436,7 +432,7 @@ class SecurityTest extends FunctionalTest
|
||||
|
||||
// Request new password by email
|
||||
$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');
|
||||
|
||||
@ -461,6 +457,7 @@ class SecurityTest extends FunctionalTest
|
||||
$this->assertEquals($this->idFromFixture(Member::class, 'test'), $this->session()->inst_get('loggedInAs'));
|
||||
|
||||
// Check if we can login with the new password
|
||||
$this->logOut();
|
||||
$goodResponse = $this->doTestLoginForm('testuser@example.com', 'changedPassword');
|
||||
$this->assertEquals(302, $goodResponse->getStatusCode());
|
||||
$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;
|
||||
|
||||
// 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');
|
||||
$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(
|
||||
$member->LockedOutUntil,
|
||||
'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'
|
||||
);
|
||||
}
|
||||
|
||||
$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->assertNull(
|
||||
@ -533,7 +528,7 @@ class SecurityTest extends FunctionalTest
|
||||
);
|
||||
|
||||
// Log the user out
|
||||
$this->session()->inst_set('loggedInAs', null);
|
||||
$this->logOut();
|
||||
|
||||
// Login again with wrong password, but less attempts than threshold
|
||||
for ($i = 1; $i < Member::config()->lock_out_after_incorrect_logins; $i++) {
|
||||
@ -594,14 +589,14 @@ class SecurityTest extends FunctionalTest
|
||||
$attempt = DataObject::get_one(
|
||||
LoginAttempt::class,
|
||||
array(
|
||||
'"LoginAttempt"."Email"' => 'testuser@example.com'
|
||||
'"LoginAttempt"."Email"' => 'testuser@example.com'
|
||||
)
|
||||
);
|
||||
$this->assertTrue(is_object($attempt));
|
||||
$member = DataObject::get_one(
|
||||
Member::class,
|
||||
array(
|
||||
'"Member"."Email"' => 'testuser@example.com'
|
||||
'"Member"."Email"' => 'testuser@example.com'
|
||||
)
|
||||
);
|
||||
$this->assertEquals($attempt->Status, 'Failure');
|
||||
@ -648,9 +643,7 @@ class SecurityTest extends FunctionalTest
|
||||
|
||||
public function testDatabaseIsReadyWithInsufficientMemberColumns()
|
||||
{
|
||||
$old = Security::$force_database_is_ready;
|
||||
Security::$force_database_is_ready = null;
|
||||
Security::$database_is_ready = false;
|
||||
Security::clear_database_is_ready();
|
||||
DBClassName::clear_classname_cache();
|
||||
|
||||
// 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
|
||||
static::resetDBSchema(true);
|
||||
$this->assertTrue(Security::database_is_ready());
|
||||
|
||||
Security::$force_database_is_ready = $old;
|
||||
}
|
||||
|
||||
public function testSecurityControllerSendsRobotsTagHeader()
|
||||
@ -703,7 +694,7 @@ class SecurityTest extends FunctionalTest
|
||||
'Email' => $email,
|
||||
'Password' => $password,
|
||||
'AuthenticationMethod' => MemberAuthenticator::class,
|
||||
'action_dologin' => 1,
|
||||
'action_doLogin' => 1,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
@ -16,6 +16,7 @@ use SilverStripe\ORM\FieldType\DBField;
|
||||
use SilverStripe\ORM\ArrayList;
|
||||
use SilverStripe\ORM\PaginatedList;
|
||||
use SilverStripe\Security\Member;
|
||||
use SilverStripe\Security\Security;
|
||||
use SilverStripe\Security\SecurityToken;
|
||||
use SilverStripe\Security\Permission;
|
||||
use SilverStripe\View\ArrayData;
|
||||
@ -406,22 +407,22 @@ SS;
|
||||
);
|
||||
|
||||
$this->assertEquals(
|
||||
(string)Member::currentUser(),
|
||||
(string)Security::getCurrentUser(),
|
||||
$this->render('{$CurrentMember}'),
|
||||
'Member template functions result correct result'
|
||||
);
|
||||
$this->assertEquals(
|
||||
(string)Member::currentUser(),
|
||||
(string)Security::getCurrentUser(),
|
||||
$this->render('{$CurrentUser}'),
|
||||
'Member template functions result correct result'
|
||||
);
|
||||
$this->assertEquals(
|
||||
(string)Member::currentUser(),
|
||||
(string)Security::getCurrentUser(),
|
||||
$this->render('{$currentMember}'),
|
||||
'Member template functions result correct result'
|
||||
);
|
||||
$this->assertEquals(
|
||||
(string)Member::currentUser(),
|
||||
(string)Security::getCurrentUser(),
|
||||
$this->render('{$currentUser}'),
|
||||
'Member template functions result correct result'
|
||||
);
|
||||
|
Loading…
Reference in New Issue
Block a user