diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php index b9f2e20b2b..43c24ddd23 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php @@ -834,9 +834,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') diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index 3413eb2b13..beac069806 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -1262,7 +1262,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']) ; } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php index 3906a3dc7a..79ebadef28 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php @@ -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', diff --git a/src/Symfony/Component/Validator/Constraints/NotCompromisedPasswordValidator.php b/src/Symfony/Component/Validator/Constraints/NotCompromisedPasswordValidator.php index 20ecdd381a..7b8dd42353 100644 --- a/src/Symfony/Component/Validator/Constraints/NotCompromisedPasswordValidator.php +++ b/src/Symfony/Component/Validator/Constraints/NotCompromisedPasswordValidator.php @@ -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(); diff --git a/src/Symfony/Component/Validator/Tests/Constraints/NotCompromisedPasswordValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/NotCompromisedPasswordValidatorTest.php index 8f0ecd2ecf..5dab9be108 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/NotCompromisedPasswordValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/NotCompromisedPasswordValidatorTest.php @@ -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; + } }