mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 14:05:37 +02:00
ENH Use symfony/validation logic (#11399)
This commit is contained in:
parent
aa2b8c380e
commit
7f11bf3587
@ -28,7 +28,8 @@ SilverStripe\Dev\Backtrace:
|
|||||||
- ['SilverStripe\Security\PasswordEncryptor_Blowfish', 'encryptA']
|
- ['SilverStripe\Security\PasswordEncryptor_Blowfish', 'encryptA']
|
||||||
- ['SilverStripe\Security\PasswordEncryptor_Blowfish', 'encryptX']
|
- ['SilverStripe\Security\PasswordEncryptor_Blowfish', 'encryptX']
|
||||||
- ['SilverStripe\Security\PasswordEncryptor_Blowfish', 'encryptY']
|
- ['SilverStripe\Security\PasswordEncryptor_Blowfish', 'encryptY']
|
||||||
- ['SilverStripe\Security\PasswordValidator', 'validate']
|
- ['SilverStripe\Security\Validation\RulesPasswordValidator', 'validate']
|
||||||
|
- ['SilverStripe\Security\Validation\EntropyPasswordValidator', 'validate']
|
||||||
- ['SilverStripe\Security\RememberLoginHash', 'setToken']
|
- ['SilverStripe\Security\RememberLoginHash', 'setToken']
|
||||||
- ['SilverStripe\Security\Security', 'encrypt_password']
|
- ['SilverStripe\Security\Security', 'encrypt_password']
|
||||||
- ['*', 'checkPassword']
|
- ['*', 'checkPassword']
|
||||||
|
@ -2,12 +2,5 @@
|
|||||||
Name: corepasswords
|
Name: corepasswords
|
||||||
---
|
---
|
||||||
SilverStripe\Core\Injector\Injector:
|
SilverStripe\Core\Injector\Injector:
|
||||||
SilverStripe\Security\PasswordValidator:
|
SilverStripe\Security\Validation\PasswordValidator:
|
||||||
properties:
|
class: 'SilverStripe\Security\Validation\EntropyPasswordValidator'
|
||||||
MinLength: 8
|
|
||||||
HistoricCount: 6
|
|
||||||
|
|
||||||
# In the case someone uses `new PasswordValidator` instead of Injector, provide some safe defaults through config.
|
|
||||||
SilverStripe\Security\PasswordValidator:
|
|
||||||
min_length: 8
|
|
||||||
historic_count: 6
|
|
||||||
|
@ -50,7 +50,7 @@
|
|||||||
"symfony/mailer": "^7.0",
|
"symfony/mailer": "^7.0",
|
||||||
"symfony/mime": "^7.0",
|
"symfony/mime": "^7.0",
|
||||||
"symfony/translation": "^7.0",
|
"symfony/translation": "^7.0",
|
||||||
"symfony/validator": "^7.0",
|
"symfony/validator": "^7.1",
|
||||||
"symfony/yaml": "^7.0",
|
"symfony/yaml": "^7.0",
|
||||||
"ext-ctype": "*",
|
"ext-ctype": "*",
|
||||||
"ext-dom": "*",
|
"ext-dom": "*",
|
||||||
|
@ -4,14 +4,13 @@ namespace SilverStripe\Control\Email;
|
|||||||
|
|
||||||
use Exception;
|
use Exception;
|
||||||
use RuntimeException;
|
use RuntimeException;
|
||||||
use Egulias\EmailValidator\EmailValidator;
|
|
||||||
use Egulias\EmailValidator\Validation\RFCValidation;
|
|
||||||
use SilverStripe\Control\Director;
|
use SilverStripe\Control\Director;
|
||||||
use SilverStripe\Core\Config\Configurable;
|
use SilverStripe\Core\Config\Configurable;
|
||||||
use SilverStripe\Core\Environment;
|
use SilverStripe\Core\Environment;
|
||||||
use SilverStripe\Core\Extensible;
|
use SilverStripe\Core\Extensible;
|
||||||
use SilverStripe\Core\Injector\Injectable;
|
use SilverStripe\Core\Injector\Injectable;
|
||||||
use SilverStripe\Core\Injector\Injector;
|
use SilverStripe\Core\Injector\Injector;
|
||||||
|
use SilverStripe\Core\Validation\ConstraintValidator;
|
||||||
use SilverStripe\ORM\FieldType\DBField;
|
use SilverStripe\ORM\FieldType\DBField;
|
||||||
use SilverStripe\Model\ArrayData;
|
use SilverStripe\Model\ArrayData;
|
||||||
use SilverStripe\View\Requirements;
|
use SilverStripe\View\Requirements;
|
||||||
@ -22,6 +21,8 @@ use Symfony\Component\Mailer\MailerInterface;
|
|||||||
use Symfony\Component\Mime\Address;
|
use Symfony\Component\Mime\Address;
|
||||||
use Symfony\Component\Mime\Email as SymfonyEmail;
|
use Symfony\Component\Mime\Email as SymfonyEmail;
|
||||||
use Symfony\Component\Mime\Part\AbstractPart;
|
use Symfony\Component\Mime\Part\AbstractPart;
|
||||||
|
use Symfony\Component\Validator\Constraints\Email as EmailConstraint;
|
||||||
|
use Symfony\Component\Validator\Constraints\NotBlank;
|
||||||
|
|
||||||
class Email extends SymfonyEmail
|
class Email extends SymfonyEmail
|
||||||
{
|
{
|
||||||
@ -63,16 +64,17 @@ class Email extends SymfonyEmail
|
|||||||
private bool $dataHasBeenSet = false;
|
private bool $dataHasBeenSet = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks for RFC822-valid email format.
|
* Checks for RFC valid email format.
|
||||||
*
|
|
||||||
* @copyright Cal Henderson <cal@iamcal.com>
|
|
||||||
* This code is licensed under a Creative Commons Attribution-ShareAlike 2.5 License
|
|
||||||
* http://creativecommons.org/licenses/by-sa/2.5/
|
|
||||||
*/
|
*/
|
||||||
public static function is_valid_address(string $address): bool
|
public static function is_valid_address(string $address): bool
|
||||||
{
|
{
|
||||||
$validator = new EmailValidator();
|
return ConstraintValidator::validate(
|
||||||
return $validator->isValid($address, new RFCValidation());
|
$address,
|
||||||
|
[
|
||||||
|
new EmailConstraint(mode: EmailConstraint::VALIDATION_MODE_STRICT),
|
||||||
|
new NotBlank()
|
||||||
|
]
|
||||||
|
)->isValid();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function getSendAllEmailsTo(): array
|
public static function getSendAllEmailsTo(): array
|
||||||
@ -117,7 +119,7 @@ class Email extends SymfonyEmail
|
|||||||
$addresses = [];
|
$addresses = [];
|
||||||
if (is_array($config)) {
|
if (is_array($config)) {
|
||||||
foreach ($config as $key => $val) {
|
foreach ($config as $key => $val) {
|
||||||
if (filter_var($key, FILTER_VALIDATE_EMAIL)) {
|
if (static::is_valid_address($key)) {
|
||||||
$addresses[] = new Address($key, $val);
|
$addresses[] = new Address($key, $val);
|
||||||
} else {
|
} else {
|
||||||
$addresses[] = new Address($val);
|
$addresses[] = new Address($val);
|
||||||
|
@ -7,6 +7,8 @@ use BadMethodCallException;
|
|||||||
use InvalidArgumentException;
|
use InvalidArgumentException;
|
||||||
use SilverStripe\Core\ClassInfo;
|
use SilverStripe\Core\ClassInfo;
|
||||||
use SilverStripe\Core\ArrayLib;
|
use SilverStripe\Core\ArrayLib;
|
||||||
|
use Symfony\Component\Validator\Constraints\Ip;
|
||||||
|
use Symfony\Component\Validator\Constraints\IpValidator;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents a HTTP-request, including a URL that is tokenised for parsing, and a request method
|
* Represents a HTTP-request, including a URL that is tokenised for parsing, and a request method
|
||||||
@ -810,7 +812,8 @@ class HTTPRequest implements ArrayAccess
|
|||||||
*/
|
*/
|
||||||
public function setIP($ip)
|
public function setIP($ip)
|
||||||
{
|
{
|
||||||
if (!filter_var($ip, FILTER_VALIDATE_IP)) {
|
// We can't use ConstraintValidator here because it relies on injector and the kernel may not have booted yet.
|
||||||
|
if (!IpValidator::checkIp($ip, Ip::ALL)) {
|
||||||
throw new InvalidArgumentException("Invalid ip $ip");
|
throw new InvalidArgumentException("Invalid ip $ip");
|
||||||
}
|
}
|
||||||
$this->ip = $ip;
|
$this->ip = $ip;
|
||||||
|
@ -3,7 +3,10 @@
|
|||||||
namespace SilverStripe\Control\Middleware;
|
namespace SilverStripe\Control\Middleware;
|
||||||
|
|
||||||
use SilverStripe\Control\HTTPRequest;
|
use SilverStripe\Control\HTTPRequest;
|
||||||
|
use SilverStripe\Core\Validation\ConstraintValidator;
|
||||||
use Symfony\Component\HttpFoundation\IpUtils;
|
use Symfony\Component\HttpFoundation\IpUtils;
|
||||||
|
use Symfony\Component\Validator\Constraints\Ip;
|
||||||
|
use Symfony\Component\Validator\Constraints\NotBlank;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This middleware will rewrite headers that provide IP and host details from an upstream proxy.
|
* This middleware will rewrite headers that provide IP and host details from an upstream proxy.
|
||||||
@ -220,14 +223,14 @@ class TrustedProxyMiddleware implements HTTPMiddleware
|
|||||||
|
|
||||||
// Prioritise filters
|
// Prioritise filters
|
||||||
$filters = [
|
$filters = [
|
||||||
FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE,
|
Ip::ALL_ONLY_PUBLIC,
|
||||||
FILTER_FLAG_NO_PRIV_RANGE,
|
Ip::ALL_NO_PRIVATE,
|
||||||
null
|
Ip::ALL
|
||||||
];
|
];
|
||||||
foreach ($filters as $filter) {
|
foreach ($filters as $filter) {
|
||||||
// Find best IP
|
// Find best IP
|
||||||
foreach ($ips as $ip) {
|
foreach ($ips as $ip) {
|
||||||
if (filter_var($ip, FILTER_VALIDATE_IP, $filter ?? 0)) {
|
if (ConstraintValidator::validate($ip, [new Ip(version: $filter), new NotBlank()])->isValid()) {
|
||||||
return $ip;
|
return $ip;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,9 +2,12 @@
|
|||||||
|
|
||||||
namespace SilverStripe\Core;
|
namespace SilverStripe\Core;
|
||||||
|
|
||||||
|
use SilverStripe\Core\Validation\ConstraintValidator;
|
||||||
use SimpleXMLElement;
|
use SimpleXMLElement;
|
||||||
use SilverStripe\ORM\DB;
|
use SilverStripe\ORM\DB;
|
||||||
use SilverStripe\View\Parsers\URLSegmentFilter;
|
use SilverStripe\View\Parsers\URLSegmentFilter;
|
||||||
|
use Symfony\Component\Validator\Constraints\NotBlank;
|
||||||
|
use Symfony\Component\Validator\Constraints\Url;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Library of conversion functions, implemented as static methods.
|
* Library of conversion functions, implemented as static methods.
|
||||||
@ -226,16 +229,14 @@ class Convert
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a link if the string is a valid URL
|
* Create a link if the string is a valid URL
|
||||||
*
|
|
||||||
* @param string $string The string to linkify
|
|
||||||
* @return string A link to the URL if string is a URL
|
|
||||||
*/
|
*/
|
||||||
public static function linkIfMatch($string)
|
public static function linkIfMatch(
|
||||||
{
|
string $string,
|
||||||
if (preg_match('/^[a-z+]+\:\/\/[a-zA-Z0-9$-_.+?&=!*\'()%]+$/', $string ?? '')) {
|
array $protocols = ['file', 'ftp', 'http', 'https', 'imap', 'nntp']
|
||||||
|
): string {
|
||||||
|
if (ConstraintValidator::validate($string, [new Url(protocols: $protocols), new NotBlank()])->isValid()) {
|
||||||
return "<a style=\"white-space: nowrap\" href=\"$string\">$string</a>";
|
return "<a style=\"white-space: nowrap\" href=\"$string\">$string</a>";
|
||||||
}
|
}
|
||||||
|
|
||||||
return $string;
|
return $string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -10,6 +10,8 @@ use SilverStripe\Security\Authenticator;
|
|||||||
use SilverStripe\Security\Security;
|
use SilverStripe\Security\Security;
|
||||||
use SilverStripe\View\HTML;
|
use SilverStripe\View\HTML;
|
||||||
use Closure;
|
use Closure;
|
||||||
|
use SilverStripe\Core\Validation\ConstraintValidator;
|
||||||
|
use Symfony\Component\Validator\Constraints\PasswordStrength;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Two masked input fields, checks for matching passwords.
|
* Two masked input fields, checks for matching passwords.
|
||||||
@ -25,34 +27,33 @@ class ConfirmedPasswordField extends FormField
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Minimum character length of the password.
|
* Minimum character length of the password.
|
||||||
*
|
|
||||||
* @var int
|
|
||||||
*/
|
*/
|
||||||
public $minLength = null;
|
public int $minLength = 0;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Maximum character length of the password.
|
* Maximum character length of the password.
|
||||||
*
|
* 0 means no maximum length.
|
||||||
* @var int
|
|
||||||
*/
|
*/
|
||||||
public $maxLength = null;
|
public int $maxLength = 0;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Enforces at least one digit and one alphanumeric
|
* Enforces password strength validation based on entropy.
|
||||||
* character (in addition to {$minLength} and {$maxLength}
|
* See setMinPasswordStrength()
|
||||||
*
|
|
||||||
* @var boolean
|
|
||||||
*/
|
*/
|
||||||
public $requireStrongPassword = false;
|
public bool $requireStrongPassword = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Allow empty fields when entering the password for the first time
|
* Allow empty fields when entering the password for the first time
|
||||||
* If this is set to true then a random password may be generated if the field is empty
|
* If this is set to true then a random password may be generated if the field is empty
|
||||||
* depending on the value of $ConfirmedPasswordField::generateRandomPasswordOnEmtpy
|
* depending on the value of $ConfirmedPasswordField::generateRandomPasswordOnEmtpy
|
||||||
*
|
|
||||||
* @var boolean
|
|
||||||
*/
|
*/
|
||||||
public $canBeEmpty = false;
|
public bool $canBeEmpty = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Minimum password strength if requireStrongPassword is true
|
||||||
|
* See https://symfony.com/doc/current/reference/constraints/PasswordStrength.html#minscore
|
||||||
|
*/
|
||||||
|
private int $minPasswordStrength = PasswordStrength::STRENGTH_STRONG;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Callback used to generate a random password if $this->canBeEmpty is true and the field is left blank
|
* Callback used to generate a random password if $this->canBeEmpty is true and the field is left blank
|
||||||
@ -72,79 +73,54 @@ class ConfirmedPasswordField extends FormField
|
|||||||
*
|
*
|
||||||
* Caution: The form field does not include any JavaScript or CSS when used outside of the CMS context,
|
* Caution: The form field does not include any JavaScript or CSS when used outside of the CMS context,
|
||||||
* since the required frontend dependencies are included through CMS bundling.
|
* since the required frontend dependencies are included through CMS bundling.
|
||||||
*
|
|
||||||
* @param boolean $showOnClick
|
|
||||||
*/
|
*/
|
||||||
protected $showOnClick = false;
|
protected bool $showOnClick = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if the existing password should be entered first
|
* Check if the existing password should be entered first
|
||||||
*
|
|
||||||
* @var bool
|
|
||||||
*/
|
*/
|
||||||
protected $requireExistingPassword = false;
|
protected bool $requireExistingPassword = false;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A place to temporarily store the confirm password value
|
* A place to temporarily store the confirm password value
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
*/
|
*/
|
||||||
protected $confirmValue;
|
protected ?string $confirmValue = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Store value of "Current Password" field
|
* Store value of "Current Password" field
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
*/
|
*/
|
||||||
protected $currentPasswordValue;
|
protected ?string $currentPasswordValue = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Title for the link that triggers the visibility of password fields.
|
* Title for the link that triggers the visibility of password fields.
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
*/
|
*/
|
||||||
public $showOnClickTitle;
|
public string $showOnClickTitle = '';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Child fields (_Password, _ConfirmPassword)
|
* Child fields (_Password, _ConfirmPassword)
|
||||||
*
|
|
||||||
* @var FieldList
|
|
||||||
*/
|
*/
|
||||||
public $children;
|
public FieldList $children;
|
||||||
|
|
||||||
protected $schemaDataType = FormField::SCHEMA_DATA_TYPE_STRUCTURAL;
|
protected $schemaDataType = FormField::SCHEMA_DATA_TYPE_STRUCTURAL;
|
||||||
|
|
||||||
/**
|
protected ?PasswordField $passwordField;
|
||||||
* @var PasswordField
|
|
||||||
*/
|
protected ?PasswordField $confirmPasswordfield;
|
||||||
protected $passwordField = null;
|
|
||||||
|
protected ?HiddenField $hiddenField = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var PasswordField
|
|
||||||
*/
|
|
||||||
protected $confirmPasswordfield = null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @var HiddenField
|
|
||||||
*/
|
|
||||||
protected $hiddenField = null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param string $name
|
|
||||||
* @param string $title
|
|
||||||
* @param mixed $value
|
|
||||||
* @param Form $form Ignored for ConfirmedPasswordField.
|
* @param Form $form Ignored for ConfirmedPasswordField.
|
||||||
* @param boolean $showOnClick
|
|
||||||
* @param string $titleConfirmField Alternate title (not localizeable)
|
* @param string $titleConfirmField Alternate title (not localizeable)
|
||||||
*/
|
*/
|
||||||
public function __construct(
|
public function __construct(
|
||||||
$name,
|
string $name,
|
||||||
$title = null,
|
?string $title = null,
|
||||||
$value = "",
|
mixed $value = "",
|
||||||
$form = null,
|
?Form $form = null,
|
||||||
$showOnClick = false,
|
bool $showOnClick = false,
|
||||||
$titleConfirmField = null
|
?string $titleConfirmField = null
|
||||||
) {
|
) {
|
||||||
|
|
||||||
// Set field title
|
// Set field title
|
||||||
@ -528,14 +504,18 @@ class ConfirmedPasswordField extends FormField
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($this->getRequireStrongPassword()) {
|
if ($this->getRequireStrongPassword()) {
|
||||||
if (!preg_match('/^(([a-zA-Z]+\d+)|(\d+[a-zA-Z]+))[a-zA-Z0-9]*$/', $value ?? '')) {
|
$strongEnough = ConstraintValidator::validate(
|
||||||
|
$value,
|
||||||
|
new PasswordStrength(minScore: $this->getMinPasswordStrength())
|
||||||
|
)->isValid();
|
||||||
|
if (!$strongEnough) {
|
||||||
$validator->validationError(
|
$validator->validationError(
|
||||||
$name,
|
$name,
|
||||||
_t(
|
_t(
|
||||||
'SilverStripe\\Forms\\Form.VALIDATIONSTRONGPASSWORD',
|
__CLASS__ . '.VALIDATIONSTRONGPASSWORD',
|
||||||
'Passwords must have at least one digit and one alphanumeric character'
|
'The password strength is too low. Please use a stronger password.'
|
||||||
),
|
),
|
||||||
"validation"
|
'validation'
|
||||||
);
|
);
|
||||||
|
|
||||||
return $this->extendValidationResult(false, $validator);
|
return $this->extendValidationResult(false, $validator);
|
||||||
@ -637,24 +617,21 @@ class ConfirmedPasswordField extends FormField
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if existing password is required
|
* Check if existing password is required
|
||||||
*
|
* If true, an extra form field will be added to enter the existing password
|
||||||
* @return bool
|
|
||||||
*/
|
*/
|
||||||
public function getRequireExistingPassword()
|
public function getRequireExistingPassword(): bool
|
||||||
{
|
{
|
||||||
return $this->requireExistingPassword;
|
return $this->requireExistingPassword;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set if the existing password should be required
|
* Set if the existing password should be required
|
||||||
*
|
* If true, an extra form field will be added to enter the existing password
|
||||||
* @param bool $show Flag to show or hide this field
|
|
||||||
* @return $this
|
|
||||||
*/
|
*/
|
||||||
public function setRequireExistingPassword($show)
|
public function setRequireExistingPassword(bool $show): static
|
||||||
{
|
{
|
||||||
// Don't modify if already added / removed
|
// Don't modify if already added / removed
|
||||||
if ((bool)$show === $this->requireExistingPassword) {
|
if ($show === $this->requireExistingPassword) {
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
$this->requireExistingPassword = $show;
|
$this->requireExistingPassword = $show;
|
||||||
@ -670,79 +647,91 @@ class ConfirmedPasswordField extends FormField
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return PasswordField
|
* Get the FormField that represents the main password field
|
||||||
*/
|
*/
|
||||||
public function getPasswordField()
|
public function getPasswordField(): PasswordField
|
||||||
{
|
{
|
||||||
return $this->passwordField;
|
return $this->passwordField;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return PasswordField
|
* Get the FormField that represents the "confirm" password field
|
||||||
*/
|
*/
|
||||||
public function getConfirmPasswordField()
|
public function getConfirmPasswordField(): PasswordField
|
||||||
{
|
{
|
||||||
return $this->confirmPasswordfield;
|
return $this->confirmPasswordfield;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the minimum length required for passwords
|
* Set the minimum length required for passwords
|
||||||
*
|
|
||||||
* @param int $minLength
|
|
||||||
* @return $this
|
|
||||||
*/
|
*/
|
||||||
public function setMinLength($minLength)
|
public function setMinLength(int $minLength): static
|
||||||
{
|
{
|
||||||
$this->minLength = (int) $minLength;
|
$this->minLength = $minLength;
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return int
|
* Get the minimum length required for passwords
|
||||||
*/
|
*/
|
||||||
public function getMinLength()
|
public function getMinLength(): int
|
||||||
{
|
{
|
||||||
return $this->minLength;
|
return $this->minLength;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the maximum length required for passwords
|
* Set the maximum length required for passwords.
|
||||||
*
|
* 0 means no max length.
|
||||||
* @param int $maxLength
|
|
||||||
* @return $this
|
|
||||||
*/
|
*/
|
||||||
public function setMaxLength($maxLength)
|
public function setMaxLength(int $maxLength): static
|
||||||
{
|
{
|
||||||
$this->maxLength = (int) $maxLength;
|
$this->maxLength = $maxLength;
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return int
|
* Get the maximum length required for passwords.
|
||||||
|
* 0 means no max length.
|
||||||
*/
|
*/
|
||||||
public function getMaxLength()
|
public function getMaxLength(): int
|
||||||
{
|
{
|
||||||
return $this->maxLength;
|
return $this->maxLength;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param bool $requireStrongPassword
|
* Set whether password strength validation is enforced.
|
||||||
* @return $this
|
* See setMinPasswordStrength()
|
||||||
*/
|
*/
|
||||||
public function setRequireStrongPassword($requireStrongPassword)
|
public function setRequireStrongPassword($requireStrongPassword): static
|
||||||
{
|
{
|
||||||
$this->requireStrongPassword = (bool) $requireStrongPassword;
|
$this->requireStrongPassword = (bool) $requireStrongPassword;
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return bool
|
* Get whether password strength validation is enforced.
|
||||||
|
* See setMinPasswordStrength()
|
||||||
*/
|
*/
|
||||||
public function getRequireStrongPassword()
|
public function getRequireStrongPassword(): bool
|
||||||
{
|
{
|
||||||
return $this->requireStrongPassword;
|
return $this->requireStrongPassword;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set minimum password strength. Only applies if requireStrongPassword is true
|
||||||
|
* See https://symfony.com/doc/current/reference/constraints/PasswordStrength.html#minscore
|
||||||
|
*/
|
||||||
|
public function setMinPasswordStrength(int $strength): static
|
||||||
|
{
|
||||||
|
$this->minPasswordStrength = $strength;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getMinPasswordStrength(): int
|
||||||
|
{
|
||||||
|
return $this->minPasswordStrength;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Appends a warning to the right title, or removes that appended warning.
|
* Appends a warning to the right title, or removes that appended warning.
|
||||||
*/
|
*/
|
||||||
|
@ -2,52 +2,40 @@
|
|||||||
|
|
||||||
namespace SilverStripe\Forms;
|
namespace SilverStripe\Forms;
|
||||||
|
|
||||||
|
use SilverStripe\Core\Validation\ConstraintValidator;
|
||||||
|
use Symfony\Component\Validator\Constraints\Email as EmailConstraint;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Text input field with validation for correct email format according to RFC 2822.
|
* Text input field with validation for correct email format according to the relevant RFC.
|
||||||
*/
|
*/
|
||||||
class EmailField extends TextField
|
class EmailField extends TextField
|
||||||
{
|
{
|
||||||
|
|
||||||
protected $inputType = 'email';
|
protected $inputType = 'email';
|
||||||
/**
|
|
||||||
* {@inheritdoc}
|
|
||||||
*/
|
|
||||||
public function Type()
|
public function Type()
|
||||||
{
|
{
|
||||||
return 'email text';
|
return 'email text';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validates for RFC 2822 compliant email addresses.
|
* Validates for RFC compliant email addresses.
|
||||||
*
|
|
||||||
* @see http://www.regular-expressions.info/email.html
|
|
||||||
* @see http://www.ietf.org/rfc/rfc2822.txt
|
|
||||||
*
|
*
|
||||||
* @param Validator $validator
|
* @param Validator $validator
|
||||||
*
|
|
||||||
* @return string
|
|
||||||
*/
|
*/
|
||||||
public function validate($validator)
|
public function validate($validator)
|
||||||
{
|
{
|
||||||
$result = true;
|
|
||||||
$this->value = trim($this->value ?? '');
|
$this->value = trim($this->value ?? '');
|
||||||
|
|
||||||
$pattern = '^[a-z0-9!#$%&\'*+/=?^_`{|}~-]+(?:\\.[a-z0-9!#$%&\'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$';
|
$message = _t('SilverStripe\\Forms\\EmailField.VALIDATION', 'Please enter an email address');
|
||||||
|
$result = ConstraintValidator::validate(
|
||||||
|
$this->value,
|
||||||
|
new EmailConstraint(message: $message, mode: EmailConstraint::VALIDATION_MODE_STRICT),
|
||||||
|
$this->getName()
|
||||||
|
);
|
||||||
|
$validator->getResult()->combineAnd($result);
|
||||||
|
$isValid = $result->isValid();
|
||||||
|
|
||||||
// Escape delimiter characters.
|
return $this->extendValidationResult($isValid, $validator);
|
||||||
$safePattern = str_replace('/', '\\/', $pattern ?? '');
|
|
||||||
|
|
||||||
if ($this->value && !preg_match('/' . $safePattern . '/i', $this->value ?? '')) {
|
|
||||||
$validator->validationError(
|
|
||||||
$this->name,
|
|
||||||
_t('SilverStripe\\Forms\\EmailField.VALIDATION', 'Please enter an email address'),
|
|
||||||
'validation'
|
|
||||||
);
|
|
||||||
|
|
||||||
$result = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->extendValidationResult($result, $validator);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getSchemaValidation()
|
public function getSchemaValidation()
|
||||||
|
@ -7,10 +7,24 @@ use Symfony\Component\Validator\Constraints\Url;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Text input field with validation for a url
|
* Text input field with validation for a url
|
||||||
* Url must include a scheme, either http:// or https://
|
* Url must include a protocol (aka scheme) such as https:// or http://
|
||||||
*/
|
*/
|
||||||
class UrlField extends TextField
|
class UrlField extends TextField
|
||||||
{
|
{
|
||||||
|
/**
|
||||||
|
* The default set of protocols allowed for valid URLs
|
||||||
|
*/
|
||||||
|
private static array $default_protocols = ['https', 'http'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default value for whether a relative protocol (// on its own) is allowed
|
||||||
|
*/
|
||||||
|
private static bool $default_allow_relative_protocol = false;
|
||||||
|
|
||||||
|
private array $protocols = [];
|
||||||
|
|
||||||
|
private ?bool $allowRelativeProtocol = null;
|
||||||
|
|
||||||
public function Type()
|
public function Type()
|
||||||
{
|
{
|
||||||
return 'text url';
|
return 'text url';
|
||||||
@ -18,15 +32,64 @@ class UrlField extends TextField
|
|||||||
|
|
||||||
public function validate($validator)
|
public function validate($validator)
|
||||||
{
|
{
|
||||||
$result = true;
|
$allowedProtocols = $this->getAllowedProtocols();
|
||||||
if ($this->value && !ConstraintValidator::validate($this->value, new Url())->isValid()) {
|
$message = _t(
|
||||||
$validator->validationError(
|
__CLASS__ . '.INVALID_WITH_PROTOCOL',
|
||||||
$this->name,
|
'Please enter a valid URL including a protocol, e.g {protocol}://example.com',
|
||||||
_t(__CLASS__ . '.INVALID', 'Please enter a valid URL'),
|
['protocol' => $allowedProtocols[0]]
|
||||||
'validation'
|
);
|
||||||
);
|
$result = ConstraintValidator::validate(
|
||||||
$result = false;
|
$this->value,
|
||||||
|
new Url(
|
||||||
|
message: $message,
|
||||||
|
protocols: $allowedProtocols,
|
||||||
|
relativeProtocol: $this->getAllowRelativeProtocol()
|
||||||
|
),
|
||||||
|
$this->getName()
|
||||||
|
);
|
||||||
|
$validator->getResult()->combineAnd($result);
|
||||||
|
$isValid = $result->isValid();
|
||||||
|
return $this->extendValidationResult($isValid, $validator);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set which protocols valid URLs are allowed to have
|
||||||
|
*/
|
||||||
|
public function setAllowedProtocols(array $protocols): static
|
||||||
|
{
|
||||||
|
// Ensure the array isn't associative so we can use 0 index in validate().
|
||||||
|
$this->protocols = array_keys($protocols);
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get which protocols valid URLs are allowed to have
|
||||||
|
*/
|
||||||
|
public function getAllowedProtocols(): array
|
||||||
|
{
|
||||||
|
if (empty($this->protocols)) {
|
||||||
|
return static::config()->get('default_protocols');
|
||||||
}
|
}
|
||||||
return $this->extendValidationResult($result, $validator);
|
return $this->protocols;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set whether a relative protocol (// on its own) is allowed
|
||||||
|
*/
|
||||||
|
public function setAllowRelativeProtocol(?bool $allow): static
|
||||||
|
{
|
||||||
|
$this->allowRelativeProtocol = $allow;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get whether a relative protocol (// on its own) is allowed
|
||||||
|
*/
|
||||||
|
public function getAllowRelativeProtocol(): bool
|
||||||
|
{
|
||||||
|
if ($this->allowRelativeProtocol === null) {
|
||||||
|
return static::config()->get('default_allow_relative_protocol');
|
||||||
|
}
|
||||||
|
return $this->allowRelativeProtocol;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -45,7 +45,8 @@ use SilverStripe\Forms\FormField;
|
|||||||
use SilverStripe\Forms\SearchableDropdownField;
|
use SilverStripe\Forms\SearchableDropdownField;
|
||||||
use SilverStripe\Forms\SearchableMultiDropdownField;
|
use SilverStripe\Forms\SearchableMultiDropdownField;
|
||||||
use SilverStripe\ORM\FieldType\DBForeignKey;
|
use SilverStripe\ORM\FieldType\DBForeignKey;
|
||||||
use SilverStripe\Dev\Deprecation;
|
use SilverStripe\Security\Validation\PasswordValidator;
|
||||||
|
use SilverStripe\Security\Validation\RulesPasswordValidator;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The member class which represents the users of the system
|
* The member class which represents the users of the system
|
||||||
@ -380,10 +381,8 @@ class Member extends DataObject
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Set a {@link PasswordValidator} object to use to validate member's passwords.
|
* Set a {@link PasswordValidator} object to use to validate member's passwords.
|
||||||
*
|
|
||||||
* @param PasswordValidator $validator
|
|
||||||
*/
|
*/
|
||||||
public static function set_password_validator(PasswordValidator $validator = null)
|
public static function set_password_validator(?PasswordValidator $validator = null)
|
||||||
{
|
{
|
||||||
// Override existing config
|
// Override existing config
|
||||||
Config::modify()->remove(Injector::class, PasswordValidator::class);
|
Config::modify()->remove(Injector::class, PasswordValidator::class);
|
||||||
@ -396,13 +395,11 @@ class Member extends DataObject
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the default {@link PasswordValidator}
|
* Returns the default {@link PasswordValidator}
|
||||||
*
|
|
||||||
* @return PasswordValidator|null
|
|
||||||
*/
|
*/
|
||||||
public static function password_validator()
|
public static function password_validator(): ?PasswordValidator
|
||||||
{
|
{
|
||||||
if (Injector::inst()->has(PasswordValidator::class)) {
|
if (Injector::inst()->has(PasswordValidator::class)) {
|
||||||
return Deprecation::withSuppressedNotice(fn() => Injector::inst()->get(PasswordValidator::class));
|
return Injector::inst()->get(PasswordValidator::class);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -1779,11 +1776,16 @@ class Member extends DataObject
|
|||||||
{
|
{
|
||||||
$password = '';
|
$password = '';
|
||||||
$validator = Member::password_validator();
|
$validator = Member::password_validator();
|
||||||
if ($length && $validator && $length < $validator->getMinLength()) {
|
if ($validator instanceof RulesPasswordValidator) {
|
||||||
throw new InvalidArgumentException('length argument is less than password validator minLength');
|
$validatorMinLength = $validator->getMinLength();
|
||||||
|
if ($length && $length < $validatorMinLength) {
|
||||||
|
throw new InvalidArgumentException('length argument is less than password validator minLength');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Make sure the password is long enough to beat even very strict entropy tests
|
||||||
|
$validatorMinLength = 128;
|
||||||
}
|
}
|
||||||
$validatorMinLength = $validator ? $validator->getMinLength() : 0;
|
$len = max($length, $validatorMinLength, 20);
|
||||||
$len = $length ?: max($validatorMinLength, 20);
|
|
||||||
// The default PasswordValidator checks the password includes the following four character sets
|
// The default PasswordValidator checks the password includes the following four character sets
|
||||||
$charsets = [
|
$charsets = [
|
||||||
'abcdefghijklmnopqrstuvwyxz',
|
'abcdefghijklmnopqrstuvwyxz',
|
||||||
|
33
src/Security/Validation/EntropyPasswordValidator.php
Normal file
33
src/Security/Validation/EntropyPasswordValidator.php
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace SilverStripe\Security\Validation;
|
||||||
|
|
||||||
|
use SilverStripe\Core\Extensible;
|
||||||
|
use SilverStripe\Core\Validation\ConstraintValidator;
|
||||||
|
use SilverStripe\Core\Validation\ValidationResult;
|
||||||
|
use SilverStripe\Security\Member;
|
||||||
|
use Symfony\Component\Validator\Constraints\NotBlank;
|
||||||
|
use Symfony\Component\Validator\Constraints\PasswordStrength;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates passwords based on entropy.
|
||||||
|
*/
|
||||||
|
class EntropyPasswordValidator extends PasswordValidator
|
||||||
|
{
|
||||||
|
use Extensible;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The strength of a valid password.
|
||||||
|
* See https://symfony.com/doc/current/reference/constraints/PasswordStrength.html#minscore
|
||||||
|
*/
|
||||||
|
private static int $password_strength = PasswordStrength::STRENGTH_STRONG;
|
||||||
|
|
||||||
|
public function validate(string $password, Member $member): ValidationResult
|
||||||
|
{
|
||||||
|
$minScore = static::config()->get('password_strength');
|
||||||
|
$result = ConstraintValidator::validate($password, [new PasswordStrength(minScore: $minScore), new NotBlank()]);
|
||||||
|
$result->combineAnd(parent::validate($password, $member));
|
||||||
|
$this->extend('updateValidatePassword', $password, $member, $result, $this);
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
}
|
72
src/Security/Validation/PasswordValidator.php
Normal file
72
src/Security/Validation/PasswordValidator.php
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace SilverStripe\Security\Validation;
|
||||||
|
|
||||||
|
use SilverStripe\Core\Config\Configurable;
|
||||||
|
use SilverStripe\Core\Injector\Injectable;
|
||||||
|
use SilverStripe\ORM\DataObject;
|
||||||
|
use SilverStripe\Core\Validation\ValidationResult;
|
||||||
|
use SilverStripe\Security\Member;
|
||||||
|
use SilverStripe\Security\MemberPassword;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abstract validator with functionality for checking for reusing old passwords.
|
||||||
|
*/
|
||||||
|
abstract class PasswordValidator
|
||||||
|
{
|
||||||
|
use Injectable;
|
||||||
|
use Configurable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default number of previous passwords to check for a reusing old passwords.
|
||||||
|
*/
|
||||||
|
private static int $historic_count = 6;
|
||||||
|
|
||||||
|
protected ?int $historicalPasswordCount = null;
|
||||||
|
|
||||||
|
public function validate(string $password, Member $member): ValidationResult
|
||||||
|
{
|
||||||
|
$result = ValidationResult::create();
|
||||||
|
|
||||||
|
$historicCount = $this->getHistoricCount();
|
||||||
|
if ($historicCount) {
|
||||||
|
$idColumn = DataObject::getSchema()->sqlColumnForField(MemberPassword::class, 'MemberID');
|
||||||
|
$previousPasswords = MemberPassword::get()
|
||||||
|
->where([$idColumn => $member->ID])
|
||||||
|
->sort(['Created' => 'DESC', 'ID' => 'DESC'])
|
||||||
|
->limit($historicCount);
|
||||||
|
foreach ($previousPasswords as $previousPassword) {
|
||||||
|
if ($previousPassword->checkPassword($password)) {
|
||||||
|
$error = _t(
|
||||||
|
PasswordValidator::class . '.PREVPASSWORD',
|
||||||
|
'You\'ve already used that password in the past, please choose a new password'
|
||||||
|
);
|
||||||
|
$result->addError($error, 'bad', 'PREVIOUS_PASSWORD');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the number of previous passwords to check for a reusing old passwords.
|
||||||
|
*/
|
||||||
|
public function getHistoricCount(): int
|
||||||
|
{
|
||||||
|
if ($this->historicalPasswordCount !== null) {
|
||||||
|
return $this->historicalPasswordCount;
|
||||||
|
}
|
||||||
|
return $this->config()->get('historic_count') ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the number of previous passwords to check for a reusing old passwords.
|
||||||
|
*/
|
||||||
|
public function setHistoricCount(int $count): static
|
||||||
|
{
|
||||||
|
$this->historicalPasswordCount = $count;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
}
|
@ -1,39 +1,32 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace SilverStripe\Security;
|
namespace SilverStripe\Security\Validation;
|
||||||
|
|
||||||
use SilverStripe\Core\Config\Configurable;
|
|
||||||
use SilverStripe\Core\Extensible;
|
use SilverStripe\Core\Extensible;
|
||||||
use SilverStripe\Core\Injector\Injectable;
|
|
||||||
use SilverStripe\Core\Validation\ValidationResult;
|
use SilverStripe\Core\Validation\ValidationResult;
|
||||||
use SilverStripe\Dev\Deprecation;
|
use SilverStripe\Security\Member;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This class represents a validator for member passwords.
|
* Validates passwords based on a set of regex rules about what the password must contain.
|
||||||
*
|
*
|
||||||
* <code>
|
* <code>
|
||||||
* $pwdVal = new PasswordValidator();
|
* $pwdVal = new PasswordValidator();
|
||||||
* $pwdValidator->setMinLength(7);
|
* $pwdValidator->setMinLength(7);
|
||||||
* $pwdValidator->checkHistoricalPasswords(6);
|
* $pwdValidator->setHistoricCount(6);
|
||||||
* $pwdValidator->setMinTestScore(3);
|
* $pwdValidator->setMinTestScore(3);
|
||||||
* $pwdValidator->setTestNames(array("lowercase", "uppercase", "digits", "punctuation"));
|
* $pwdValidator->setTestNames(array("lowercase", "uppercase", "digits", "punctuation"));
|
||||||
*
|
*
|
||||||
* Member::set_password_validator($pwdValidator);
|
* Member::set_password_validator($pwdValidator);
|
||||||
* </code>
|
* </code>
|
||||||
*
|
|
||||||
* @deprecated 5.4.0 Will be renamed to SilverStripe\Security\Validation\RulesPasswordValidator
|
|
||||||
*/
|
*/
|
||||||
class PasswordValidator
|
class RulesPasswordValidator extends PasswordValidator
|
||||||
{
|
{
|
||||||
use Injectable;
|
|
||||||
use Configurable;
|
|
||||||
use Extensible;
|
use Extensible;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @config
|
* Regex to test the password against. See min_test_score.
|
||||||
* @var array
|
|
||||||
*/
|
*/
|
||||||
private static $character_strength_tests = [
|
private static array $character_strength_tests = [
|
||||||
'lowercase' => '/[a-z]/',
|
'lowercase' => '/[a-z]/',
|
||||||
'uppercase' => '/[A-Z]/',
|
'uppercase' => '/[A-Z]/',
|
||||||
'digits' => '/[0-9]/',
|
'digits' => '/[0-9]/',
|
||||||
@ -41,89 +34,62 @@ class PasswordValidator
|
|||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @config
|
* Default minimum number of characters for a valid password.
|
||||||
* @var int
|
|
||||||
*/
|
*/
|
||||||
private static $min_length = null;
|
private static int $min_length = 8;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @config
|
* Default minimum test score for a valid password.
|
||||||
* @var int
|
* The test score is the number of character_strength_tests that the password matches.
|
||||||
*/
|
*/
|
||||||
private static $min_test_score = null;
|
private static int $min_test_score = 0;
|
||||||
|
|
||||||
/**
|
protected ?int $minLength = null;
|
||||||
* @config
|
|
||||||
* @var int
|
|
||||||
*/
|
|
||||||
private static $historic_count = null;
|
|
||||||
|
|
||||||
/**
|
protected ?int $minScore = null;
|
||||||
* @var int
|
|
||||||
*/
|
|
||||||
protected $minLength = null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @var int
|
|
||||||
*/
|
|
||||||
protected $minScore = null;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var string[]
|
* @var string[]
|
||||||
*/
|
*/
|
||||||
protected $testNames = null;
|
protected ?array $testNames = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var int
|
* Get the minimum number of characters for a valid password.
|
||||||
*/
|
*/
|
||||||
protected $historicalPasswordCount = null;
|
public function getMinLength(): int
|
||||||
|
|
||||||
public function __construct()
|
|
||||||
{
|
|
||||||
Deprecation::notice(
|
|
||||||
'5.4.0',
|
|
||||||
'Will be renamed to SilverStripe\Security\Validation\RulesPasswordValidator',
|
|
||||||
Deprecation::SCOPE_CLASS
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return int
|
|
||||||
*/
|
|
||||||
public function getMinLength()
|
|
||||||
{
|
{
|
||||||
if ($this->minLength !== null) {
|
if ($this->minLength !== null) {
|
||||||
return $this->minLength;
|
return $this->minLength;
|
||||||
}
|
}
|
||||||
return $this->config()->get('min_length');
|
return $this->config()->get('min_length') ?? 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param int $minLength
|
* Set the minimum number of characters for a valid password.
|
||||||
* @return $this
|
|
||||||
*/
|
*/
|
||||||
public function setMinLength($minLength)
|
public function setMinLength(int $minLength): static
|
||||||
{
|
{
|
||||||
$this->minLength = $minLength;
|
$this->minLength = $minLength;
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return integer
|
* Get the minimum test score for a valid password.
|
||||||
|
* The test score is the number of character_strength_tests that the password matches.
|
||||||
*/
|
*/
|
||||||
public function getMinTestScore()
|
public function getMinTestScore(): int
|
||||||
{
|
{
|
||||||
if ($this->minScore !== null) {
|
if ($this->minScore !== null) {
|
||||||
return $this->minScore;
|
return $this->minScore;
|
||||||
}
|
}
|
||||||
return $this->config()->get('min_test_score');
|
return $this->config()->get('min_test_score') ?? 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param int $minScore
|
* Set the minimum test score for a valid password.
|
||||||
* @return $this
|
* The test score is the number of character_strength_tests that the password matches.
|
||||||
*/
|
*/
|
||||||
public function setMinTestScore($minScore)
|
public function setMinTestScore(int $minScore): static
|
||||||
{
|
{
|
||||||
$this->minScore = $minScore;
|
$this->minScore = $minScore;
|
||||||
return $this;
|
return $this;
|
||||||
@ -134,7 +100,7 @@ class PasswordValidator
|
|||||||
*
|
*
|
||||||
* @return string[]
|
* @return string[]
|
||||||
*/
|
*/
|
||||||
public function getTestNames()
|
public function getTestNames(): array
|
||||||
{
|
{
|
||||||
if ($this->testNames !== null) {
|
if ($this->testNames !== null) {
|
||||||
return $this->testNames;
|
return $this->testNames;
|
||||||
@ -146,51 +112,22 @@ class PasswordValidator
|
|||||||
* Set list of tests to use for this validator
|
* Set list of tests to use for this validator
|
||||||
*
|
*
|
||||||
* @param string[] $testNames
|
* @param string[] $testNames
|
||||||
* @return $this
|
|
||||||
*/
|
*/
|
||||||
public function setTestNames($testNames)
|
public function setTestNames(array $testNames): static
|
||||||
{
|
{
|
||||||
$this->testNames = $testNames;
|
$this->testNames = $testNames;
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @return int
|
|
||||||
*/
|
|
||||||
public function getHistoricCount()
|
|
||||||
{
|
|
||||||
if ($this->historicalPasswordCount !== null) {
|
|
||||||
return $this->historicalPasswordCount;
|
|
||||||
}
|
|
||||||
return $this->config()->get('historic_count');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param int $count
|
|
||||||
* @return $this
|
|
||||||
*/
|
|
||||||
public function setHistoricCount($count)
|
|
||||||
{
|
|
||||||
$this->historicalPasswordCount = $count;
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets all possible tests
|
* Gets all possible tests
|
||||||
*
|
|
||||||
* @return array
|
|
||||||
*/
|
*/
|
||||||
public function getTests()
|
public function getTests(): array
|
||||||
{
|
{
|
||||||
return $this->config()->get('character_strength_tests');
|
return $this->config()->get('character_strength_tests');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public function validate(string $password, Member $member): ValidationResult
|
||||||
* @param string $password
|
|
||||||
* @param Member $member
|
|
||||||
* @return ValidationResult
|
|
||||||
*/
|
|
||||||
public function validate($password, $member)
|
|
||||||
{
|
{
|
||||||
$valid = ValidationResult::create();
|
$valid = ValidationResult::create();
|
||||||
|
|
||||||
@ -234,23 +171,7 @@ class PasswordValidator
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$historicCount = $this->getHistoricCount();
|
$valid->combineAnd(parent::validate($password, $member));
|
||||||
if ($historicCount) {
|
|
||||||
$previousPasswords = MemberPassword::get()
|
|
||||||
->where(['"MemberPassword"."MemberID"' => $member->ID])
|
|
||||||
->sort('"Created" DESC, "ID" DESC')
|
|
||||||
->limit($historicCount);
|
|
||||||
foreach ($previousPasswords as $previousPassword) {
|
|
||||||
if ($previousPassword->checkPassword($password)) {
|
|
||||||
$error = _t(
|
|
||||||
__CLASS__ . '.PREVPASSWORD',
|
|
||||||
'You\'ve already used that password in the past, please choose a new password'
|
|
||||||
);
|
|
||||||
$valid->addError($error, 'bad', 'PREVIOUS_PASSWORD');
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->extend('updateValidatePassword', $password, $member, $valid, $this);
|
$this->extend('updateValidatePassword', $password, $member, $valid, $this);
|
||||||
|
|
@ -11,11 +11,9 @@ use SilverStripe\Forms\Form;
|
|||||||
use SilverStripe\Forms\ReadonlyField;
|
use SilverStripe\Forms\ReadonlyField;
|
||||||
use SilverStripe\Forms\RequiredFields;
|
use SilverStripe\Forms\RequiredFields;
|
||||||
use SilverStripe\Security\Member;
|
use SilverStripe\Security\Member;
|
||||||
use SilverStripe\Security\PasswordValidator;
|
|
||||||
use SilverStripe\View\SSViewer;
|
use SilverStripe\View\SSViewer;
|
||||||
use Closure;
|
use Closure;
|
||||||
use PHPUnit\Framework\Attributes\DataProvider;
|
use PHPUnit\Framework\Attributes\DataProvider;
|
||||||
use SilverStripe\Dev\Deprecation;
|
|
||||||
|
|
||||||
class ConfirmedPasswordFieldTest extends SapphireTest
|
class ConfirmedPasswordFieldTest extends SapphireTest
|
||||||
{
|
{
|
||||||
@ -25,11 +23,7 @@ class ConfirmedPasswordFieldTest extends SapphireTest
|
|||||||
{
|
{
|
||||||
parent::setUp();
|
parent::setUp();
|
||||||
|
|
||||||
Deprecation::withSuppressedNotice(
|
Member::set_password_validator(null);
|
||||||
fn() => PasswordValidator::singleton()
|
|
||||||
->setMinLength(0)
|
|
||||||
->setTestNames([])
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testSetValue()
|
public function testSetValue()
|
||||||
@ -217,10 +211,10 @@ class ConfirmedPasswordFieldTest extends SapphireTest
|
|||||||
return [
|
return [
|
||||||
'valid: within min and max' => [3, 8, true],
|
'valid: within min and max' => [3, 8, true],
|
||||||
'invalid: lower than min with max' => [8, 12, false, 'Passwords must be 8 to 12 characters long'],
|
'invalid: lower than min with max' => [8, 12, false, 'Passwords must be 8 to 12 characters long'],
|
||||||
'valid: greater than min' => [3, null, true],
|
'valid: greater than min' => [3, 0, true],
|
||||||
'invalid: lower than min' => [8, null, false, 'Passwords must be at least 8 characters long'],
|
'invalid: lower than min' => [8, 0, false, 'Passwords must be at least 8 characters long'],
|
||||||
'valid: less than max' => [null, 8, true],
|
'valid: less than max' => [0, 8, true],
|
||||||
'invalid: greater than max' => [null, 4, false, 'Passwords must be at most 4 characters long'],
|
'invalid: greater than max' => [0, 4, false, 'Passwords must be at most 4 characters long'],
|
||||||
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@ -239,7 +233,7 @@ class ConfirmedPasswordFieldTest extends SapphireTest
|
|||||||
$this->assertFalse($result, 'Validate method should return its result');
|
$this->assertFalse($result, 'Validate method should return its result');
|
||||||
$this->assertFalse($validator->getResult()->isValid());
|
$this->assertFalse($validator->getResult()->isValid());
|
||||||
$this->assertStringContainsString(
|
$this->assertStringContainsString(
|
||||||
'Passwords must have at least one digit and one alphanumeric character',
|
'The password strength is too low. Please use a stronger password.',
|
||||||
json_encode($validator->getResult()->__serialize())
|
json_encode($validator->getResult()->__serialize())
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -4,9 +4,7 @@ namespace SilverStripe\Forms\Tests;
|
|||||||
|
|
||||||
use SilverStripe\Dev\FunctionalTest;
|
use SilverStripe\Dev\FunctionalTest;
|
||||||
use SilverStripe\Forms\EmailField;
|
use SilverStripe\Forms\EmailField;
|
||||||
use Exception;
|
use SilverStripe\Forms\FieldsValidator;
|
||||||
use PHPUnit\Framework\AssertionFailedError;
|
|
||||||
use SilverStripe\Forms\Tests\EmailFieldTest\TestValidator;
|
|
||||||
|
|
||||||
class EmailFieldTest extends FunctionalTest
|
class EmailFieldTest extends FunctionalTest
|
||||||
{
|
{
|
||||||
@ -40,21 +38,14 @@ class EmailFieldTest extends FunctionalTest
|
|||||||
$field = new EmailField("MyEmail");
|
$field = new EmailField("MyEmail");
|
||||||
$field->setValue($email);
|
$field->setValue($email);
|
||||||
|
|
||||||
$val = new TestValidator();
|
if ($expectSuccess) {
|
||||||
try {
|
$message = $checkText . " (/$email/ did not pass validation, but was expected to)";
|
||||||
$field->validate($val);
|
} else {
|
||||||
// If we expect failure and processing gets here without an exception, the test failed
|
$message = $checkText . " (/$email/ passed validation, but not expected to)";
|
||||||
$this->assertTrue($expectSuccess, $checkText . " (/$email/ passed validation, but not expected to)");
|
|
||||||
} catch (Exception $e) {
|
|
||||||
if ($e instanceof AssertionFailedError) {
|
|
||||||
// re-throw assertion failure
|
|
||||||
throw $e;
|
|
||||||
} elseif ($expectSuccess) {
|
|
||||||
$this->fail(
|
|
||||||
$checkText . ": " . $e->getMessage() . " (/$email/ did not pass validation, but was expected to)"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$result = $field->validate(new FieldsValidator());
|
||||||
|
$this->assertSame($expectSuccess, $result, $message);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1,27 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace SilverStripe\Forms\Tests\EmailFieldTest;
|
|
||||||
|
|
||||||
use Exception;
|
|
||||||
use SilverStripe\Forms\Validator;
|
|
||||||
use SilverStripe\Core\Validation\ValidationResult;
|
|
||||||
|
|
||||||
class TestValidator extends Validator
|
|
||||||
{
|
|
||||||
public function validationError(
|
|
||||||
$fieldName,
|
|
||||||
$message,
|
|
||||||
$messageType = ValidationResult::TYPE_ERROR,
|
|
||||||
$cast = ValidationResult::CAST_TEXT
|
|
||||||
) {
|
|
||||||
throw new Exception($message);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function javascript()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public function php($data)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
@ -6,7 +6,6 @@ use SilverStripe\Control\Controller;
|
|||||||
use SilverStripe\Control\NullHTTPRequest;
|
use SilverStripe\Control\NullHTTPRequest;
|
||||||
use SilverStripe\Core\Config\Config;
|
use SilverStripe\Core\Config\Config;
|
||||||
use SilverStripe\Core\Injector\Injector;
|
use SilverStripe\Core\Injector\Injector;
|
||||||
use SilverStripe\Dev\Deprecation;
|
|
||||||
use SilverStripe\Dev\SapphireTest;
|
use SilverStripe\Dev\SapphireTest;
|
||||||
use SilverStripe\ORM\FieldType\DBDatetime;
|
use SilverStripe\ORM\FieldType\DBDatetime;
|
||||||
use SilverStripe\Core\Validation\ValidationResult;
|
use SilverStripe\Core\Validation\ValidationResult;
|
||||||
@ -19,7 +18,6 @@ use SilverStripe\Security\MemberAuthenticator\CMSMemberAuthenticator;
|
|||||||
use SilverStripe\Security\MemberAuthenticator\CMSMemberLoginForm;
|
use SilverStripe\Security\MemberAuthenticator\CMSMemberLoginForm;
|
||||||
use SilverStripe\Security\MemberAuthenticator\MemberAuthenticator;
|
use SilverStripe\Security\MemberAuthenticator\MemberAuthenticator;
|
||||||
use SilverStripe\Security\MemberAuthenticator\MemberLoginForm;
|
use SilverStripe\Security\MemberAuthenticator\MemberLoginForm;
|
||||||
use SilverStripe\Security\PasswordValidator;
|
|
||||||
use SilverStripe\Security\Security;
|
use SilverStripe\Security\Security;
|
||||||
|
|
||||||
class MemberAuthenticatorTest extends SapphireTest
|
class MemberAuthenticatorTest extends SapphireTest
|
||||||
@ -44,12 +42,8 @@ class MemberAuthenticatorTest extends SapphireTest
|
|||||||
}
|
}
|
||||||
DefaultAdminService::setDefaultAdmin('admin', 'password');
|
DefaultAdminService::setDefaultAdmin('admin', 'password');
|
||||||
|
|
||||||
// Enforce dummy validation (this can otherwise be influenced by recipe config)
|
// Enforce no password validation
|
||||||
Deprecation::withSuppressedNotice(
|
Member::set_password_validator(null);
|
||||||
fn() => PasswordValidator::singleton()
|
|
||||||
->setMinLength(0)
|
|
||||||
->setTestNames([])
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function tearDown(): void
|
protected function tearDown(): void
|
||||||
|
@ -2,12 +2,10 @@
|
|||||||
|
|
||||||
namespace SilverStripe\Security\Tests;
|
namespace SilverStripe\Security\Tests;
|
||||||
|
|
||||||
use SilverStripe\Dev\Deprecation;
|
|
||||||
use SilverStripe\ORM\DataObject;
|
use SilverStripe\ORM\DataObject;
|
||||||
use SilverStripe\Security\Group;
|
use SilverStripe\Security\Group;
|
||||||
use SilverStripe\Security\MemberCsvBulkLoader;
|
use SilverStripe\Security\MemberCsvBulkLoader;
|
||||||
use SilverStripe\Security\Member;
|
use SilverStripe\Security\Member;
|
||||||
use SilverStripe\Security\PasswordValidator;
|
|
||||||
use SilverStripe\Security\Security;
|
use SilverStripe\Security\Security;
|
||||||
use SilverStripe\Dev\SapphireTest;
|
use SilverStripe\Dev\SapphireTest;
|
||||||
use SilverStripe\Security\MemberAuthenticator\MemberAuthenticator;
|
use SilverStripe\Security\MemberAuthenticator\MemberAuthenticator;
|
||||||
@ -19,12 +17,7 @@ class MemberCsvBulkLoaderTest extends SapphireTest
|
|||||||
protected function setUp(): void
|
protected function setUp(): void
|
||||||
{
|
{
|
||||||
parent::setUp();
|
parent::setUp();
|
||||||
|
Member::set_password_validator(null);
|
||||||
Deprecation::withSuppressedNotice(
|
|
||||||
fn() => PasswordValidator::singleton()
|
|
||||||
->setMinLength(0)
|
|
||||||
->setTestNames([])
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testNewImport()
|
public function testNewImport()
|
||||||
|
@ -8,7 +8,6 @@ use SilverStripe\Control\Cookie;
|
|||||||
use SilverStripe\Core\Config\Config;
|
use SilverStripe\Core\Config\Config;
|
||||||
use SilverStripe\Core\Convert;
|
use SilverStripe\Core\Convert;
|
||||||
use SilverStripe\Core\Injector\Injector;
|
use SilverStripe\Core\Injector\Injector;
|
||||||
use SilverStripe\Dev\Deprecation;
|
|
||||||
use SilverStripe\Dev\FunctionalTest;
|
use SilverStripe\Dev\FunctionalTest;
|
||||||
use SilverStripe\Forms\CheckboxField;
|
use SilverStripe\Forms\CheckboxField;
|
||||||
use SilverStripe\Forms\FieldList;
|
use SilverStripe\Forms\FieldList;
|
||||||
@ -29,7 +28,6 @@ use SilverStripe\Security\MemberAuthenticator\MemberAuthenticator;
|
|||||||
use SilverStripe\Security\MemberAuthenticator\SessionAuthenticationHandler;
|
use SilverStripe\Security\MemberAuthenticator\SessionAuthenticationHandler;
|
||||||
use SilverStripe\Security\MemberPassword;
|
use SilverStripe\Security\MemberPassword;
|
||||||
use SilverStripe\Security\PasswordEncryptor_Blowfish;
|
use SilverStripe\Security\PasswordEncryptor_Blowfish;
|
||||||
use SilverStripe\Security\PasswordValidator;
|
|
||||||
use SilverStripe\Security\Permission;
|
use SilverStripe\Security\Permission;
|
||||||
use SilverStripe\Security\RememberLoginHash;
|
use SilverStripe\Security\RememberLoginHash;
|
||||||
use SilverStripe\Security\Security;
|
use SilverStripe\Security\Security;
|
||||||
@ -37,6 +35,8 @@ use SilverStripe\Security\Tests\MemberTest\FieldsExtension;
|
|||||||
use SilverStripe\SessionManager\Models\LoginSession;
|
use SilverStripe\SessionManager\Models\LoginSession;
|
||||||
use ReflectionMethod;
|
use ReflectionMethod;
|
||||||
use PHPUnit\Framework\Attributes\DataProvider;
|
use PHPUnit\Framework\Attributes\DataProvider;
|
||||||
|
use SilverStripe\Security\Validation\EntropyPasswordValidator;
|
||||||
|
use SilverStripe\Security\Validation\RulesPasswordValidator;
|
||||||
|
|
||||||
class MemberTest extends FunctionalTest
|
class MemberTest extends FunctionalTest
|
||||||
{
|
{
|
||||||
@ -75,12 +75,6 @@ class MemberTest extends FunctionalTest
|
|||||||
|
|
||||||
Member::config()->set('unique_identifier_field', 'Email');
|
Member::config()->set('unique_identifier_field', 'Email');
|
||||||
|
|
||||||
Deprecation::withSuppressedNotice(
|
|
||||||
fn() => PasswordValidator::singleton()
|
|
||||||
->setMinLength(0)
|
|
||||||
->setTestNames([])
|
|
||||||
);
|
|
||||||
|
|
||||||
i18n::set_locale('en_US');
|
i18n::set_locale('en_US');
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1743,7 +1737,7 @@ class MemberTest extends FunctionalTest
|
|||||||
public function testChangePasswordOnlyValidatesPlaintext()
|
public function testChangePasswordOnlyValidatesPlaintext()
|
||||||
{
|
{
|
||||||
// This validator requires passwords to be 17 characters long
|
// This validator requires passwords to be 17 characters long
|
||||||
Member::set_password_validator(Deprecation::withSuppressedNotice(fn() => new MemberTest\VerySpecificPasswordValidator()));
|
Member::set_password_validator(new MemberTest\VerySpecificPasswordValidator());
|
||||||
|
|
||||||
// This algorithm will never return a 17 character hash
|
// This algorithm will never return a 17 character hash
|
||||||
Security::config()->set('password_encryption_algorithm', 'blowfish');
|
Security::config()->set('password_encryption_algorithm', 'blowfish');
|
||||||
@ -1770,11 +1764,23 @@ class MemberTest extends FunctionalTest
|
|||||||
$this->assertNotNull(Member::get()->find('Email', 'trimmed@test.com'));
|
$this->assertNotNull(Member::get()->find('Email', 'trimmed@test.com'));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testChangePasswordToBlankIsValidated()
|
public static function provideChangePasswordToBlankIsValidated(): array
|
||||||
{
|
{
|
||||||
Member::set_password_validator(Deprecation::withSuppressedNotice(fn() => new PasswordValidator()));
|
return [
|
||||||
// override setup() function which setMinLength(0)
|
[
|
||||||
PasswordValidator::singleton()->setMinLength(8);
|
'validatorClass' => RulesPasswordValidator::class,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'validatorClass' => EntropyPasswordValidator::class,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
#[DataProvider('provideChangePasswordToBlankIsValidated')]
|
||||||
|
public function testChangePasswordToBlankIsValidated(string $validatorClass): void
|
||||||
|
{
|
||||||
|
$validator = new $validatorClass();
|
||||||
|
Member::set_password_validator($validator);
|
||||||
// 'test' member has a password defined in yml
|
// 'test' member has a password defined in yml
|
||||||
$member = $this->objFromFixture(Member::class, 'test');
|
$member = $this->objFromFixture(Member::class, 'test');
|
||||||
$result = $member->changePassword('');
|
$result = $member->changePassword('');
|
||||||
@ -1896,34 +1902,57 @@ class MemberTest extends FunctionalTest
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testGenerateRandomPassword()
|
public static function provideGenerateRandomPassword(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
[
|
||||||
|
'validatorClass' => RulesPasswordValidator::class,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'validatorClass' => EntropyPasswordValidator::class,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
#[DataProvider('provideGenerateRandomPassword')]
|
||||||
|
public function testGenerateRandomPassword(string $validatorClass): void
|
||||||
{
|
{
|
||||||
$member = new Member();
|
$member = new Member();
|
||||||
// no password validator
|
// no password validator
|
||||||
Member::set_password_validator(null);
|
Member::set_password_validator(null);
|
||||||
// password length is same as length argument
|
// password length is min 128 chars long
|
||||||
$password = $member->generateRandomPassword(5);
|
$password = $member->generateRandomPassword(5);
|
||||||
$this->assertSame(5, strlen($password));
|
$this->assertSame(128, strlen($password));
|
||||||
// default to 20 if not length argument
|
// password length can be longer
|
||||||
$password = $member->generateRandomPassword();
|
$password = $member->generateRandomPassword(130);
|
||||||
$this->assertSame(20, strlen($password));
|
$this->assertSame(130, strlen($password));
|
||||||
// password validator
|
// password validator
|
||||||
$validator = Deprecation::withSuppressedNotice(fn() => new PasswordValidator());
|
$validator = new $validatorClass();
|
||||||
Member::set_password_validator($validator);
|
Member::set_password_validator($validator);
|
||||||
// Password length of 20 even if validator minLength is less than 20
|
if ($validator instanceof RulesPasswordValidator) {
|
||||||
$validator->setMinLength(10);
|
// Password length of 20 even if validator minLength is less than 20
|
||||||
|
$validator->setMinLength(10);
|
||||||
|
$minLengthInMember = 20;
|
||||||
|
} else {
|
||||||
|
$minLengthInMember = 128;
|
||||||
|
}
|
||||||
$password = $member->generateRandomPassword();
|
$password = $member->generateRandomPassword();
|
||||||
$this->assertSame(20, strlen($password));
|
$this->assertSame($minLengthInMember, strlen($password));
|
||||||
// Password length of 25 if passing length argument, and validator minlength is less than length argument
|
// Password length of 256 if passing length argument, and validator minlength is less than length argument
|
||||||
$password = $member->generateRandomPassword(25);
|
$password = $member->generateRandomPassword(256);
|
||||||
$this->assertSame(25, strlen($password));
|
$this->assertSame(256, strlen($password));
|
||||||
// Password length is validator minLength if validator minLength is greater than 20 and no length argument
|
if ($validator instanceof RulesPasswordValidator) {
|
||||||
$validator->setMinLength(30);
|
// Password length is validator minLength if validator minLength is greater than 20 and no length argument
|
||||||
$password = $member->generateRandomPassword();
|
$validator->setMinLength(30);
|
||||||
$this->assertSame(30, strlen($password));
|
$password = $member->generateRandomPassword();
|
||||||
// Exception throw if length argument is less than validator minLength
|
$this->assertSame(30, strlen($password));
|
||||||
$this->expectException(InvalidArgumentException::class);
|
// Exception throw if length argument is less than validator minLength
|
||||||
$this->expectExceptionMessage('length argument is less than password validator minLength');
|
$this->expectException(InvalidArgumentException::class);
|
||||||
$password = $member->generateRandomPassword(15);
|
$this->expectExceptionMessage('length argument is less than password validator minLength');
|
||||||
|
$password = $member->generateRandomPassword(15);
|
||||||
|
} else {
|
||||||
|
// No exception for entropy validator
|
||||||
|
$password = $member->generateRandomPassword(15);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,9 +3,9 @@
|
|||||||
namespace SilverStripe\Security\Tests\MemberTest;
|
namespace SilverStripe\Security\Tests\MemberTest;
|
||||||
|
|
||||||
use SilverStripe\Dev\TestOnly;
|
use SilverStripe\Dev\TestOnly;
|
||||||
use SilverStripe\Security\PasswordValidator;
|
use SilverStripe\Security\Validation\RulesPasswordValidator;
|
||||||
|
|
||||||
class TestPasswordValidator extends PasswordValidator implements TestOnly
|
class TestPasswordValidator extends RulesPasswordValidator implements TestOnly
|
||||||
{
|
{
|
||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
|
@ -4,11 +4,12 @@ namespace SilverStripe\Security\Tests\MemberTest;
|
|||||||
|
|
||||||
use SilverStripe\Dev\TestOnly;
|
use SilverStripe\Dev\TestOnly;
|
||||||
use SilverStripe\Core\Validation\ValidationResult;
|
use SilverStripe\Core\Validation\ValidationResult;
|
||||||
use SilverStripe\Security\PasswordValidator;
|
use SilverStripe\Security\Member;
|
||||||
|
use SilverStripe\Security\Validation\PasswordValidator;
|
||||||
|
|
||||||
class VerySpecificPasswordValidator extends PasswordValidator implements TestOnly
|
class VerySpecificPasswordValidator extends PasswordValidator implements TestOnly
|
||||||
{
|
{
|
||||||
public function validate($password, $member)
|
public function validate(string $password, Member $member): ValidationResult
|
||||||
{
|
{
|
||||||
$result = ValidationResult::create();
|
$result = ValidationResult::create();
|
||||||
if (strlen($password ?? '') !== 17) {
|
if (strlen($password ?? '') !== 17) {
|
||||||
|
@ -22,7 +22,6 @@ use SilverStripe\Core\Validation\ValidationResult;
|
|||||||
use SilverStripe\Security\LoginAttempt;
|
use SilverStripe\Security\LoginAttempt;
|
||||||
use SilverStripe\Security\Member;
|
use SilverStripe\Security\Member;
|
||||||
use SilverStripe\Security\MemberAuthenticator\MemberAuthenticator;
|
use SilverStripe\Security\MemberAuthenticator\MemberAuthenticator;
|
||||||
use SilverStripe\Security\PasswordValidator;
|
|
||||||
use SilverStripe\Security\Security;
|
use SilverStripe\Security\Security;
|
||||||
use SilverStripe\Security\SecurityToken;
|
use SilverStripe\Security\SecurityToken;
|
||||||
|
|
||||||
@ -60,11 +59,6 @@ class SecurityTest extends FunctionalTest
|
|||||||
|
|
||||||
Member::config()->set('unique_identifier_field', 'Email');
|
Member::config()->set('unique_identifier_field', 'Email');
|
||||||
|
|
||||||
PasswordValidator::config()
|
|
||||||
->remove('min_length')
|
|
||||||
->remove('historic_count')
|
|
||||||
->remove('min_test_score');
|
|
||||||
|
|
||||||
Member::set_password_validator(null);
|
Member::set_password_validator(null);
|
||||||
|
|
||||||
parent::setUp();
|
parent::setUp();
|
||||||
|
@ -0,0 +1,42 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace SilverStripe\Security\Tests\Validation;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\Attributes\DataProvider;
|
||||||
|
use SilverStripe\Dev\SapphireTest;
|
||||||
|
use SilverStripe\Security\Member;
|
||||||
|
use SilverStripe\Security\Validation\EntropyPasswordValidator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* EntropyPasswordValidator uses a third-party for its validation so we don't need rigorous testing here.
|
||||||
|
* Just test that stupid simple passwords don't pass, and complex ones do.
|
||||||
|
*/
|
||||||
|
class EntropyPasswordValidatorTest extends SapphireTest
|
||||||
|
{
|
||||||
|
protected $usesDatabase = false;
|
||||||
|
|
||||||
|
public static function provideValidate(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
[
|
||||||
|
'password' => '',
|
||||||
|
'expected' => false,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'password' => 'password123',
|
||||||
|
'expected' => false,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'password' => 'This is a really long and complex PASSWORD',
|
||||||
|
'expected' => true,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
#[DataProvider('provideValidate')]
|
||||||
|
public function testValidate(string $password, bool $expected): void
|
||||||
|
{
|
||||||
|
$validator = new EntropyPasswordValidator();
|
||||||
|
$this->assertSame($expected, $validator->validate($password, new Member())->isValid());
|
||||||
|
}
|
||||||
|
}
|
40
tests/php/Security/Validation/PasswordValidatorTest.php
Normal file
40
tests/php/Security/Validation/PasswordValidatorTest.php
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace SilverStripe\Security\Tests\Validation;
|
||||||
|
|
||||||
|
use SilverStripe\Dev\SapphireTest;
|
||||||
|
use SilverStripe\Security\Member;
|
||||||
|
use SilverStripe\Security\Tests\Validation\RulesPasswordValidatorTest\DummyPasswordValidator;
|
||||||
|
|
||||||
|
class PasswordValidatorTest extends SapphireTest
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
* @var bool
|
||||||
|
*/
|
||||||
|
protected $usesDatabase = true;
|
||||||
|
|
||||||
|
public function testValidate()
|
||||||
|
{
|
||||||
|
$validator = new DummyPasswordValidator;
|
||||||
|
$validator->setHistoricCount(3);
|
||||||
|
Member::set_password_validator($validator);
|
||||||
|
|
||||||
|
$member = new Member;
|
||||||
|
$member->FirstName = 'Repeat';
|
||||||
|
$member->Surname = 'Password-Man';
|
||||||
|
$member->Password = 'honk';
|
||||||
|
$member->write();
|
||||||
|
|
||||||
|
// Create a set of used passwords
|
||||||
|
$member->changePassword('foobar');
|
||||||
|
$member->changePassword('foobaz');
|
||||||
|
$member->changePassword('barbaz');
|
||||||
|
|
||||||
|
$this->assertFalse($member->changePassword('barbaz')->isValid());
|
||||||
|
$this->assertFalse($member->changePassword('foobaz')->isValid());
|
||||||
|
$this->assertFalse($member->changePassword('foobar')->isValid());
|
||||||
|
$this->assertTrue($member->changePassword('honk')->isValid());
|
||||||
|
$this->assertTrue($member->changePassword('newpassword')->isValid());
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,11 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace SilverStripe\Security\Tests\Validation\RulesPasswordValidatorTest;
|
||||||
|
|
||||||
|
use SilverStripe\Dev\TestOnly;
|
||||||
|
use SilverStripe\Security\Validation\PasswordValidator;
|
||||||
|
|
||||||
|
class DummyPasswordValidator extends PasswordValidator implements TestOnly
|
||||||
|
{
|
||||||
|
// no-op, just need a concrete class instead of an abstract one.
|
||||||
|
}
|
@ -1,25 +1,20 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace SilverStripe\Security\Tests;
|
namespace SilverStripe\Security\Tests\Validation;
|
||||||
|
|
||||||
use SilverStripe\Dev\Deprecation;
|
|
||||||
use SilverStripe\Dev\SapphireTest;
|
use SilverStripe\Dev\SapphireTest;
|
||||||
use SilverStripe\Security\Member;
|
use SilverStripe\Security\Member;
|
||||||
use SilverStripe\Security\PasswordValidator;
|
use SilverStripe\Security\Validation\RulesPasswordValidator;
|
||||||
|
|
||||||
class PasswordValidatorTest extends SapphireTest
|
class RulesPasswordValidatorTest extends SapphireTest
|
||||||
{
|
{
|
||||||
/**
|
protected $usesDatabase = false;
|
||||||
* {@inheritDoc}
|
|
||||||
* @var bool
|
|
||||||
*/
|
|
||||||
protected $usesDatabase = true;
|
|
||||||
|
|
||||||
protected function setUp(): void
|
protected function setUp(): void
|
||||||
{
|
{
|
||||||
parent::setUp();
|
parent::setUp();
|
||||||
|
|
||||||
PasswordValidator::config()
|
RulesPasswordValidator::config()
|
||||||
->remove('min_length')
|
->remove('min_length')
|
||||||
->remove('historic_count')
|
->remove('historic_count')
|
||||||
->set('min_test_score', 0);
|
->set('min_test_score', 0);
|
||||||
@ -27,7 +22,7 @@ class PasswordValidatorTest extends SapphireTest
|
|||||||
|
|
||||||
public function testValidate()
|
public function testValidate()
|
||||||
{
|
{
|
||||||
$v = Deprecation::withSuppressedNotice(fn() => new PasswordValidator());
|
$v = new RulesPasswordValidator();
|
||||||
$r = $v->validate('', new Member());
|
$r = $v->validate('', new Member());
|
||||||
$this->assertTrue($r->isValid(), 'Empty password is valid by default');
|
$this->assertTrue($r->isValid(), 'Empty password is valid by default');
|
||||||
|
|
||||||
@ -37,7 +32,7 @@ class PasswordValidatorTest extends SapphireTest
|
|||||||
|
|
||||||
public function testValidateMinLength()
|
public function testValidateMinLength()
|
||||||
{
|
{
|
||||||
$v = Deprecation::withSuppressedNotice(fn() => new PasswordValidator());
|
$v = new RulesPasswordValidator();
|
||||||
|
|
||||||
$v->setMinLength(4);
|
$v->setMinLength(4);
|
||||||
$r = $v->validate('123', new Member());
|
$r = $v->validate('123', new Member());
|
||||||
@ -51,7 +46,7 @@ class PasswordValidatorTest extends SapphireTest
|
|||||||
public function testValidateMinScore()
|
public function testValidateMinScore()
|
||||||
{
|
{
|
||||||
// Set both score and set of tests
|
// Set both score and set of tests
|
||||||
$v = Deprecation::withSuppressedNotice(fn() => new PasswordValidator());
|
$v = new RulesPasswordValidator();
|
||||||
$v->setMinTestScore(3);
|
$v->setMinTestScore(3);
|
||||||
$v->setTestNames(["lowercase", "uppercase", "digits", "punctuation"]);
|
$v->setTestNames(["lowercase", "uppercase", "digits", "punctuation"]);
|
||||||
|
|
||||||
@ -62,7 +57,7 @@ class PasswordValidatorTest extends SapphireTest
|
|||||||
$this->assertTrue($r->isValid(), 'Passing enough tests');
|
$this->assertTrue($r->isValid(), 'Passing enough tests');
|
||||||
|
|
||||||
// Ensure min score without tests works (uses default tests)
|
// Ensure min score without tests works (uses default tests)
|
||||||
$v = Deprecation::withSuppressedNotice(fn() => new PasswordValidator());
|
$v = new RulesPasswordValidator();
|
||||||
$v->setMinTestScore(3);
|
$v->setMinTestScore(3);
|
||||||
|
|
||||||
$r = $v->validate('aA', new Member());
|
$r = $v->validate('aA', new Member());
|
||||||
@ -76,31 +71,4 @@ class PasswordValidatorTest extends SapphireTest
|
|||||||
$r = $v->validate('aA1!', new Member());
|
$r = $v->validate('aA1!', new Member());
|
||||||
$this->assertTrue($r->isValid(), 'Passing enough tests');
|
$this->assertTrue($r->isValid(), 'Passing enough tests');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Test that a certain number of historical passwords are checked if specified
|
|
||||||
*/
|
|
||||||
public function testHistoricalPasswordCount()
|
|
||||||
{
|
|
||||||
$validator = Deprecation::withSuppressedNotice(fn() => new PasswordValidator);
|
|
||||||
$validator->setHistoricCount(3);
|
|
||||||
Member::set_password_validator($validator);
|
|
||||||
|
|
||||||
$member = new Member;
|
|
||||||
$member->FirstName = 'Repeat';
|
|
||||||
$member->Surname = 'Password-Man';
|
|
||||||
$member->Password = 'honk';
|
|
||||||
$member->write();
|
|
||||||
|
|
||||||
// Create a set of used passwords
|
|
||||||
$member->changePassword('foobar');
|
|
||||||
$member->changePassword('foobaz');
|
|
||||||
$member->changePassword('barbaz');
|
|
||||||
|
|
||||||
$this->assertFalse($member->changePassword('barbaz')->isValid());
|
|
||||||
$this->assertFalse($member->changePassword('foobaz')->isValid());
|
|
||||||
$this->assertFalse($member->changePassword('foobar')->isValid());
|
|
||||||
$this->assertTrue($member->changePassword('honk')->isValid());
|
|
||||||
$this->assertTrue($member->changePassword('newpassword')->isValid());
|
|
||||||
}
|
|
||||||
}
|
}
|
@ -3,24 +3,11 @@
|
|||||||
namespace SilverStripe\Security\Tests;
|
namespace SilverStripe\Security\Tests;
|
||||||
|
|
||||||
use SilverStripe\Control\Controller;
|
use SilverStripe\Control\Controller;
|
||||||
use SilverStripe\Control\NullHTTPRequest;
|
|
||||||
use SilverStripe\Core\Config\Config;
|
|
||||||
use SilverStripe\Core\Injector\Injector;
|
|
||||||
use SilverStripe\Dev\Deprecation;
|
|
||||||
use SilverStripe\Dev\SapphireTest;
|
use SilverStripe\Dev\SapphireTest;
|
||||||
use SilverStripe\ORM\FieldType\DBDatetime;
|
use SilverStripe\ORM\FieldType\DBDatetime;
|
||||||
use SilverStripe\Core\Validation\ValidationResult;
|
use SilverStripe\Core\Validation\ValidationResult;
|
||||||
use SilverStripe\Security\Authenticator;
|
|
||||||
use SilverStripe\Security\DefaultAdminService;
|
|
||||||
use SilverStripe\Security\IdentityStore;
|
|
||||||
use SilverStripe\Security\LoginAttempt;
|
|
||||||
use SilverStripe\Security\Member;
|
use SilverStripe\Security\Member;
|
||||||
use SilverStripe\Security\MemberAuthenticator\CMSMemberAuthenticator;
|
|
||||||
use SilverStripe\Security\MemberAuthenticator\CMSMemberLoginForm;
|
|
||||||
use SilverStripe\Security\MemberAuthenticator\MemberAuthenticator;
|
use SilverStripe\Security\MemberAuthenticator\MemberAuthenticator;
|
||||||
use SilverStripe\Security\MemberAuthenticator\MemberLoginForm;
|
|
||||||
use SilverStripe\Security\PasswordValidator;
|
|
||||||
use SilverStripe\Security\Security;
|
|
||||||
use SilverStripe\Versioned\Versioned;
|
use SilverStripe\Versioned\Versioned;
|
||||||
|
|
||||||
class VersionedMemberAuthenticatorTest extends SapphireTest
|
class VersionedMemberAuthenticatorTest extends SapphireTest
|
||||||
@ -43,12 +30,8 @@ class VersionedMemberAuthenticatorTest extends SapphireTest
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enforce dummy validation (this can otherwise be influenced by recipe config)
|
// Remove password validation
|
||||||
Deprecation::withSuppressedNotice(
|
Member::set_password_validator(null);
|
||||||
fn() => PasswordValidator::singleton()
|
|
||||||
->setMinLength(0)
|
|
||||||
->setTestNames([])
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function tearDown(): void
|
protected function tearDown(): void
|
||||||
|
Loading…
Reference in New Issue
Block a user