Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -835,9 +835,18 @@ private function addValidationSection(ArrayNodeDefinition $rootNode)
->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')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1249,7 +1249,8 @@ private function registerValidationConfiguration(array $config, ContainerBuilder

$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'])
;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,10 @@ protected static function getBundleDefaultConfig()
'paths' => [],
],
'auto_mapping' => [],
'disable_not_compromised_password' => false,
'not_compromised_password' => [
'enabled' => true,
'endpoint' => null,
],
],
'annotations' => [
'cache' => 'php_array',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,21 +29,23 @@
*/
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));
}

$this->httpClient = $httpClient ?? HttpClient::create();
$this->charset = $charset;
$this->disabled = $disabled;
$this->enabled = $enabled;
$this->endpoint = $endpoint ?? self::DEFAULT_API_ENDPOINT;
}

/**
Expand All @@ -57,7 +59,7 @@ public function validate($value, Constraint $constraint)
throw new UnexpectedTypeException($constraint, NotCompromisedPassword::class);
}

if ($this->disabled) {
if (!$this->enabled) {
return;
}

Expand All @@ -76,7 +78,7 @@ public function validate($value, Constraint $constraint)

$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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,9 @@ public function testEmptyStringIsValid()

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());

Expand Down Expand Up @@ -128,6 +128,29 @@ public function testNonUtf8CharsetInvalid()
->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
*/
Expand Down Expand Up @@ -184,4 +207,21 @@ public function getResponse(): ResponseInterface

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;
}
}