Security update for Core, with self-updated composer
[yaffs-website] / web / core / modules / rest / tests / src / Functional / EntityResource / User / UserResourceTestBase.php
1 <?php
2
3 namespace Drupal\Tests\rest\Functional\EntityResource\User;
4
5 use Drupal\Core\Url;
6 use Drupal\Tests\rest\Functional\BcTimestampNormalizerUnixTestTrait;
7 use Drupal\Tests\rest\Functional\EntityResource\EntityResourceTestBase;
8 use Drupal\user\Entity\User;
9 use GuzzleHttp\RequestOptions;
10
11 abstract class UserResourceTestBase extends EntityResourceTestBase {
12
13   use BcTimestampNormalizerUnixTestTrait;
14
15   /**
16    * {@inheritdoc}
17    */
18   public static $modules = ['user'];
19
20   /**
21    * {@inheritdoc}
22    */
23   protected static $entityTypeId = 'user';
24
25   /**
26    * {@inheritdoc}
27    */
28   protected static $patchProtectedFieldNames = [
29     'changed',
30   ];
31
32   /**
33    * @var \Drupal\user\UserInterface
34    */
35   protected $entity;
36
37   /**
38    * {@inheritdoc}
39    */
40   protected static $labelFieldName = 'name';
41
42   /**
43    * {@inheritdoc}
44    */
45   protected static $firstCreatedEntityId = 4;
46
47   /**
48    * {@inheritdoc}
49    */
50   protected static $secondCreatedEntityId = 5;
51
52   /**
53    * {@inheritdoc}
54    */
55   protected function setUpAuthorization($method) {
56     switch ($method) {
57       case 'GET':
58         $this->grantPermissionsToTestedRole(['access user profiles']);
59         break;
60       case 'POST':
61       case 'PATCH':
62       case 'DELETE':
63         $this->grantPermissionsToTestedRole(['administer users']);
64         break;
65     }
66   }
67
68   /**
69    * {@inheritdoc}
70    */
71   protected function createEntity() {
72     // Create a "Llama" user.
73     $user = User::create(['created' => 123456789]);
74     $user->setUsername('Llama')
75       ->setChangedTime(123456789)
76       ->activate()
77       ->save();
78
79     return $user;
80   }
81
82   /**
83    * {@inheritdoc}
84    */
85   protected function getExpectedNormalizedEntity() {
86     return [
87       'uid' => [
88         ['value' => 3],
89       ],
90       'uuid' => [
91         ['value' => $this->entity->uuid()],
92       ],
93       'langcode' => [
94         [
95           'value' => 'en',
96         ],
97       ],
98       'name' => [
99         [
100           'value' => 'Llama',
101         ],
102       ],
103       'created' => [
104         $this->formatExpectedTimestampItemValues(123456789),
105       ],
106       'changed' => [
107         $this->formatExpectedTimestampItemValues($this->entity->getChangedTime()),
108       ],
109       'default_langcode' => [
110         [
111           'value' => TRUE,
112         ],
113       ],
114     ];
115   }
116
117   /**
118    * {@inheritdoc}
119    */
120   protected function getNormalizedPostEntity() {
121     return [
122       'name' => [
123         [
124           'value' => 'Dramallama',
125         ],
126       ],
127     ];
128   }
129
130   /**
131    * Tests PATCHing security-sensitive base fields of the logged in account.
132    */
133   public function testPatchDxForSecuritySensitiveBaseFields() {
134     // The anonymous user is never allowed to modify itself.
135     if (!static::$auth) {
136       $this->markTestSkipped();
137     }
138
139     $this->initAuthentication();
140     $this->provisionEntityResource();
141
142     /** @var \Drupal\user\UserInterface $user */
143     $user = static::$auth ? $this->account : User::load(0);
144     // @todo Remove the array_diff_key() call in https://www.drupal.org/node/2821077.
145     $original_normalization = array_diff_key($this->serializer->normalize($user, static::$format), ['created' => TRUE, 'changed' => TRUE, 'name' => TRUE]);
146
147     // Since this test must be performed by the user that is being modified,
148     // we cannot use $this->getUrl().
149     $url = $user->toUrl()->setOption('query', ['_format' => static::$format]);
150     $request_options = [
151       RequestOptions::HEADERS => ['Content-Type' => static::$mimeType],
152     ];
153     $request_options = array_merge_recursive($request_options, $this->getAuthenticationRequestOptions('PATCH'));
154
155     // Test case 1: changing email.
156     $normalization = $original_normalization;
157     $normalization['mail'] = [['value' => 'new-email@example.com']];
158     $request_options[RequestOptions::BODY] = $this->serializer->encode($normalization, static::$format);
159
160     // DX: 422 when changing email without providing the password.
161     $response = $this->request('PATCH', $url, $request_options);
162     $this->assertResourceErrorResponse(422, "Unprocessable Entity: validation failed.\nmail: Your current password is missing or incorrect; it's required to change the Email.\n", $response);
163
164     $normalization['pass'] = [['existing' => 'wrong']];
165     $request_options[RequestOptions::BODY] = $this->serializer->encode($normalization, static::$format);
166
167     // DX: 422 when changing email while providing a wrong password.
168     $response = $this->request('PATCH', $url, $request_options);
169     $this->assertResourceErrorResponse(422, "Unprocessable Entity: validation failed.\nmail: Your current password is missing or incorrect; it's required to change the Email.\n", $response);
170
171     $normalization['pass'] = [['existing' => $this->account->passRaw]];
172     $request_options[RequestOptions::BODY] = $this->serializer->encode($normalization, static::$format);
173
174     // 200 for well-formed request.
175     $response = $this->request('PATCH', $url, $request_options);
176     $this->assertResourceResponse(200, FALSE, $response);
177
178     // Test case 2: changing password.
179     $normalization = $original_normalization;
180     $new_password = $this->randomString();
181     $normalization['pass'] = [['value' => $new_password]];
182     $request_options[RequestOptions::BODY] = $this->serializer->encode($normalization, static::$format);
183
184     // DX: 422 when changing password without providing the current password.
185     $response = $this->request('PATCH', $url, $request_options);
186     $this->assertResourceErrorResponse(422, "Unprocessable Entity: validation failed.\npass: Your current password is missing or incorrect; it's required to change the Password.\n", $response);
187
188     $normalization['pass'][0]['existing'] = $this->account->pass_raw;
189     $request_options[RequestOptions::BODY] = $this->serializer->encode($normalization, static::$format);
190
191     // 200 for well-formed request.
192     $response = $this->request('PATCH', $url, $request_options);
193     $this->assertResourceResponse(200, FALSE, $response);
194
195     // Verify that we can log in with the new password.
196     $this->assertRpcLogin($user->getAccountName(), $new_password);
197
198     // Update password in $this->account, prepare for future requests.
199     $this->account->passRaw = $new_password;
200     $this->initAuthentication();
201     $request_options = [
202       RequestOptions::HEADERS => ['Content-Type' => static::$mimeType],
203     ];
204     $request_options = array_merge_recursive($request_options, $this->getAuthenticationRequestOptions('PATCH'));
205
206     // Test case 3: changing name.
207     $normalization = $original_normalization;
208     $normalization['name'] = [['value' => 'Cooler Llama']];
209     $request_options[RequestOptions::BODY] = $this->serializer->encode($normalization, static::$format);
210
211     // DX: 403 when modifying username without required permission.
212     $response = $this->request('PATCH', $url, $request_options);
213     $this->assertResourceErrorResponse(403, "Access denied on updating field 'name'.", $response);
214
215     $this->grantPermissionsToTestedRole(['change own username']);
216
217     // 200 for well-formed request.
218     $response = $this->request('PATCH', $url, $request_options);
219     $this->assertResourceResponse(200, FALSE, $response);
220
221     // Verify that we can log in with the new username.
222     $this->assertRpcLogin('Cooler Llama', $new_password);
223   }
224
225   /**
226    * Verifies that logging in with the given username and password works.
227    *
228    * @param string $username
229    *   The username to log in with.
230    * @param string $password
231    *   The password to log in with.
232    */
233   protected function assertRpcLogin($username, $password) {
234     $request_body = [
235       'name' => $username,
236       'pass' => $password,
237     ];
238     $request_options = [
239       RequestOptions::HEADERS => [],
240       RequestOptions::BODY => $this->serializer->encode($request_body, 'json'),
241     ];
242     $response = $this->request('POST', Url::fromRoute('user.login.http')->setRouteParameter('_format', 'json'), $request_options);
243     $this->assertSame(200, $response->getStatusCode());
244   }
245
246   /**
247    * {@inheritdoc}
248    */
249   protected function getExpectedUnauthorizedAccessMessage($method) {
250     if ($this->config('rest.settings')->get('bc_entity_resource_permissions')) {
251       return parent::getExpectedUnauthorizedAccessMessage($method);
252     }
253
254     switch ($method) {
255       case 'GET':
256         return "The 'access user profiles' permission is required and the user must be active.";
257       case 'PATCH':
258         return "You are not authorized to update this user entity.";
259       case 'DELETE':
260         return 'You are not authorized to delete this user entity.';
261       default:
262         return parent::getExpectedUnauthorizedAccessMessage($method);
263     }
264   }
265
266 }