From 518a37d2946108257a3d5fbbcbe1b2f182227a80 Mon Sep 17 00:00:00 2001 From: Steven Nguyen Date: Thu, 27 Apr 2023 19:10:42 -0700 Subject: [PATCH 01/20] Add a generic OIDC OAuth2 provider --- app/config/providers.php | 10 + src/Appwrite/Auth/OAuth2/Oidc.php | 296 ++++++++++++++++++++++++++++++ 2 files changed, 306 insertions(+) create mode 100644 src/Appwrite/Auth/OAuth2/Oidc.php diff --git a/app/config/providers.php b/app/config/providers.php index 62430faae1..5e4dc0aa11 100644 --- a/app/config/providers.php +++ b/app/config/providers.php @@ -201,6 +201,16 @@ return [ // Ordered by ABC. 'beta' => false, 'mock' => false, ], + 'oidc' => [ + 'name' => 'OpenID Connect', + 'developers' => 'https://openid.net/connect/faq/', + 'icon' => 'icon-oidc', + 'enabled' => true, + 'sandbox' => false, + 'form' => 'oidc.phtml', + 'beta' => false, + 'mock' => false, + ], 'okta' => [ 'name' => 'Okta', 'developers' => 'https://developer.okta.com/', diff --git a/src/Appwrite/Auth/OAuth2/Oidc.php b/src/Appwrite/Auth/OAuth2/Oidc.php new file mode 100644 index 0000000000..de2eab65c8 --- /dev/null +++ b/src/Appwrite/Auth/OAuth2/Oidc.php @@ -0,0 +1,296 @@ +getAuthorizationEndpoint() . '?' . \http_build_query([ + 'client_id' => $this->appID, + 'redirect_uri' => $this->callback, + 'state' => \json_encode($this->state), + 'scope' => \implode(' ', $this->getScopes()), + 'response_type' => 'code', + ]); + } + + /** + * @param string $code + * + * @return array + */ + protected function getTokens(string $code): array + { + if (empty($this->tokens)) { + $headers = ['Content-Type: application/x-www-form-urlencoded']; + $this->tokens = \json_decode($this->request( + 'POST', + $this->getTokenEndpoint(), + $headers, + \http_build_query([ + 'code' => $code, + 'client_id' => $this->appID, + 'client_secret' => $this->getClientSecret(), + 'redirect_uri' => $this->callback, + 'scope' => \implode(' ', $this->getScopes()), + 'grant_type' => 'authorization_code' + ]) + ), true); + } + return $this->tokens; + } + + + /** + * @param string $refreshToken + * + * @return array + */ + public function refreshTokens(string $refreshToken): array + { + $headers = ['Content-Type: application/x-www-form-urlencoded']; + $this->tokens = \json_decode($this->request( + 'POST', + $this->getTokenEndpoint(), + $headers, + \http_build_query([ + 'refresh_token' => $refreshToken, + 'client_id' => $this->appID, + 'client_secret' => $this->getClientSecret(), + 'grant_type' => 'refresh_token' + ]) + ), true); + + if (empty($this->tokens['refresh_token'])) { + $this->tokens['refresh_token'] = $refreshToken; + } + + return $this->tokens; + } + + /** + * @param string $accessToken + * + * @return string + */ + public function getUserID(string $accessToken): string + { + $user = $this->getUser($accessToken); + + if (isset($user['sub'])) { + return $user['sub']; + } + + return ''; + } + + /** + * @param string $accessToken + * + * @return string + */ + public function getUserEmail(string $accessToken): string + { + $user = $this->getUser($accessToken); + + if (isset($user['email'])) { + return $user['email']; + } + + return ''; + } + + /** + * Check if the User email is verified + * + * @param string $accessToken + * + * @return bool + */ + public function isEmailVerified(string $accessToken): bool + { + $user = $this->getUser($accessToken); + + return $user['email_verified'] ?? false; + } + + /** + * @param string $accessToken + * + * @return string + */ + public function getUserName(string $accessToken): string + { + $user = $this->getUser($accessToken); + + if (isset($user['name'])) { + return $user['name']; + } + + return ''; + } + + /** + * @param string $accessToken + * + * @return array + */ + protected function getUser(string $accessToken): array + { + if (empty($this->user)) { + $headers = ['Authorization: Bearer ' . \urlencode($accessToken)]; + $user = $this->request('GET', $this->getUserinfoEndpoint(), $headers); + $this->user = \json_decode($user, true); + } + + return $this->user; + } + + /** + * Extracts the Client Secret from the JSON stored in appSecret + * + * @return string + */ + protected function getClientSecret(): string + { + $secret = $this->getAppSecret(); + + return $secret['clientSecret'] ?? ''; + } + + /** + * Extracts the well known endpoint from the JSON stored in appSecret. + * + * @return string + */ + protected function getWellKnownEndpoint(): string + { + $secret = $this->getAppSecret(); + return $secret['wellKnownEndpoint'] ?? ''; + } + + /** + * Extracts the authorization endpoint from the JSON stored in appSecret. + * + * If one is not provided, it will be retrieved from the well-known configuration. + * + * @return string + */ + protected function getAuthorizationEndpoint(): string + { + $secret = $this->getAppSecret(); + + $endpoint = $secret['authorizationEndpoint'] ?? ''; + if (!empty($endpoint)) { + return $endpoint; + } + + $wellKnownConfiguration = $this->getWellKnownConfiguration(); + return $wellKnownConfiguration['authorization_endpoint'] ?? ''; + } + + /** + * Extracts the token endpoint from the JSON stored in appSecret. + * + * If one is not provided, it will be retrieved from the well-known configuration. + * + * @return string + */ + protected function getTokenEndpoint(): string + { + $secret = $this->getAppSecret(); + + $endpoint = $secret['tokenEndpoint'] ?? ''; + if (!empty($endpoint)) { + return $endpoint; + } + + $wellKnownConfiguration = $this->getWellKnownConfiguration(); + return $wellKnownConfiguration['token_endpoint'] ?? ''; + } + + /** + * Extracts the userinfo endpoint from the JSON stored in appSecret. + * + * If one is not provided, it will be retrieved from the well-known configuration. + * + * @return string + */ + protected function getUserinfoEndpoint(): string + { + $secret = $this->getAppSecret(); + $endpoint = $secret['userinfoEndpoint'] ?? ''; + if (!empty($endpoint)) { + return $endpoint; + } + + $wellKnownConfiguration = $this->getWellKnownConfiguration(); + return $wellKnownConfiguration['userinfo_endpoint'] ?? ''; + } + + /** + * Get the well-known configuration using the well known endpoint + */ + protected function getWellKnownConfiguration(): array + { + if (empty($this->wellKnownConfiguration)) { + $response = $this->request('GET', $this->getWellKnownEndpoint()); + $this->wellKnownConfiguration = \json_decode($response, true); + } + + return $this->wellKnownConfiguration; + } + + /** + * Decode the JSON stored in appSecret + * + * @return array + */ + protected function getAppSecret(): array + { + try { + $secret = \json_decode($this->appSecret, true, 512, JSON_THROW_ON_ERROR); + } catch (\Throwable $th) { + throw new \Exception('Invalid secret'); + } + return $secret; + } +} From 6edea596b49ab183f7779cb1d10e25fed18cbdfe Mon Sep 17 00:00:00 2001 From: "Vincent (Wen Yu) Ge" Date: Wed, 14 Jun 2023 17:49:52 +0000 Subject: [PATCH 02/20] Update delete session description to be actually correct --- docs/references/account/delete-session.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/references/account/delete-session.md b/docs/references/account/delete-session.md index cd1f22f627..78d43cf890 100644 --- a/docs/references/account/delete-session.md +++ b/docs/references/account/delete-session.md @@ -1 +1 @@ -Use this endpoint to log out the currently logged in user from all their account sessions across all of their different devices. When using the Session ID argument, only the unique session ID provided is deleted. +Logout the current user. Use 'current' as the session ID to logout on this device, or provide a specific session ID to target a different session. \ No newline at end of file From 57621de57d47828f1aa45faa85362f63d8242d38 Mon Sep 17 00:00:00 2001 From: "Vincent (Wen Yu) Ge" Date: Thu, 15 Jun 2023 18:38:15 +0000 Subject: [PATCH 03/20] Damodar's comments --- docs/references/account/delete-session.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/references/account/delete-session.md b/docs/references/account/delete-session.md index 78d43cf890..c7439638af 100644 --- a/docs/references/account/delete-session.md +++ b/docs/references/account/delete-session.md @@ -1 +1 @@ -Logout the current user. Use 'current' as the session ID to logout on this device, or provide a specific session ID to target a different session. \ No newline at end of file +Logout the user. Use 'current' as the session ID to logout on this device, use a session ID to logout on another device. If you're looking to logout the user on all devices, use [Delete Sessions](/docs/client/account#accountDeleteSessions) instead. \ No newline at end of file From 70cebe4460c277e5f594f1ecc16de79636f5723a Mon Sep 17 00:00:00 2001 From: Mendel Gordon Date: Thu, 29 Jun 2023 10:32:03 -0400 Subject: [PATCH 04/20] Update create-magic-url-session.md Fix typo --- docs/references/account/create-magic-url-session.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/references/account/create-magic-url-session.md b/docs/references/account/create-magic-url-session.md index 03383713ed..b1e63e144c 100644 --- a/docs/references/account/create-magic-url-session.md +++ b/docs/references/account/create-magic-url-session.md @@ -1,3 +1,3 @@ -Sends the user an email with a secret key for creating a session. If the provided user ID has not be registered, a new user will be created. When the user clicks the link in the email, the user is redirected back to the URL you provided with the secret key and userId values attached to the URL query string. Use the query string parameters to submit a request to the [PUT /account/sessions/magic-url](/docs/client/account#accountUpdateMagicURLSession) endpoint to complete the login process. The link sent to the user's email address is valid for 1 hour. If you are on a mobile device you can leave the URL parameter empty, so that the login completion will be handled by your Appwrite instance by default. +Sends the user an email with a secret key for creating a session. If the provided user ID has not been registered, a new user will be created. When the user clicks the link in the email, the user is redirected back to the URL you provided with the secret key and userId values attached to the URL query string. Use the query string parameters to submit a request to the [PUT /account/sessions/magic-url](/docs/client/account#accountUpdateMagicURLSession) endpoint to complete the login process. The link sent to the user's email address is valid for 1 hour. If you are on a mobile device you can leave the URL parameter empty, so that the login completion will be handled by your Appwrite instance by default. -A user is limited to 10 active sessions at a time by default. [Learn more about session limits](/docs/authentication-security#limits). \ No newline at end of file +A user is limited to 10 active sessions at a time by default. [Learn more about session limits](/docs/authentication-security#limits). From 131e9658a5ef644e3d422f554d1ae0e5601d561c Mon Sep 17 00:00:00 2001 From: Prateek Banga Date: Wed, 19 Jul 2023 14:04:35 +0530 Subject: [PATCH 05/20] change to use findOne instead of iterating index array --- app/controllers/api/databases.php | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/app/controllers/api/databases.php b/app/controllers/api/databases.php index 4459322baf..eb37bea9f0 100644 --- a/app/controllers/api/databases.php +++ b/app/controllers/api/databases.php @@ -2535,20 +2535,18 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/indexes/:key') throw new Exception(Exception::COLLECTION_NOT_FOUND); } - $indexes = $collection->getAttribute('indexes'); - - // Search for index - $indexIndex = array_search($key, array_map(fn($idx) => $idx['key'], $indexes)); - - if ($indexIndex === false) { + $index = $dbForProject->findOne('indexes', [ + Query::equal('$id',[$database->getInternalId().'_'.$collection->getInternalId().'_'.$key]) + ]); + + if ($index->isEmpty()) { throw new Exception(Exception::INDEX_NOT_FOUND); } - $index = $indexes[$indexIndex]; $index->setAttribute('collectionId', $database->getInternalId() . '_' . $collectionId); $response->dynamic($index, Response::MODEL_INDEX); - }); + });; App::delete('/v1/databases/:databaseId/collections/:collectionId/indexes/:key') ->alias('/v1/database/collections/:collectionId/indexes/:key', ['databaseId' => 'default']) From 8328dac86bb3c6d3010eedaa7063c8cab77af565 Mon Sep 17 00:00:00 2001 From: Prateek Banga Date: Wed, 19 Jul 2023 14:08:02 +0530 Subject: [PATCH 06/20] lint issues --- app/controllers/api/databases.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/controllers/api/databases.php b/app/controllers/api/databases.php index eb37bea9f0..37694fcb65 100644 --- a/app/controllers/api/databases.php +++ b/app/controllers/api/databases.php @@ -2536,9 +2536,9 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/indexes/:key') } $index = $dbForProject->findOne('indexes', [ - Query::equal('$id',[$database->getInternalId().'_'.$collection->getInternalId().'_'.$key]) + Query::equal('$id', [$database->getInternalId() . '_' . $collection->getInternalId() . '_' . $key]) ]); - + if ($index->isEmpty()) { throw new Exception(Exception::INDEX_NOT_FOUND); } @@ -2546,7 +2546,8 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/indexes/:key') $index->setAttribute('collectionId', $database->getInternalId() . '_' . $collectionId); $response->dynamic($index, Response::MODEL_INDEX); - });; + }); +; App::delete('/v1/databases/:databaseId/collections/:collectionId/indexes/:key') ->alias('/v1/database/collections/:collectionId/indexes/:key', ['databaseId' => 'default']) From 803d653e44ea0a9118a34cbc0df6c53852828bfe Mon Sep 17 00:00:00 2001 From: Steven <1477010+stnguyen90@users.noreply.github.com> Date: Wed, 19 Jul 2023 18:29:01 +0000 Subject: [PATCH 07/20] Upgrade utopia-php/domains Version 1.1.0 was an incorrectly named git tag; it was equivalent to 0.2.0. This bump to 0.3.* is safe because it adds Registrar, but it won't be used in Appwrite yet. Bumping to 0.3.* is necessary to fix some bugs where valid domains were considered invalid. --- composer.json | 2 +- composer.lock | 36 +++++++++++++++++++++--------------- 2 files changed, 22 insertions(+), 16 deletions(-) diff --git a/composer.json b/composer.json index 0ba26540bd..8560f450bc 100644 --- a/composer.json +++ b/composer.json @@ -50,7 +50,7 @@ "utopia-php/cli": "0.13.*", "utopia-php/config": "0.2.*", "utopia-php/database": "0.36.*", - "utopia-php/domains": "1.1.*", + "utopia-php/domains": "0.3.*", "utopia-php/framework": "0.28.*", "utopia-php/image": "0.5.*", "utopia-php/locale": "0.4.*", diff --git a/composer.lock b/composer.lock index 0c6b961ea2..303130bea5 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "3eadbfe5543aafdf8682ea0465159e3c", + "content-hash": "21b8661eef2ca7648831651b99a11544", "packages": [ { "name": "adhocore/jwt", @@ -2164,23 +2164,25 @@ }, { "name": "utopia-php/domains", - "version": "v1.1.0", + "version": "0.3.2", "source": { "type": "git", "url": "https://github.com/utopia-php/domains.git", - "reference": "1665e1d9932afa3be63b5c1e0dcfe01fe77d8e73" + "reference": "aaa8c9a96c69ccb397997b1f4f2299c66f77eefb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/domains/zipball/1665e1d9932afa3be63b5c1e0dcfe01fe77d8e73", - "reference": "1665e1d9932afa3be63b5c1e0dcfe01fe77d8e73", + "url": "https://api.github.com/repos/utopia-php/domains/zipball/aaa8c9a96c69ccb397997b1f4f2299c66f77eefb", + "reference": "aaa8c9a96c69ccb397997b1f4f2299c66f77eefb", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=8.0", + "utopia-php/framework": "0.*.*" }, "require-dev": { - "phpunit/phpunit": "^7.0" + "laravel/pint": "1.2.*", + "phpunit/phpunit": "^9.3" }, "type": "library", "autoload": { @@ -2196,6 +2198,10 @@ { "name": "Eldad Fux", "email": "eldad@appwrite.io" + }, + { + "name": "Wess Cope", + "email": "wess@appwrite.io" } ], "description": "Utopia Domains library is simple and lite library for parsing web domains. This library is aiming to be as simple and easy to learn and use.", @@ -2212,9 +2218,9 @@ ], "support": { "issues": "https://github.com/utopia-php/domains/issues", - "source": "https://github.com/utopia-php/domains/tree/master" + "source": "https://github.com/utopia-php/domains/tree/0.3.2" }, - "time": "2020-02-23T07:40:02+00:00" + "time": "2023-07-19T16:39:24+00:00" }, { "name": "utopia-php/framework", @@ -3031,16 +3037,16 @@ "packages-dev": [ { "name": "appwrite/sdk-generator", - "version": "0.33.6", + "version": "0.33.7", "source": { "type": "git", "url": "https://github.com/appwrite/sdk-generator.git", - "reference": "237fe97b68090a244382c36f96482c352880a38c" + "reference": "9f5db4a637b23879ceacea9ed2d33b0486771ffc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/237fe97b68090a244382c36f96482c352880a38c", - "reference": "237fe97b68090a244382c36f96482c352880a38c", + "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/9f5db4a637b23879ceacea9ed2d33b0486771ffc", + "reference": "9f5db4a637b23879ceacea9ed2d33b0486771ffc", "shasum": "" }, "require": { @@ -3076,9 +3082,9 @@ "description": "Appwrite PHP library for generating API SDKs for multiple programming languages and platforms", "support": { "issues": "https://github.com/appwrite/sdk-generator/issues", - "source": "https://github.com/appwrite/sdk-generator/tree/0.33.6" + "source": "https://github.com/appwrite/sdk-generator/tree/0.33.7" }, - "time": "2023-07-10T16:27:53+00:00" + "time": "2023-07-12T12:15:43+00:00" }, { "name": "doctrine/deprecations", From cc42700299b307893ae429e43aaf5e5a036bb915 Mon Sep 17 00:00:00 2001 From: Steven <1477010+stnguyen90@users.noreply.github.com> Date: Wed, 19 Jul 2023 23:52:33 +0000 Subject: [PATCH 08/20] Fix test after session expire format was updated --- tests/e2e/Services/Account/AccountBase.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/e2e/Services/Account/AccountBase.php b/tests/e2e/Services/Account/AccountBase.php index b08686fa48..f6d10653d8 100644 --- a/tests/e2e/Services/Account/AccountBase.php +++ b/tests/e2e/Services/Account/AccountBase.php @@ -1296,7 +1296,7 @@ trait AccountBase $token = substr($lastEmail['text'], strpos($lastEmail['text'], '&secret=', 0) + 8, 256); - $expireTime = strpos($lastEmail['text'], 'expire=' . urlencode(DateTime::format(new \DateTime($response['body']['expire']))), 0); + $expireTime = strpos($lastEmail['text'], 'expire=' . urlencode($response['body']['expire']), 0); $this->assertNotFalse($expireTime); From 35a25cc2a74e1e33241c3fe85a9ca45705d1e133 Mon Sep 17 00:00:00 2001 From: Steven Nguyen Date: Wed, 12 Jul 2023 10:06:12 -0700 Subject: [PATCH 09/20] Bump audit version to get rid of userInternalId attribute --- composer.json | 2 +- composer.lock | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/composer.json b/composer.json index 8560f450bc..b02a6111bc 100644 --- a/composer.json +++ b/composer.json @@ -45,7 +45,7 @@ "appwrite/php-runtimes": "0.11.*", "utopia-php/abuse": "0.25.*", "utopia-php/analytics": "0.2.*", - "utopia-php/audit": "0.26.*", + "utopia-php/audit": "0.27.*", "utopia-php/cache": "0.8.*", "utopia-php/cli": "0.13.*", "utopia-php/config": "0.2.*", diff --git a/composer.lock b/composer.lock index 303130bea5..5e6640b9fb 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "21b8661eef2ca7648831651b99a11544", + "content-hash": "a15f62920dedaf99e321ffd94e89d9f4", "packages": [ { "name": "adhocore/jwt", @@ -1906,16 +1906,16 @@ }, { "name": "utopia-php/audit", - "version": "0.26.0", + "version": "0.27.0", "source": { "type": "git", "url": "https://github.com/utopia-php/audit.git", - "reference": "e7228080f14df28737fbb050c180c26d86ac0403" + "reference": "bdf89d7fe381bd4c891ad217612580a35e8c7642" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/audit/zipball/e7228080f14df28737fbb050c180c26d86ac0403", - "reference": "e7228080f14df28737fbb050c180c26d86ac0403", + "url": "https://api.github.com/repos/utopia-php/audit/zipball/bdf89d7fe381bd4c891ad217612580a35e8c7642", + "reference": "bdf89d7fe381bd4c891ad217612580a35e8c7642", "shasum": "" }, "require": { @@ -1947,9 +1947,9 @@ ], "support": { "issues": "https://github.com/utopia-php/audit/issues", - "source": "https://github.com/utopia-php/audit/tree/0.26.0" + "source": "https://github.com/utopia-php/audit/tree/0.27.0" }, - "time": "2023-04-27T15:43:50+00:00" + "time": "2023-05-15T07:04:48+00:00" }, { "name": "utopia-php/cache", From 58fa7546c15c878ffea2a0e8bd87a302056c5bf3 Mon Sep 17 00:00:00 2001 From: Steven Nguyen Date: Wed, 12 Jul 2023 10:18:34 -0700 Subject: [PATCH 10/20] Update Appwrite to save internal id as audit user id It's important to use userInternalId so that if a user is recreated with the same ID, lookups for the user will not return the data of the old deleted user. We will still store userId in data so that it can be pulled out and returned for the log API calls. --- app/workers/audits.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/workers/audits.php b/app/workers/audits.php index b25430ec41..57614e60f6 100644 --- a/app/workers/audits.php +++ b/app/workers/audits.php @@ -40,8 +40,7 @@ class AuditsV1 extends Worker $dbForProject = $this->getProjectDB($project->getId()); $audit = new Audit($dbForProject); $audit->log( - userInternalId: $user->getInternalId(), - userId: $user->getId(), + userId: $user->getInternalId(), // Pass first, most verbose event pattern event: $event, resource: $resource, @@ -49,6 +48,7 @@ class AuditsV1 extends Worker ip: $ip, location: '', data: [ + 'userId' => $user->getId(), 'userName' => $userName, 'userEmail' => $userEmail, 'mode' => $mode, From 187f3dc6ff85a4c4a2239da9fe5e6e4c2f58ab53 Mon Sep 17 00:00:00 2001 From: Steven Nguyen Date: Wed, 12 Jul 2023 10:27:57 -0700 Subject: [PATCH 11/20] Update get logs APIs to return userId from data The audit userId is actually the internal id and the user id is in the data attribute. --- app/controllers/api/databases.php | 6 +++--- app/controllers/api/teams.php | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/controllers/api/databases.php b/app/controllers/api/databases.php index 4459322baf..29f9c6d0be 100644 --- a/app/controllers/api/databases.php +++ b/app/controllers/api/databases.php @@ -566,7 +566,7 @@ App::get('/v1/databases/:databaseId/logs') $output[$i] = new Document([ 'event' => $log['event'], - 'userId' => ID::custom($log['userId']), + 'userId' => ID::custom($log['data']['userId']), 'userEmail' => $log['data']['userEmail'] ?? null, 'userName' => $log['data']['userName'] ?? null, 'mode' => $log['data']['mode'] ?? null, @@ -917,7 +917,7 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/logs') $output[$i] = new Document([ 'event' => $log['event'], - 'userId' => $log['userId'], + 'userId' => $log['data']['userId'], 'userEmail' => $log['data']['userEmail'] ?? null, 'userName' => $log['data']['userName'] ?? null, 'mode' => $log['data']['mode'] ?? null, @@ -3149,7 +3149,7 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents/:documen $output[$i] = new Document([ 'event' => $log['event'], - 'userId' => $log['userId'], + 'userId' => $log['data']['userId'], 'userEmail' => $log['data']['userEmail'] ?? null, 'userName' => $log['data']['userName'] ?? null, 'mode' => $log['data']['mode'] ?? null, diff --git a/app/controllers/api/teams.php b/app/controllers/api/teams.php index cdac9ba2fb..a06ab6b2a0 100644 --- a/app/controllers/api/teams.php +++ b/app/controllers/api/teams.php @@ -1043,7 +1043,7 @@ App::get('/v1/teams/:teamId/logs') $output[$i] = new Document([ 'event' => $log['event'], - 'userId' => $log['userId'], + 'userId' => $log['data']['userId'], 'userEmail' => $log['data']['userEmail'] ?? null, 'userName' => $log['data']['userName'] ?? null, 'mode' => $log['data']['mode'] ?? null, From 9908a9021f6504d6d74ee5a8ea0379270ac6ac8e Mon Sep 17 00:00:00 2001 From: Steven Nguyen Date: Wed, 12 Jul 2023 10:56:24 -0700 Subject: [PATCH 12/20] Update get logs by user to pass user internal id The userId in audit is actually the userInternalId. --- app/controllers/api/account.php | 2 +- app/controllers/api/users.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index 3cd2c7bd70..37485b92d8 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -1427,7 +1427,7 @@ App::get('/v1/account/logs') $audit = new EventAudit($dbForProject); - $logs = $audit->getLogsByUser($user->getId(), $limit, $offset); + $logs = $audit->getLogsByUser($user->getInternalId(), $limit, $offset); $output = []; diff --git a/app/controllers/api/users.php b/app/controllers/api/users.php index cd217afe31..d84d83ff77 100644 --- a/app/controllers/api/users.php +++ b/app/controllers/api/users.php @@ -577,7 +577,7 @@ App::get('/v1/users/:userId/logs') $audit = new Audit($dbForProject); - $logs = $audit->getLogsByUser($user->getId(), $limit, $offset); + $logs = $audit->getLogsByUser($user->getInternalId(), $limit, $offset); $output = []; From cd78706944aff2372d5db684b84241a6a1a84d52 Mon Sep 17 00:00:00 2001 From: Steven Nguyen Date: Wed, 12 Jul 2023 10:57:55 -0700 Subject: [PATCH 13/20] Update migration for audit collection 1. Remove the userInternalId attribute 2. Replace userId in audit documents with userInternalId and put userId in data --- src/Appwrite/Migration/Version/V18.php | 38 ++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/src/Appwrite/Migration/Version/V18.php b/src/Appwrite/Migration/Version/V18.php index be628e0fbb..839269f940 100644 --- a/src/Appwrite/Migration/Version/V18.php +++ b/src/Appwrite/Migration/Version/V18.php @@ -133,6 +133,16 @@ class V18 extends Migration Console::warning("'options' from {$id}: {$th->getMessage()}"); } break; + case 'audit': + try { + /** + * Delete 'userInternalId' attribute + */ + $this->projectDB->deleteAttribute($id, 'userInternalId'); + } catch (\Throwable $th) { + Console::warning("'userInternalId' from {$id}: {$th->getMessage()}"); + } + break; default: break; } @@ -195,6 +205,34 @@ class V18 extends Migration Console::warning($th->getMessage()); } break; + case 'audit': + /** + * Set the userId to the userInternalId and add userId to data + */ + try { + $userId = $document->getAttribute('userId'); + $data = $document->getAttribute('data', []); + $mode = $data['mode'] ?? 'default'; + $user = match ($mode) { + 'admin' => $this->consoleDB->getDocument('users', $userId), + default => $this->projectDB->getDocument('users', $userId), + }; + + if ($user->isEmpty()) { + // The audit userId could already be an internal Id. + // Otherwise, the user could have been deleted. + // Nonetheless, there's nothing else we can do here. + break; + } + $internalId = $user->getInternalId(); + $document->setAttribute('userId', $internalId); + $data = $document->getAttribute('data', []); + $data['userId'] = $user->getId(); + $document->setAttribute('data', $data); + } catch (\Throwable $th) { + Console::warning($th->getMessage()); + } + break; } return $document; From 3f9cedcc44e766f25c0a1f91f4c70d56bb81d0bf Mon Sep 17 00:00:00 2001 From: Steven <1477010+stnguyen90@users.noreply.github.com> Date: Tue, 18 Jul 2023 22:21:58 +0000 Subject: [PATCH 14/20] Prepare 1.3.8 release --- CHANGES.md | 5 +++++ README-CN.md | 6 +++--- README.md | 6 +++--- app/init.php | 2 +- src/Appwrite/Migration/Migration.php | 1 + 5 files changed, 13 insertions(+), 7 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index d19c8490b7..fb46b2b7d1 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,8 @@ +# Version 1.3.8 + +## Bugs +- Fix audit user internal [#5809](https://github.com/appwrite/appwrite/pull/5809) + # Version 1.3.7 ## Bugs diff --git a/README-CN.md b/README-CN.md index d3f52058b5..b634458547 100644 --- a/README-CN.md +++ b/README-CN.md @@ -66,7 +66,7 @@ docker run -it --rm \ --volume /var/run/docker.sock:/var/run/docker.sock \ --volume "$(pwd)"/appwrite:/usr/src/code/appwrite:rw \ --entrypoint="install" \ - appwrite/appwrite:1.3.7 + appwrite/appwrite:1.3.8 ``` ### Windows @@ -78,7 +78,7 @@ docker run -it --rm ^ --volume //var/run/docker.sock:/var/run/docker.sock ^ --volume "%cd%"/appwrite:/usr/src/code/appwrite:rw ^ --entrypoint="install" ^ - appwrite/appwrite:1.3.7 + appwrite/appwrite:1.3.8 ``` #### PowerShell @@ -88,7 +88,7 @@ docker run -it --rm ` --volume /var/run/docker.sock:/var/run/docker.sock ` --volume ${pwd}/appwrite:/usr/src/code/appwrite:rw ` --entrypoint="install" ` - appwrite/appwrite:1.3.7 + appwrite/appwrite:1.3.8 ``` 运行后,可以在浏览器上访问 http://localhost 找到 Appwrite 控制台。在非 Linux 的本机主机上完成安装后,服务器可能需要几分钟才能启动。 diff --git a/README.md b/README.md index b8fffd110a..5099cd6238 100644 --- a/README.md +++ b/README.md @@ -75,7 +75,7 @@ docker run -it --rm \ --volume /var/run/docker.sock:/var/run/docker.sock \ --volume "$(pwd)"/appwrite:/usr/src/code/appwrite:rw \ --entrypoint="install" \ - appwrite/appwrite:1.3.7 + appwrite/appwrite:1.3.8 ``` ### Windows @@ -87,7 +87,7 @@ docker run -it --rm ^ --volume //var/run/docker.sock:/var/run/docker.sock ^ --volume "%cd%"/appwrite:/usr/src/code/appwrite:rw ^ --entrypoint="install" ^ - appwrite/appwrite:1.3.7 + appwrite/appwrite:1.3.8 ``` #### PowerShell @@ -97,7 +97,7 @@ docker run -it --rm ` --volume /var/run/docker.sock:/var/run/docker.sock ` --volume ${pwd}/appwrite:/usr/src/code/appwrite:rw ` --entrypoint="install" ` - appwrite/appwrite:1.3.7 + appwrite/appwrite:1.3.8 ``` Once the Docker installation is complete, go to http://localhost to access the Appwrite console from your browser. Please note that on non-Linux native hosts, the server might take a few minutes to start after completing the installation. diff --git a/app/init.php b/app/init.php index fc7541513d..132acb62ce 100644 --- a/app/init.php +++ b/app/init.php @@ -101,7 +101,7 @@ const APP_LIMIT_LIST_DEFAULT = 25; // Default maximum number of items to return const APP_KEY_ACCCESS = 24 * 60 * 60; // 24 hours const APP_CACHE_UPDATE = 24 * 60 * 60; // 24 hours const APP_CACHE_BUSTER = 506; -const APP_VERSION_STABLE = '1.3.7'; +const APP_VERSION_STABLE = '1.3.8'; const APP_DATABASE_ATTRIBUTE_EMAIL = 'email'; const APP_DATABASE_ATTRIBUTE_ENUM = 'enum'; const APP_DATABASE_ATTRIBUTE_IP = 'ip'; diff --git a/src/Appwrite/Migration/Migration.php b/src/Appwrite/Migration/Migration.php index 7a1f3cad24..ffb5ce6ac4 100644 --- a/src/Appwrite/Migration/Migration.php +++ b/src/Appwrite/Migration/Migration.php @@ -63,6 +63,7 @@ abstract class Migration '1.3.5' => 'V18', '1.3.6' => 'V18', '1.3.7' => 'V18', + '1.3.8' => 'V18', ]; /** From 1e0158cdef7658ad6f83de91df2623256d58e230 Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Thu, 20 Jul 2023 11:35:59 +0545 Subject: [PATCH 15/20] Update README.md --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index b8fffd110a..5613d4f560 100644 --- a/README.md +++ b/README.md @@ -128,6 +128,12 @@ Choose from one of the providers below:
Gitpod + + + Akamai Logo +
Akamai
+ + From 2c2c7d9028eafe333d47c94294bca75762292891 Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Thu, 20 Jul 2023 11:36:40 +0545 Subject: [PATCH 16/20] Create akamai-logo.svg --- public/images/integrations/akamai-logo.svg | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 public/images/integrations/akamai-logo.svg diff --git a/public/images/integrations/akamai-logo.svg b/public/images/integrations/akamai-logo.svg new file mode 100644 index 0000000000..cdcf31b4c2 --- /dev/null +++ b/public/images/integrations/akamai-logo.svg @@ -0,0 +1,3 @@ + + + From b700336d45c64a386939db34cc8db93119290c12 Mon Sep 17 00:00:00 2001 From: Prateek Banga Date: Thu, 20 Jul 2023 14:52:05 +0530 Subject: [PATCH 17/20] fix get index route to use find --- app/controllers/api/databases.php | 21 +++------------------ 1 file changed, 3 insertions(+), 18 deletions(-) diff --git a/app/controllers/api/databases.php b/app/controllers/api/databases.php index 37694fcb65..d16bd57f4c 100644 --- a/app/controllers/api/databases.php +++ b/app/controllers/api/databases.php @@ -2524,28 +2524,13 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/indexes/:key') ->inject('dbForProject') ->action(function (string $databaseId, string $collectionId, string $key, Response $response, Database $dbForProject) { - $database = Authorization::skip(fn() => $dbForProject->getDocument('databases', $databaseId)); + $index = $dbForProject->find('indexes', [Query::equal('collectionId', [$collectionId]), Query::equal('key', [$key]), Query::equal('databaseId', [$databaseId]), Query::limit(1)]); - if ($database->isEmpty()) { - throw new Exception(Exception::DATABASE_NOT_FOUND); - } - $collection = $dbForProject->getDocument('database_' . $database->getInternalId(), $collectionId); - - if ($collection->isEmpty()) { - throw new Exception(Exception::COLLECTION_NOT_FOUND); - } - - $index = $dbForProject->findOne('indexes', [ - Query::equal('$id', [$database->getInternalId() . '_' . $collection->getInternalId() . '_' . $key]) - ]); - - if ($index->isEmpty()) { + if (empty($index)) { throw new Exception(Exception::INDEX_NOT_FOUND); } - $index->setAttribute('collectionId', $database->getInternalId() . '_' . $collectionId); - - $response->dynamic($index, Response::MODEL_INDEX); + $response->dynamic($index[0], Response::MODEL_INDEX); }); ; From 5be6d8e90bd3fe275c31750169ab6471f94cfbfd Mon Sep 17 00:00:00 2001 From: Steven Nguyen Date: Thu, 20 Jul 2023 15:54:38 -0700 Subject: [PATCH 18/20] Update Project providers to also include the provider key Pass the provider key and name back so that a nicely formatted name can be shown in the Appwrite Console. --- app/config/providers.php | 4 ++-- src/Appwrite/Utopia/Response/Model/Project.php | 5 +++-- src/Appwrite/Utopia/Response/Model/Provider.php | 6 ++++++ tests/e2e/Services/Projects/ProjectsConsoleClientTest.php | 4 ++-- 4 files changed, 13 insertions(+), 6 deletions(-) diff --git a/app/config/providers.php b/app/config/providers.php index 5e4dc0aa11..dedb4ec665 100644 --- a/app/config/providers.php +++ b/app/config/providers.php @@ -232,7 +232,7 @@ return [ // Ordered by ABC. 'mock' => false ], 'paypalSandbox' => [ - 'name' => 'PayPal', + 'name' => 'PayPal Sandbox', 'developers' => 'https://developer.paypal.com/docs/api/overview/', 'icon' => 'icon-paypal', 'enabled' => true, @@ -302,7 +302,7 @@ return [ // Ordered by ABC. 'mock' => false, ], 'tradeshiftBox' => [ - 'name' => 'Tradeshift', + 'name' => 'Tradeshift Sandbox', 'developers' => 'https://developers.tradeshift.com/docs/api', 'icon' => 'icon-tradeshiftbox', 'enabled' => true, diff --git a/src/Appwrite/Utopia/Response/Model/Project.php b/src/Appwrite/Utopia/Response/Model/Project.php index 6fbc794698..ed41c250fc 100644 --- a/src/Appwrite/Utopia/Response/Model/Project.php +++ b/src/Appwrite/Utopia/Response/Model/Project.php @@ -136,7 +136,7 @@ class Project extends Model 'type' => Response::MODEL_PROVIDER, 'description' => 'List of Providers.', 'default' => [], - 'example' => new \stdClass(), + 'example' => [new \stdClass()], 'array' => true, ]) ->addRule('platforms', [ @@ -273,7 +273,8 @@ class Project extends Model } $projectProviders[] = new Document([ - 'name' => ucfirst($key), + 'key' => $key, + 'name' => $provider['name'] ?? '', 'appId' => $providerValues[$key . 'Appid'] ?? '', 'secret' => $providerValues[$key . 'Secret'] ?? '', 'enabled' => $providerValues[$key . 'Enabled'] ?? false, diff --git a/src/Appwrite/Utopia/Response/Model/Provider.php b/src/Appwrite/Utopia/Response/Model/Provider.php index 0f14993508..c589011a46 100644 --- a/src/Appwrite/Utopia/Response/Model/Provider.php +++ b/src/Appwrite/Utopia/Response/Model/Provider.php @@ -15,6 +15,12 @@ class Provider extends Model public function __construct() { $this + ->addRule('key', [ + 'type' => self::TYPE_STRING, + 'description' => 'Provider.', + 'default' => '', + 'example' => 'github', + ]) ->addRule('name', [ 'type' => self::TYPE_STRING, 'description' => 'Provider name.', diff --git a/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php b/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php index c09bd262e3..52991b4621 100644 --- a/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php +++ b/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php @@ -618,7 +618,7 @@ class ProjectsConsoleClientTest extends Scope foreach ($providers as $key => $provider) { $asserted = false; foreach ($response['body']['providers'] as $responseProvider) { - if ($responseProvider['name'] === ucfirst($key)) { + if ($responseProvider['key'] === $key) { $this->assertEquals('AppId-' . ucfirst($key), $responseProvider['appId']); $this->assertEquals('Secret-' . ucfirst($key), $responseProvider['secret']); $this->assertFalse($responseProvider['enabled']); @@ -660,7 +660,7 @@ class ProjectsConsoleClientTest extends Scope foreach ($providers as $key => $provider) { $asserted = false; foreach ($response['body']['providers'] as $responseProvider) { - if ($responseProvider['name'] === ucfirst($key)) { + if ($responseProvider['key'] === $key) { // On first provider, test enabled=false $this->assertEquals($i !== 0, $responseProvider['enabled']); $asserted = true; From 7d412b4741899addb72f73fd34098609f023853b Mon Sep 17 00:00:00 2001 From: Prateek Banga Date: Mon, 24 Jul 2023 12:02:28 +0530 Subject: [PATCH 19/20] adds get db call and get collection call --- app/controllers/api/databases.php | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/app/controllers/api/databases.php b/app/controllers/api/databases.php index 6bfc08731f..4f2f42c849 100644 --- a/app/controllers/api/databases.php +++ b/app/controllers/api/databases.php @@ -2524,7 +2524,18 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/indexes/:key') ->inject('dbForProject') ->action(function (string $databaseId, string $collectionId, string $key, Response $response, Database $dbForProject) { - $index = $dbForProject->find('indexes', [Query::equal('collectionId', [$collectionId]), Query::equal('key', [$key]), Query::equal('databaseId', [$databaseId]), Query::limit(1)]); + $database = Authorization::skip(fn() => $dbForProject->getDocument('databases', $databaseId)); + + if ($database->isEmpty()) { + throw new Exception(Exception::DATABASE_NOT_FOUND); + } + $collection = $dbForProject->getDocument('database_' . $database->getInternalId(), $collectionId); + + if ($collection->isEmpty()) { + throw new Exception(Exception::COLLECTION_NOT_FOUND); + } + + $index = $dbForProject->find('indexes', [Query::equal('$id', [$database->getInternalId() . '_' . $collection->getInternalId() . '_' . $key]), Query::limit(1)]); if (empty($index)) { throw new Exception(Exception::INDEX_NOT_FOUND); @@ -2532,7 +2543,7 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/indexes/:key') $response->dynamic($index[0], Response::MODEL_INDEX); }); -; + App::delete('/v1/databases/:databaseId/collections/:collectionId/indexes/:key') ->alias('/v1/database/collections/:collectionId/indexes/:key', ['databaseId' => 'default']) From f7c02d9308039aed8c540c31dc9e68fd4bbc0ae3 Mon Sep 17 00:00:00 2001 From: Prateek Banga Date: Mon, 24 Jul 2023 23:41:49 +0530 Subject: [PATCH 20/20] removes extra db call and uses document find method --- app/controllers/api/databases.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/app/controllers/api/databases.php b/app/controllers/api/databases.php index 4f2f42c849..c815954747 100644 --- a/app/controllers/api/databases.php +++ b/app/controllers/api/databases.php @@ -2535,13 +2535,12 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/indexes/:key') throw new Exception(Exception::COLLECTION_NOT_FOUND); } - $index = $dbForProject->find('indexes', [Query::equal('$id', [$database->getInternalId() . '_' . $collection->getInternalId() . '_' . $key]), Query::limit(1)]); - + $index = $collection->find('key', $key, 'indexes'); if (empty($index)) { throw new Exception(Exception::INDEX_NOT_FOUND); } - $response->dynamic($index[0], Response::MODEL_INDEX); + $response->dynamic($index, Response::MODEL_INDEX); });