feature #30898 [Validator] Wire NotCompromisedPassword in FrameworkBundle and handle non UTF-8 password (tgalopin)
This PR was squashed before being merged into the 4.3-dev branch (closes #30898).
Discussion
----------
[Validator] Wire NotCompromisedPassword in FrameworkBundle and handle non UTF-8 password
| Q | A
| ------------- | ---
| Branch? | master
| Bug fix? | yes
| New feature? | no
| BC breaks? | no
| Deprecations? | no
| Tests pass? | yes
| Fixed tickets | https://github.com/symfony/symfony/issues/30870
| License | MIT
| Doc PR | -
Live from #eu-fossa
Fix https://github.com/symfony/symfony/issues/30870
Commits
-------
8ac712b29d
[Validator] Wire NotCompromisedPassword in FrameworkBundle and handle non UTF-8 password
This commit is contained in:
commit
b045acaf09
@ -61,6 +61,12 @@
|
|||||||
<tag name="validator.constraint_validator" alias="Symfony\Component\Validator\Constraints\EmailValidator" />
|
<tag name="validator.constraint_validator" alias="Symfony\Component\Validator\Constraints\EmailValidator" />
|
||||||
</service>
|
</service>
|
||||||
|
|
||||||
|
<service id="validator.not_compromised_password" class="Symfony\Component\Validator\Constraints\NotCompromisedPasswordValidator">
|
||||||
|
<argument type="service" id="http_client" on-invalid="null" />
|
||||||
|
<argument>%kernel.charset%</argument>
|
||||||
|
<tag name="validator.constraint_validator" alias="Symfony\Component\Validator\Constraints\NotCompromisedPasswordValidator" />
|
||||||
|
</service>
|
||||||
|
|
||||||
<service id="validator.property_info_loader" class="Symfony\Component\Validator\Mapping\Loader\PropertyInfoLoader">
|
<service id="validator.property_info_loader" class="Symfony\Component\Validator\Mapping\Loader\PropertyInfoLoader">
|
||||||
<argument type="service" id="property_info" />
|
<argument type="service" id="property_info" />
|
||||||
<argument type="service" id="property_info" />
|
<argument type="service" id="property_info" />
|
||||||
|
@ -31,14 +31,16 @@ class NotCompromisedPasswordValidator extends ConstraintValidator
|
|||||||
private const RANGE_API = 'https://api.pwnedpasswords.com/range/%s';
|
private const RANGE_API = 'https://api.pwnedpasswords.com/range/%s';
|
||||||
|
|
||||||
private $httpClient;
|
private $httpClient;
|
||||||
|
private $charset;
|
||||||
|
|
||||||
public function __construct(HttpClientInterface $httpClient = null)
|
public function __construct(HttpClientInterface $httpClient = null, string $charset = 'UTF-8')
|
||||||
{
|
{
|
||||||
if (null === $httpClient && !class_exists(HttpClient::class)) {
|
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));
|
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->httpClient = $httpClient ?? HttpClient::create();
|
||||||
|
$this->charset = $charset;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -61,6 +63,10 @@ class NotCompromisedPasswordValidator extends ConstraintValidator
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ('UTF-8' !== $this->charset) {
|
||||||
|
$value = mb_convert_encoding($value, 'UTF-8', $this->charset);
|
||||||
|
}
|
||||||
|
|
||||||
$hash = strtoupper(sha1($value));
|
$hash = strtoupper(sha1($value));
|
||||||
$hashPrefix = substr($hash, 0, 5);
|
$hashPrefix = substr($hash, 0, 5);
|
||||||
$url = sprintf(self::RANGE_API, $hashPrefix);
|
$url = sprintf(self::RANGE_API, $hashPrefix);
|
||||||
|
@ -28,40 +28,22 @@ class NotCompromisedPasswordValidatorTest extends ConstraintValidatorTestCase
|
|||||||
private const PASSWORD_TRIGGERING_AN_ERROR_RANGE_URL = 'https://api.pwnedpasswords.com/range/3EF27'; // https://api.pwnedpasswords.com/range/3EF27 is the range for the value "apiError"
|
private const PASSWORD_TRIGGERING_AN_ERROR_RANGE_URL = 'https://api.pwnedpasswords.com/range/3EF27'; // https://api.pwnedpasswords.com/range/3EF27 is the range for the value "apiError"
|
||||||
private const PASSWORD_LEAKED = 'maman';
|
private const PASSWORD_LEAKED = 'maman';
|
||||||
private const PASSWORD_NOT_LEAKED = ']<0585"%sb^5aa$w6!b38",,72?dp3r4\45b28Hy';
|
private const PASSWORD_NOT_LEAKED = ']<0585"%sb^5aa$w6!b38",,72?dp3r4\45b28Hy';
|
||||||
|
private const PASSWORD_NON_UTF8_LEAKED = 'мама';
|
||||||
|
private const PASSWORD_NON_UTF8_NOT_LEAKED = 'м<в0dp3r4\45b28Hy';
|
||||||
|
|
||||||
private const RETURN = [
|
private const RETURN = [
|
||||||
'35E033023A46402F94CFB4F654C5BFE44A1:1',
|
'35E033023A46402F94CFB4F654C5BFE44A1:1',
|
||||||
'35F079CECCC31812288257CD770AA7968D7:53',
|
'35F079CECCC31812288257CD770AA7968D7:53',
|
||||||
'36039744C253F9B2A4E90CBEDB02EBFB82D:5', // this is the matching line, password: maman
|
'36039744C253F9B2A4E90CBEDB02EBFB82D:5', // UTF-8 leaked password: maman
|
||||||
|
'273CA8A2A78C9B2D724144F4FAF4D221C86:6', // ISO-8859-5 leaked password: мама
|
||||||
'3686792BBC66A72D40D928ED15621124CFE:7',
|
'3686792BBC66A72D40D928ED15621124CFE:7',
|
||||||
'36EEC709091B810AA240179A44317ED415C:2',
|
'36EEC709091B810AA240179A44317ED415C:2',
|
||||||
];
|
];
|
||||||
|
|
||||||
protected function createValidator()
|
protected function createValidator()
|
||||||
{
|
{
|
||||||
$httpClientStub = $this->createMock(HttpClientInterface::class);
|
// Pass HttpClient::create() instead of the mock to run the tests against the real API
|
||||||
$httpClientStub->method('request')->will(
|
return new NotCompromisedPasswordValidator($this->createHttpClientStub());
|
||||||
$this->returnCallback(function (string $method, string $url): ResponseInterface {
|
|
||||||
if (self::PASSWORD_TRIGGERING_AN_ERROR_RANGE_URL === $url) {
|
|
||||||
throw new class('Problem contacting the Have I been Pwned API.') extends \Exception implements ServerExceptionInterface {
|
|
||||||
public function getResponse(): ResponseInterface
|
|
||||||
{
|
|
||||||
throw new \RuntimeException('Not implemented');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
$responseStub = $this->createMock(ResponseInterface::class);
|
|
||||||
$responseStub
|
|
||||||
->method('getContent')
|
|
||||||
->willReturn(implode("\r\n", self::RETURN));
|
|
||||||
|
|
||||||
return $responseStub;
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
// Pass HttpClient::create() instead of this mock to run the tests against the real API
|
|
||||||
return new NotCompromisedPasswordValidator($httpClientStub);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testNullIsValid()
|
public function testNullIsValid()
|
||||||
@ -112,6 +94,29 @@ class NotCompromisedPasswordValidatorTest extends ConstraintValidatorTestCase
|
|||||||
$this->assertNoViolation();
|
$this->assertNoViolation();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testNonUtf8CharsetValid()
|
||||||
|
{
|
||||||
|
$validator = new NotCompromisedPasswordValidator($this->createHttpClientStub(), 'ISO-8859-5');
|
||||||
|
$validator->validate(mb_convert_encoding(self::PASSWORD_NON_UTF8_NOT_LEAKED, 'ISO-8859-5', 'UTF-8'), new NotCompromisedPassword());
|
||||||
|
|
||||||
|
$this->assertNoViolation();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testNonUtf8CharsetInvalid()
|
||||||
|
{
|
||||||
|
$constraint = new NotCompromisedPassword();
|
||||||
|
|
||||||
|
$this->context = $this->createContext();
|
||||||
|
|
||||||
|
$validator = new NotCompromisedPasswordValidator($this->createHttpClientStub(), 'ISO-8859-5');
|
||||||
|
$validator->initialize($this->context);
|
||||||
|
$validator->validate(mb_convert_encoding(self::PASSWORD_NON_UTF8_LEAKED, 'ISO-8859-5', 'UTF-8'), $constraint);
|
||||||
|
|
||||||
|
$this->buildViolation($constraint->message)
|
||||||
|
->setCode(NotCompromisedPassword::COMPROMISED_PASSWORD_ERROR)
|
||||||
|
->assertRaised();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @expectedException \Symfony\Component\Validator\Exception\UnexpectedTypeException
|
* @expectedException \Symfony\Component\Validator\Exception\UnexpectedTypeException
|
||||||
*/
|
*/
|
||||||
@ -142,4 +147,30 @@ class NotCompromisedPasswordValidatorTest extends ConstraintValidatorTestCase
|
|||||||
$this->validator->validate(self::PASSWORD_TRIGGERING_AN_ERROR, new NotCompromisedPassword(['skipOnError' => true]));
|
$this->validator->validate(self::PASSWORD_TRIGGERING_AN_ERROR, new NotCompromisedPassword(['skipOnError' => true]));
|
||||||
$this->assertTrue(true); // No exception have been thrown
|
$this->assertTrue(true); // No exception have been thrown
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function createHttpClientStub(): HttpClientInterface
|
||||||
|
{
|
||||||
|
$httpClientStub = $this->createMock(HttpClientInterface::class);
|
||||||
|
$httpClientStub->method('request')->will(
|
||||||
|
$this->returnCallback(function (string $method, string $url): ResponseInterface {
|
||||||
|
if (self::PASSWORD_TRIGGERING_AN_ERROR_RANGE_URL === $url) {
|
||||||
|
throw new class('Problem contacting the Have I been Pwned API.') extends \Exception implements ServerExceptionInterface {
|
||||||
|
public function getResponse(): ResponseInterface
|
||||||
|
{
|
||||||
|
throw new \RuntimeException('Not implemented');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
$responseStub = $this->createMock(ResponseInterface::class);
|
||||||
|
$responseStub
|
||||||
|
->method('getContent')
|
||||||
|
->willReturn(implode("\r\n", self::RETURN));
|
||||||
|
|
||||||
|
return $responseStub;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return $httpClientStub;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user