7974cba7306ec32634618a86246b8b3cba58b936
[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 createAnotherEntity() {
86     /** @var \Drupal\user\UserInterface $user */
87     $user = $this->entity->createDuplicate();
88     $user->setUsername($user->label() . '_dupe');
89     $user->save();
90     return $user;
91   }
92
93   /**
94    * {@inheritdoc}
95    */
96   protected function getExpectedNormalizedEntity() {
97     return [
98       'uid' => [
99         ['value' => 3],
100       ],
101       'uuid' => [
102         ['value' => $this->entity->uuid()],
103       ],
104       'langcode' => [
105         [
106           'value' => 'en',
107         ],
108       ],
109       'name' => [
110         [
111           'value' => 'Llama',
112         ],
113       ],
114       'created' => [
115         $this->formatExpectedTimestampItemValues(123456789),
116       ],
117       'changed' => [
118         $this->formatExpectedTimestampItemValues($this->entity->getChangedTime()),
119       ],
120       'default_langcode' => [
121         [
122           'value' => TRUE,
123         ],
124       ],
125     ];
126   }
127
128   /**
129    * {@inheritdoc}
130    */
131   protected function getNormalizedPostEntity() {
132     return [
133       'name' => [
134         [
135           'value' => 'Dramallama',
136         ],
137       ],
138     ];
139   }
140
141   /**
142    * Tests PATCHing security-sensitive base fields of the logged in account.
143    */
144   public function testPatchDxForSecuritySensitiveBaseFields() {
145     // The anonymous user is never allowed to modify itself.
146     if (!static::$auth) {
147       $this->markTestSkipped();
148     }
149
150     $this->initAuthentication();
151     $this->provisionEntityResource();
152
153     /** @var \Drupal\user\UserInterface $user */
154     $user = static::$auth ? $this->account : User::load(0);
155     // @todo Remove the array_diff_key() call in https://www.drupal.org/node/2821077.
156     $original_normalization = array_diff_key($this->serializer->normalize($user, static::$format), ['created' => TRUE, 'changed' => TRUE, 'name' => TRUE]);
157
158     // Since this test must be performed by the user that is being modified,
159     // we cannot use $this->getUrl().
160     $url = $user->toUrl()->setOption('query', ['_format' => static::$format]);
161     $request_options = [
162       RequestOptions::HEADERS => ['Content-Type' => static::$mimeType],
163     ];
164     $request_options = array_merge_recursive($request_options, $this->getAuthenticationRequestOptions('PATCH'));
165
166     // Test case 1: changing email.
167     $normalization = $original_normalization;
168     $normalization['mail'] = [['value' => 'new-email@example.com']];
169     $request_options[RequestOptions::BODY] = $this->serializer->encode($normalization, static::$format);
170
171     // DX: 422 when changing email without providing the password.
172     $response = $this->request('PATCH', $url, $request_options);
173     $this->assertResourceErrorResponse(422, "Unprocessable Entity: validation failed.\nmail: Your current password is missing or incorrect; it's required to change the Email.\n", $response, FALSE, FALSE, FALSE, FALSE);
174
175     $normalization['pass'] = [['existing' => 'wrong']];
176     $request_options[RequestOptions::BODY] = $this->serializer->encode($normalization, static::$format);
177
178     // DX: 422 when changing email while providing a wrong password.
179     $response = $this->request('PATCH', $url, $request_options);
180     $this->assertResourceErrorResponse(422, "Unprocessable Entity: validation failed.\nmail: Your current password is missing or incorrect; it's required to change the Email.\n", $response, FALSE, FALSE, FALSE, FALSE);
181
182     $normalization['pass'] = [['existing' => $this->account->passRaw]];
183     $request_options[RequestOptions::BODY] = $this->serializer->encode($normalization, static::$format);
184
185     // 200 for well-formed request.
186     $response = $this->request('PATCH', $url, $request_options);
187     $this->assertResourceResponse(200, FALSE, $response);
188
189     // Test case 2: changing password.
190     $normalization = $original_normalization;
191     $new_password = $this->randomString();
192     $normalization['pass'] = [['value' => $new_password]];
193     $request_options[RequestOptions::BODY] = $this->serializer->encode($normalization, static::$format);
194
195     // DX: 422 when changing password without providing the current password.
196     $response = $this->request('PATCH', $url, $request_options);
197     $this->assertResourceErrorResponse(422, "Unprocessable Entity: validation failed.\npass: Your current password is missing or incorrect; it's required to change the Password.\n", $response, FALSE, FALSE, FALSE, FALSE);
198
199     $normalization['pass'][0]['existing'] = $this->account->pass_raw;
200     $request_options[RequestOptions::BODY] = $this->serializer->encode($normalization, static::$format);
201
202     // 200 for well-formed request.
203     $response = $this->request('PATCH', $url, $request_options);
204     $this->assertResourceResponse(200, FALSE, $response);
205
206     // Verify that we can log in with the new password.
207     $this->assertRpcLogin($user->getAccountName(), $new_password);
208
209     // Update password in $this->account, prepare for future requests.
210     $this->account->passRaw = $new_password;
211     $this->initAuthentication();
212     $request_options = [
213       RequestOptions::HEADERS => ['Content-Type' => static::$mimeType],
214     ];
215     $request_options = array_merge_recursive($request_options, $this->getAuthenticationRequestOptions('PATCH'));
216
217     // Test case 3: changing name.
218     $normalization = $original_normalization;
219     $normalization['name'] = [['value' => 'Cooler Llama']];
220     $request_options[RequestOptions::BODY] = $this->serializer->encode($normalization, static::$format);
221
222     // DX: 403 when modifying username without required permission.
223     $response = $this->request('PATCH', $url, $request_options);
224     $this->assertResourceErrorResponse(403, "Access denied on updating field 'name'.", $response);
225
226     $this->grantPermissionsToTestedRole(['change own username']);
227
228     // 200 for well-formed request.
229     $response = $this->request('PATCH', $url, $request_options);
230     $this->assertResourceResponse(200, FALSE, $response);
231
232     // Verify that we can log in with the new username.
233     $this->assertRpcLogin('Cooler Llama', $new_password);
234   }
235
236   /**
237    * Verifies that logging in with the given username and password works.
238    *
239    * @param string $username
240    *   The username to log in with.
241    * @param string $password
242    *   The password to log in with.
243    */
244   protected function assertRpcLogin($username, $password) {
245     $request_body = [
246       'name' => $username,
247       'pass' => $password,
248     ];
249     $request_options = [
250       RequestOptions::HEADERS => [],
251       RequestOptions::BODY => $this->serializer->encode($request_body, 'json'),
252     ];
253     $response = $this->request('POST', Url::fromRoute('user.login.http')->setRouteParameter('_format', 'json'), $request_options);
254     $this->assertSame(200, $response->getStatusCode());
255   }
256
257   /**
258    * Tests PATCHing security-sensitive base fields to change other users.
259    */
260   public function testPatchSecurityOtherUser() {
261     // The anonymous user is never allowed to modify other users.
262     if (!static::$auth) {
263       $this->markTestSkipped();
264     }
265
266     $this->initAuthentication();
267     $this->provisionEntityResource();
268
269     /** @var \Drupal\user\UserInterface $user */
270     $user = $this->account;
271     $original_normalization = array_diff_key($this->serializer->normalize($user, static::$format), ['changed' => TRUE]);
272
273     // Since this test must be performed by the user that is being modified,
274     // we cannot use $this->getUrl().
275     $url = $user->toUrl()->setOption('query', ['_format' => static::$format]);
276     $request_options = [
277       RequestOptions::HEADERS => ['Content-Type' => static::$mimeType],
278     ];
279     $request_options = array_merge_recursive($request_options, $this->getAuthenticationRequestOptions('PATCH'));
280
281     $normalization = $original_normalization;
282     $normalization['mail'] = [['value' => 'new-email@example.com']];
283     $request_options[RequestOptions::BODY] = $this->serializer->encode($normalization, static::$format);
284
285     // Try changing user 1's email.
286     $user1 = [
287       'mail' => [['value' => 'another_email_address@example.com']],
288       'uid' => [['value' => 1]],
289       'name' => [['value' => 'another_user_name']],
290       'pass' => [['existing' => $this->account->passRaw]],
291       'uuid' => [['value' => '2e9403a4-d8af-4096-a116-624710140be0']],
292     ] + $original_normalization;
293     $request_options[RequestOptions::BODY] = $this->serializer->encode($user1, static::$format);
294     $response = $this->request('PATCH', $url, $request_options);
295     // Ensure the email address has not changed.
296     $this->assertEquals('admin@example.com', $this->entityStorage->loadUnchanged(1)->getEmail());
297     $this->assertResourceErrorResponse(403, "Access denied on updating field 'uid'.", $response);
298   }
299
300   /**
301    * {@inheritdoc}
302    */
303   protected function getExpectedUnauthorizedAccessMessage($method) {
304     if ($this->config('rest.settings')->get('bc_entity_resource_permissions')) {
305       return parent::getExpectedUnauthorizedAccessMessage($method);
306     }
307
308     switch ($method) {
309       case 'GET':
310         return "The 'access user profiles' permission is required and the user must be active.";
311       case 'PATCH':
312         return "You are not authorized to update this user entity.";
313       case 'DELETE':
314         return 'You are not authorized to delete this user entity.';
315       default:
316         return parent::getExpectedUnauthorizedAccessMessage($method);
317     }
318   }
319
320   /**
321    * {@inheritdoc}
322    */
323   protected function getExpectedUnauthorizedAccessCacheability() {
324     // @see \Drupal\user\UserAccessControlHandler::checkAccess()
325     return parent::getExpectedUnauthorizedAccessCacheability()
326       ->addCacheTags(['user:3']);
327   }
328
329 }