From 8400394857b65803f451dfdc287bf2162487fff5 Mon Sep 17 00:00:00 2001 From: Steven Nguyen Date: Fri, 26 May 2023 17:23:01 -0700 Subject: [PATCH] Add an endpoint to update user labels --- app/controllers/api/users.php | 41 +++++++++ docs/references/users/update-user-labels.md | 3 + tests/e2e/Services/Users/UsersBase.php | 93 +++++++++++++++++++++ 3 files changed, 137 insertions(+) create mode 100644 docs/references/users/update-user-labels.md diff --git a/app/controllers/api/users.php b/app/controllers/api/users.php index ceed901a32..4441161e8f 100644 --- a/app/controllers/api/users.php +++ b/app/controllers/api/users.php @@ -28,6 +28,7 @@ use Utopia\Database\Validator\UID; use Utopia\Database\Database; use Utopia\Database\Query; use Utopia\Database\Validator\Authorization; +use Utopia\Validator\ArrayList; use Utopia\Validator\Assoc; use Utopia\Validator\WhiteList; use Utopia\Validator\Text; @@ -65,6 +66,7 @@ function createUser(string $hash, mixed $hashOptions, string $userId, ?string $e 'phone' => $phone, 'phoneVerification' => false, 'status' => true, + 'labels' => [], 'password' => $password, 'passwordHistory' => is_null($password) && $passwordHistory === 0 ? [] : [$password], 'passwordUpdate' => (!empty($password)) ? DateTime::now() : null, @@ -663,6 +665,45 @@ App::patch('/v1/users/:userId/status') $response->dynamic($user, Response::MODEL_USER); }); +App::put('/v1/users/:userId/labels') + ->desc('Update User Labels') + ->groups(['api', 'users']) + ->label('event', 'users.[userId].update.labels') + ->label('scope', 'users.write') + ->label('audits.event', 'user.update') + ->label('audits.resource', 'user/{response.$id}') + ->label('audits.userId', '{response.$id}') + ->label('usage.metric', 'users.{scope}.requests.update') + ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) + ->label('sdk.namespace', 'users') + ->label('sdk.method', 'updateLabels') + ->label('sdk.description', '/docs/references/users/update-user-labels.md') + ->label('sdk.response.code', Response::STATUS_CODE_OK) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_USER) + ->param('userId', '', new UID(), 'User ID.') + ->param('labels', [], new ArrayList(new Text(36, allowList: [...Text::NUMBERS, ...Text::ALPHABET_UPPER, ...Text::ALPHABET_LOWER]), 5), 'Array of user labels. Replaces the previous labels. Maximum of 5 labels are allowed, each up to 36 alphanumeric characters long.') + ->inject('response') + ->inject('dbForProject') + ->inject('events') + ->action(function (string $userId, array $labels, Response $response, Database $dbForProject, Event $events) { + + $user = $dbForProject->getDocument('users', $userId); + + if ($user->isEmpty()) { + throw new Exception(Exception::USER_NOT_FOUND); + } + + $user->setAttribute('labels', (array) \array_values(\array_unique($labels))); + + $user = $dbForProject->updateDocument('users', $user->getId(), $user); + + $events + ->setParam('userId', $user->getId()); + + $response->dynamic($user, Response::MODEL_USER); + }); + App::patch('/v1/users/:userId/verification') ->desc('Update Email Verification') ->groups(['api', 'users']) diff --git a/docs/references/users/update-user-labels.md b/docs/references/users/update-user-labels.md new file mode 100644 index 0000000000..3ff0388eae --- /dev/null +++ b/docs/references/users/update-user-labels.md @@ -0,0 +1,3 @@ +Update the user labels by its unique ID. + +Labels can be used to grant access to resources. While teams are a way for user's to share access to a resource, labels can be defined by the developer to grant access without an invitation. See the [Permissions docs](/docs/permissions) for more info. \ No newline at end of file diff --git a/tests/e2e/Services/Users/UsersBase.php b/tests/e2e/Services/Users/UsersBase.php index cb83faea82..baf601789a 100644 --- a/tests/e2e/Services/Users/UsersBase.php +++ b/tests/e2e/Services/Users/UsersBase.php @@ -3,6 +3,7 @@ namespace Tests\E2E\Services\Users; use Appwrite\Tests\Retry; +use Appwrite\Utopia\Response; use Tests\E2E\Client; use Utopia\Database\Helpers\ID; @@ -1016,6 +1017,98 @@ trait UsersBase $this->assertEquals($response['body']['users'][0]['phone'], $newNumber); } + /** + * @return array{} + */ + public function userLabelsProvider() + { + return [ + 'single label' => [ + ['admin'], + Response::STATUS_CODE_OK, + ['admin'], + ], + 'replace with multiple labels' => [ + ['vip', 'pro'], + Response::STATUS_CODE_OK, + ['vip', 'pro'], + ], + 'clear labels' => [ + [], + Response::STATUS_CODE_OK, + [], + ], + 'duplicate labels' => [ + ['vip', 'vip', 'pro'], + Response::STATUS_CODE_OK, + ['vip', 'pro'], + ], + 'invalid label' => [ + ['invalid-label'], + Response::STATUS_CODE_BAD_REQUEST, + [], + ], + 'too long' => [ + [\str_repeat('a', 129)], + Response::STATUS_CODE_BAD_REQUEST, + [], + ], + 'too many labels' => [ + [\array_fill(0, 101, 'a')], + Response::STATUS_CODE_BAD_REQUEST, + [], + ], + ]; + } + + /** + * @depends testGetUser + * @dataProvider userLabelsProvider + */ + public function testUpdateUserLabels(array $labels, int $expectedStatus, array $expectedLabels, array $data): array + { + $user = $this->client->call(Client::METHOD_PUT, '/users/' . $data['userId'] . '/labels', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'labels' => $labels, + ]); + + $this->assertEquals($expectedStatus, $user['headers']['status-code']); + if ($expectedStatus === Response::STATUS_CODE_OK) { + $this->assertEquals($user['body']['labels'], $expectedLabels); + } + + return $data; + } + + /** + * @depends testGetUser + */ + public function testUpdateUserLabelsWithoutLabels(array $data): array + { + $user = $this->client->call(Client::METHOD_PUT, '/users/' . $data['userId'] . '/labels', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), []); + + $this->assertEquals(Response::STATUS_CODE_BAD_REQUEST, $user['headers']['status-code']); + + return $data; + } + + public function testUpdateUserLabelsNonExistentUser(): void + { + $user = $this->client->call(Client::METHOD_PUT, '/users/dne/labels', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'labels' => ['admin'], + ]); + + $this->assertEquals(Response::STATUS_CODE_NOT_FOUND, $user['headers']['status-code']); + } + /** * @depends testGetUser