.../EntityResource/User/UserResourceTestBase.php | 103 +++++++++++++++++++++ 1 file changed, 103 insertions(+) diff --git a/core/modules/rest/tests/src/Functional/EntityResource/User/UserResourceTestBase.php b/core/modules/rest/tests/src/Functional/EntityResource/User/UserResourceTestBase.php index 478c442..ef04868 100644 --- a/core/modules/rest/tests/src/Functional/EntityResource/User/UserResourceTestBase.php +++ b/core/modules/rest/tests/src/Functional/EntityResource/User/UserResourceTestBase.php @@ -2,8 +2,10 @@ namespace Drupal\Tests\rest\Functional\EntityResource\User; +use Drupal\Core\Url; use Drupal\Tests\rest\Functional\EntityResource\EntityResourceTestBase; use Drupal\user\Entity\User; +use GuzzleHttp\RequestOptions; abstract class UserResourceTestBase extends EntityResourceTestBase { @@ -119,4 +121,105 @@ protected function getNormalizedPostEntity() { ]; } + /** + * Tests PATCHing security-sensitive base fields of the logged in account. + */ + public function testPatchDxForSecuritySensitiveBaseFields() { + // The anonymous user is never allowed to modify itself. + if (!static::$auth) { + $this->markTestSkipped(); + } + + $this->initAuthentication(); + $this->provisionEntityResource(); + $this->setUpAuthorization('PATCH'); + + /** @var \Drupal\user\UserInterface $user */ + $user = static::$auth ? $this->account : User::load(0); + $original_normalization = array_diff_key($this->serializer->normalize($user, static::$format), ['changed' => TRUE]); + + + // Since this test must be performed by the user that is being modified, + // we cannot use $this->getUrl(). + $url = $user->toUrl()->setOption('query', ['_format' => static::$format]); + $request_options = [ + RequestOptions::HEADERS => ['Content-Type' => static::$mimeType], + ]; + $request_options = array_merge_recursive($request_options, $this->getAuthenticationRequestOptions('PATCH')); + + + // Test case 1: changing email. + $normalization = $original_normalization; + $normalization['mail'] = [['value' => 'new-email@example.com']]; + $request_options[RequestOptions::BODY] = $this->serializer->encode($normalization, static::$format); + + + // DX: 422 when changing email without providing the password. + $response = $this->request('PATCH', $url, $request_options); + // @todo use this commented line instead of the 3 lines thereafter once https://www.drupal.org/node/2813755 lands. +// $this->assertResourceErrorResponse(422, "Unprocessable Entity: validation failed.\nmail: Your current password is missing or incorrect; it's required to change the Email.\n", $response); + $this->assertSame(422, $response->getStatusCode()); + $this->assertSame([static::$mimeType], $response->getHeader('Content-Type')); + $this->assertSame($this->serializer->encode(['message' => "Unprocessable Entity: validation failed.\nmail: Your current password is missing or incorrect; it's required to change the Email.\n"], static::$format), (string) $response->getBody()); + + + $normalization['pass'] = [['existing' => 'wrong']]; + $request_options[RequestOptions::BODY] = $this->serializer->encode($normalization, static::$format); + + // DX: 422 when changing email while providing a wrong password. + $response = $this->request('PATCH', $url, $request_options); + // @todo use this commented line instead of the 3 lines thereafter once https://www.drupal.org/node/2813755 lands. +// $this->assertResourceErrorResponse(422, "Unprocessable Entity: validation failed.\nmail: Your current password is missing or incorrect; it's required to change the Email.\n", $response); + $this->assertSame(422, $response->getStatusCode()); + $this->assertSame([static::$mimeType], $response->getHeader('Content-Type')); + $this->assertSame($this->serializer->encode(['message' => "Unprocessable Entity: validation failed.\nmail: Your current password is missing or incorrect; it's required to change the Email.\n"], static::$format), (string) $response->getBody()); + + + $normalization['pass'] = [['existing' => $this->account->passRaw]]; + $request_options[RequestOptions::BODY] = $this->serializer->encode($normalization, static::$format); + + + // 200 for well-formed request. + $response = $this->request('PATCH', $url, $request_options); + $this->assertResourceResponse(200, FALSE, $response); + + + // Test case 2: changing password. + $normalization = $original_normalization; + $new_password = $this->randomString(); + $normalization['pass'] = [['value' => $new_password]]; + $request_options[RequestOptions::BODY] = $this->serializer->encode($normalization, static::$format); + + + // DX: 422 when changing password without providing the current password. + $response = $this->request('PATCH', $url, $request_options); + // @todo use this commented line instead of the 3 lines thereafter once https://www.drupal.org/node/2813755 lands. +// $this->assertResourceErrorResponse(422, "Unprocessable Entity: validation failed.\npass: Your current password is missing or incorrect; it's required to change the Password.\n", $response); + $this->assertSame(422, $response->getStatusCode()); + $this->assertSame([static::$mimeType], $response->getHeader('Content-Type')); + $this->assertSame($this->serializer->encode(['message' => "Unprocessable Entity: validation failed.\npass: Your current password is missing or incorrect; it's required to change the Password.\n"], static::$format), (string) $response->getBody()); + + + $normalization['pass'][0]['existing'] = $this->account->pass_raw; + $request_options[RequestOptions::BODY] = $this->serializer->encode($normalization, static::$format); + + + // 200 for well-formed request. + $response = $this->request('PATCH', $url, $request_options); + $this->assertResourceResponse(200, FALSE, $response); + + + // Verify that we can log in with the new password. + $request_body = [ + 'name' => $user->getAccountName(), + 'pass' => $new_password, + ]; + $request_options = [ + RequestOptions::HEADERS => [], + RequestOptions::BODY => $this->serializer->encode($request_body, 'json'), + ]; + $response = $this->httpClient->request('POST', Url::fromRoute('user.login.http')->setRouteParameter('_format', 'json')->toString(), $request_options); + $this->assertSame(200, $response->getStatusCode()); + } + }