[Validator] Make API endpoint for NotCompromisedPasswordValidator configurable

This commit is contained in:
Andreas Erhard 2019-04-10 15:29:36 +02:00 committed by Fabien Potencier
parent b09dfd9d8e
commit f6a80c214d
5 changed files with 68 additions and 13 deletions

View File

@ -835,9 +835,18 @@ class Configuration implements ConfigurationInterface
->end()
->end()
->end()
->booleanNode('disable_not_compromised_password')
->defaultFalse()
->info('Disable NotCompromisedPassword Validator: the value will always be valid.')
->arrayNode('not_compromised_password')
->canBeDisabled()
->children()
->booleanNode('enabled')
->defaultTrue()
->info('When disabled, compromised passwords will be accepted as valid.')
->end()
->scalarNode('endpoint')
->defaultNull()
->info('API endpoint for the NotCompromisedPassword Validator.')
->end()
->end()
->end()
->arrayNode('auto_mapping')
->useAttributeAsKey('namespace')

View File

@ -1249,7 +1249,8 @@ class FrameworkExtension extends Extension
$container
->getDefinition('validator.not_compromised_password')
->setArgument(2, $config['disable_not_compromised_password'])
->setArgument(2, $config['not_compromised_password']['enabled'])
->setArgument(3, $config['not_compromised_password']['endpoint'])
;
}

View File

@ -235,7 +235,10 @@ class ConfigurationTest extends TestCase
'paths' => [],
],
'auto_mapping' => [],
'disable_not_compromised_password' => false,
'not_compromised_password' => [
'enabled' => true,
'endpoint' => null,
],
],
'annotations' => [
'cache' => 'php_array',

View File

@ -29,13 +29,14 @@ use Symfony\Contracts\HttpClient\HttpClientInterface;
*/
class NotCompromisedPasswordValidator extends ConstraintValidator
{
private const RANGE_API = 'https://api.pwnedpasswords.com/range/%s';
private const DEFAULT_API_ENDPOINT = 'https://api.pwnedpasswords.com/range/%s';
private $httpClient;
private $charset;
private $disabled;
private $enabled;
private $endpoint;
public function __construct(HttpClientInterface $httpClient = null, string $charset = 'UTF-8', bool $disabled = false)
public function __construct(HttpClientInterface $httpClient = null, string $charset = 'UTF-8', bool $enabled = true, string $endpoint = null)
{
if (null === $httpClient && !class_exists(HttpClient::class)) {
throw new \LogicException(sprintf('The "%s" class requires the "HttpClient" component. Try running "composer require symfony/http-client".', self::class));
@ -43,7 +44,8 @@ class NotCompromisedPasswordValidator extends ConstraintValidator
$this->httpClient = $httpClient ?? HttpClient::create();
$this->charset = $charset;
$this->disabled = $disabled;
$this->enabled = $enabled;
$this->endpoint = $endpoint ?? self::DEFAULT_API_ENDPOINT;
}
/**
@ -57,7 +59,7 @@ class NotCompromisedPasswordValidator extends ConstraintValidator
throw new UnexpectedTypeException($constraint, NotCompromisedPassword::class);
}
if ($this->disabled) {
if (!$this->enabled) {
return;
}
@ -76,7 +78,7 @@ class NotCompromisedPasswordValidator extends ConstraintValidator
$hash = strtoupper(sha1($value));
$hashPrefix = substr($hash, 0, 5);
$url = sprintf(self::RANGE_API, $hashPrefix);
$url = sprintf($this->endpoint, $hashPrefix);
try {
$result = $this->httpClient->request('GET', $url)->getContent();

View File

@ -62,9 +62,9 @@ class NotCompromisedPasswordValidatorTest extends ConstraintValidatorTestCase
public function testInvalidPasswordButDisabled()
{
$r = new \ReflectionProperty($this->validator, 'disabled');
$r = new \ReflectionProperty($this->validator, 'enabled');
$r->setAccessible(true);
$r->setValue($this->validator, true);
$r->setValue($this->validator, false);
$this->validator->validate(self::PASSWORD_LEAKED, new NotCompromisedPassword());
@ -128,6 +128,29 @@ class NotCompromisedPasswordValidatorTest extends ConstraintValidatorTestCase
->assertRaised();
}
public function testInvalidPasswordCustomEndpoint()
{
$endpoint = 'https://password-check.internal.example.com/range/%s';
// 50D74 - first 5 bytes of uppercase SHA1 hash of self::PASSWORD_LEAKED
$expectedEndpointUrl = 'https://password-check.internal.example.com/range/50D74';
$constraint = new NotCompromisedPassword();
$this->context = $this->createContext();
$validator = new NotCompromisedPasswordValidator(
$this->createHttpClientStubCustomEndpoint($expectedEndpointUrl),
'UTF-8',
true,
$endpoint
);
$validator->initialize($this->context);
$validator->validate(self::PASSWORD_LEAKED, $constraint);
$this->buildViolation($constraint->message)
->setCode(NotCompromisedPassword::COMPROMISED_PASSWORD_ERROR)
->assertRaised();
}
/**
* @expectedException \Symfony\Component\Validator\Exception\UnexpectedTypeException
*/
@ -184,4 +207,21 @@ class NotCompromisedPasswordValidatorTest extends ConstraintValidatorTestCase
return $httpClientStub;
}
private function createHttpClientStubCustomEndpoint($expectedEndpoint): HttpClientInterface
{
$httpClientStub = $this->createMock(HttpClientInterface::class);
$httpClientStub->method('request')->with('GET', $expectedEndpoint)->will(
$this->returnCallback(function (string $method, string $url): ResponseInterface {
$responseStub = $this->createMock(ResponseInterface::class);
$responseStub
->method('getContent')
->willReturn(implode("\r\n", self::RETURN));
return $responseStub;
})
);
return $httpClientStub;
}
}