From 113506068148834f113ea8e0f953eed8ea21e98c Mon Sep 17 00:00:00 2001 From: Bradley Schofield Date: Wed, 9 Aug 2023 16:18:17 +0100 Subject: [PATCH] Start work on identities implementation --- app/config/collections.php | 33 ------- app/controllers/api/migrations.php | 148 +++++++++++++++++++++-------- app/views/general/error.phtml | 1 + composer.json | 6 +- 4 files changed, 108 insertions(+), 80 deletions(-) diff --git a/app/config/collections.php b/app/config/collections.php index 080919efe6..a3b1e4cdd2 100644 --- a/app/config/collections.php +++ b/app/config/collections.php @@ -243,39 +243,6 @@ $commonCollections = [ 'array' => false, 'filters' => ['datetime'], ], - [ - '$id' => ID::custom('migrationsFirebaseAccessToken'), - 'type' => Database::VAR_STRING, - 'format' => '', - 'size' => 16384, - 'signed' => true, - 'required' => false, - 'default' => null, - 'array' => false, - 'filters' => ['encrypt'], - ], - [ - '$id' => ID::custom('migrationsFirebaseAccessTokenExpiry'), - 'type' => Database::VAR_DATETIME, - 'format' => '', - 'size' => 0, - 'signed' => false, - 'required' => false, - 'default' => null, - 'array' => false, - 'filters' => ['datetime'], - ], - [ - '$id' => ID::custom('migrationsFirebaseRefreshToken'), - 'type' => Database::VAR_STRING, - 'format' => '', - 'size' => 16384, - 'signed' => true, - 'required' => false, - 'default' => null, - 'array' => false, - 'filters' => ['encrypt'], - ], [ '$id' => ID::custom('migrationsFirebaseServiceAccount'), 'type' => Database::VAR_STRING, diff --git a/app/controllers/api/migrations.php b/app/controllers/api/migrations.php index 4104aa1217..254c443b6e 100644 --- a/app/controllers/api/migrations.php +++ b/app/controllers/api/migrations.php @@ -5,6 +5,8 @@ use Appwrite\Event\Delete; use Appwrite\Event\Event; use Appwrite\Event\Migration; use Appwrite\Extend\Exception; +use Appwrite\Permission; +use Appwrite\Role; use Appwrite\Utopia\Database\Validator\Queries\Migrations; use Appwrite\Utopia\Request; use Appwrite\Utopia\Response; @@ -161,9 +163,18 @@ App::post('/v1/migrations/firebase/oauth') $request->getProtocol() . '://' . $request->getHostname() . '/v1/migrations/firebase/redirect' ); - $accessToken = $user->getAttribute('migrationsFirebaseAccessToken'); - $refreshToken = $user->getAttribute('migrationsFirebaseRefreshToken'); - $accessTokenExpiry = $user->getAttribute('migrationsFirebaseAccessTokenExpiry'); + $identity = $dbForConsole->findOne('identities', [ + Query::equal('provider', ['firebase']), + Query::equal('userInternalId', [$user->getInternalId()]), + ]); + if ($identity === false || $identity->isEmpty()) { + throw new Exception(Exception::GENERAL_SERVER_ERROR); //TODO: REMOVE + // throw new Exception(Exception::USER_IDENTITY_NOT_FOUND); + } + + $accessToken = $identity->getAttribute('providerAccessToken'); + $refreshToken = $identity->getAttribute('providerRefreshToken'); + $accessTokenExpiry = $identity->getAttribute('providerAccessTokenExpiry'); $isExpired = new \DateTime($accessTokenExpiry) < new \DateTime('now'); if ($isExpired) { @@ -178,12 +189,12 @@ App::post('/v1/migrations/firebase/oauth') throw new Exception(Exception::GENERAL_RATE_LIMIT_EXCEEDED, 'Another request is currently refreshing OAuth token. Please try again.'); } - $user = $user - ->setAttribute('migrationsFirebaseAccessToken', $accessToken) - ->setAttribute('migrationsFirebaseRefreshToken', $refreshToken) - ->setAttribute('migrationsFirebaseAccessTokenExpiry', DateTime::addSeconds(new \DateTime(), (int) $firebase->getAccessTokenExpiry(''))); + $identity = $identity + ->setAttribute('providerAccessToken', $accessToken) + ->setAttribute('providerRefreshToken', $refreshToken) + ->setAttribute('providerAccessTokenExpiry', DateTime::addSeconds(new \DateTime(), (int)$firebase->getAccessTokenExpiry(''))); - $dbForConsole->updateDocument('users', $user->getId(), $user); + $dbForConsole->updateDocument('identities', $identity->getId(), $identity); } if ($user->getAttribute('migrationsFirebaseServiceAccount')) { @@ -499,9 +510,18 @@ App::get('/v1/migrations/firebase/report/oauth') $request->getProtocol() . '://' . $request->getHostname() . '/v1/migrations/firebase/redirect' ); - $accessToken = $user->getAttribute('migrationsFirebaseAccessToken'); - $refreshToken = $user->getAttribute('migrationsFirebaseRefreshToken'); - $accessTokenExpiry = $user->getAttribute('migrationsFirebaseAccessTokenExpiry'); + $identity = $dbForConsole->findOne('identities', [ + Query::equal('provider', ['firebase']), + Query::equal('userInternalId', [$user->getInternalId()]), + ]); + if ($identity === false || $identity->isEmpty()) { + throw new Exception(Exception::GENERAL_SERVER_ERROR); //TODO: REMOVE + // throw new Exception(Exception::USER_IDENTITY_NOT_FOUND); + } + + $accessToken = $identity->getAttribute('providerAccessToken'); + $refreshToken = $identity->getAttribute('providerRefreshToken'); + $accessTokenExpiry = $identity->getAttribute('providerAccessTokenExpiry'); $isExpired = new \DateTime($accessTokenExpiry) < new \DateTime('now'); if ($isExpired) { @@ -516,12 +536,12 @@ App::get('/v1/migrations/firebase/report/oauth') throw new Exception(Exception::GENERAL_RATE_LIMIT_EXCEEDED, 'Another request is currently refreshing OAuth token. Please try again.'); } - $user = $user - ->setAttribute('migrationsFirebaseAccessToken', $accessToken) - ->setAttribute('migrationsFirebaseRefreshToken', $refreshToken) - ->setAttribute('migrationsFirebaseAccessTokenExpiry', DateTime::addSeconds(new \DateTime(), (int) $firebase->getAccessTokenExpiry(''))); + $identity = $identity + ->setAttribute('providerAccessToken', $accessToken) + ->setAttribute('providerRefreshToken', $refreshToken) + ->setAttribute('providerAccessTokenExpiry', DateTime::addSeconds(new \DateTime(), (int)$firebase->getAccessTokenExpiry(''))); - $dbForConsole->updateDocument('users', $user->getId(), $user); + $dbForConsole->updateDocument('identities', $identity->getId(), $identity); } // Get Service Account @@ -641,6 +661,8 @@ App::get('/v1/migrations/firebase/redirect') $accessToken = $oauth2->getAccessToken($code); $refreshToken = $oauth2->getRefreshToken($code); $accessTokenExpiry = $oauth2->getAccessTokenExpiry($code); + $email = $oauth2->getUserEmail($accessToken); + $oauth2ID = $oauth2->getUserID($accessToken); if (empty($accessToken)) { throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to get access token.'); @@ -654,12 +676,42 @@ App::get('/v1/migrations/firebase/redirect') throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to get access token expiry.'); } - $user = $user - ->setAttribute('migrationsFirebaseAccessToken', $accessToken) - ->setAttribute('migrationsFirebaseRefreshToken', $refreshToken) - ->setAttribute('migrationsFirebaseAccessTokenExpiry', DateTime::addSeconds(new \DateTime(), (int) $accessTokenExpiry)); + // Makes sure this email is not already used in another identity + $identity = $dbForConsole->findOne('identities', [ + Query::equal('providerEmail', [$email]), + ]); - $dbForConsole->updateDocument('users', $user->getId(), $user); + if ($identity !== false && !$identity->isEmpty()) { + if ($identity->getAttribute('userInternalId', '') !== $user->getInternalId()) { + throw new Exception(Exception::USER_EMAIL_ALREADY_EXISTS); + } + } + + if ($identity !== false && !$identity->isEmpty()) { + $identity = $identity + ->setAttribute('providerAccessToken', $accessToken) + ->setAttribute('providerRefreshToken', $refreshToken) + ->setAttribute('providerAccessTokenExpiry', DateTime::addSeconds(new \DateTime(), (int)$accessTokenExpiry)); + + $dbForConsole->updateDocument('identities', $identity->getId(), $identity); + } else { + $identity = $dbForConsole->createDocument('identities', new Document([ + '$id' => ID::unique(), + '$permissions' => [ + Permission::read(Role::any()), + Permission::update(Role::user($user->getId())), + Permission::delete(Role::user($user->getId())), + ], + 'userInternalId' => $user->getInternalId(), + 'userId' => $user->getId(), + 'provider' => 'firebase', + 'providerUid' => $oauth2ID, + 'providerEmail' => $email, + 'providerAccessToken' => $accessToken, + 'providerRefreshToken' => $refreshToken, + 'providerAccessTokenExpiry' => DateTime::addSeconds(new \DateTime(), (int)$accessTokenExpiry), + ])); + } } else { throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Missing OAuth2 code.'); } @@ -686,23 +738,31 @@ App::get('/v1/migrations/firebase/projects') ->inject('dbForConsole') ->inject('request') ->action(function (Document $user, Response $response, Document $project, Database $dbForConsole, Request $request) { - if (empty($user->getAttribute('migrationsFirebaseAccessToken')) || empty($user->getAttribute('migrationsFirebaseRefreshToken')) || empty($user->getAttribute('migrationsFirebaseAccessTokenExpiry'))) { - throw new Exception(Exception::GENERAL_ACCESS_FORBIDDEN, 'Not authenticated with Firebase'); - } - - if (App::getEnv('_APP_MIGRATIONS_FIREBASE_CLIENT_ID', '') === '' || App::getEnv('_APP_MIGRATIONS_FIREBASE_CLIENT_SECRET', '') === '') { - throw new Exception(Exception::GENERAL_ACCESS_FORBIDDEN, 'Missing Google OAuth credentials'); - } - $firebase = new OAuth2Firebase( App::getEnv('_APP_MIGRATIONS_FIREBASE_CLIENT_ID', ''), App::getEnv('_APP_MIGRATIONS_FIREBASE_CLIENT_SECRET', ''), $request->getProtocol() . '://' . $request->getHostname() . '/v1/migrations/firebase/redirect' ); - $accessToken = $user->getAttribute('migrationsFirebaseAccessToken'); - $refreshToken = $user->getAttribute('migrationsFirebaseRefreshToken'); - $accessTokenExpiry = $user->getAttribute('migrationsFirebaseAccessTokenExpiry'); + $identity = $dbForConsole->findOne('identities', [ + Query::equal('provider', ['firebase']), + Query::equal('userInternalId', [$user->getInternalId()]), + ]); + if ($identity === false || $identity->isEmpty()) { + throw new Exception(Exception::GENERAL_ACCESS_FORBIDDEN, 'Not authenticated with Firebase'); //TODO: Replace with USER_IDENTITY_NOT_FOUND + } + + $accessToken = $identity->getAttribute('providerAccessToken'); + $refreshToken = $identity->getAttribute('providerRefreshToken'); + $accessTokenExpiry = $identity->getAttribute('providerAccessTokenExpiry'); + + if (empty($accessToken) || empty($refreshToken) || empty($accessTokenExpiry)) { + throw new Exception(Exception::GENERAL_ACCESS_FORBIDDEN, 'Not authenticated with Firebase'); + } + + if (App::getEnv('_APP_MIGRATIONS_FIREBASE_CLIENT_ID', '') === '' || App::getEnv('_APP_MIGRATIONS_FIREBASE_CLIENT_SECRET', '') === '') { + throw new Exception(Exception::GENERAL_ACCESS_FORBIDDEN, 'Missing Google OAuth credentials'); + } $isExpired = new \DateTime($accessTokenExpiry) < new \DateTime('now'); if ($isExpired) { @@ -717,12 +777,12 @@ App::get('/v1/migrations/firebase/projects') throw new Exception(Exception::GENERAL_RATE_LIMIT_EXCEEDED, 'Another request is currently refreshing OAuth token. Please try again.'); } - $user = $user - ->setAttribute('migrationsFirebaseAccessToken', $accessToken) - ->setAttribute('migrationsFirebaseRefreshToken', $refreshToken) - ->setAttribute('migrationsFirebaseAccessTokenExpiry', DateTime::addSeconds(new \DateTime(), (int) $firebase->getAccessTokenExpiry(''))); + $identity = $identity + ->setAttribute('providerAccessToken', $accessToken) + ->setAttribute('providerRefreshToken', $refreshToken) + ->setAttribute('providerAccessTokenExpiry', DateTime::addSeconds(new \DateTime(), (int)$firebase->getAccessTokenExpiry(''))); - $dbForConsole->updateDocument('users', $user->getId(), $user); + $dbForConsole->updateDocument('identities', $identity->getId(), $identity); } $projects = $firebase->getProjects($accessToken); @@ -754,12 +814,16 @@ App::get('/v1/migrations/firebase/deauthorize') ->inject('response') ->inject('dbForConsole') ->action(function (Document $user, Response $response, Database $dbForConsole) { - $user = $user - ->setAttribute('migrationsFirebaseAccessToken', '') - ->setAttribute('migrationsFirebaseRefreshToken', '') - ->setAttribute('migrationsFirebaseAccessTokenExpiry', ''); + $identity = $dbForConsole->findOne('identities', [ + Query::equal('provider', ['firebase']), + Query::equal('userInternalId', [$user->getInternalId()]), + ]); - $dbForConsole->updateDocument('users', $user->getId(), $user); + if ($identity === false || $identity->isEmpty()) { + throw new Exception(Exception::GENERAL_ACCESS_FORBIDDEN, 'Not authenticated with Firebase'); //TODO: Replace with USER_IDENTITY_NOT_FOUND + } + + $dbForConsole->deleteDocument('identities', $identity->getId()); $response->noContent(); }); diff --git a/app/views/general/error.phtml b/app/views/general/error.phtml index a0d46a1c57..450dcf8973 100644 --- a/app/views/general/error.phtml +++ b/app/views/general/error.phtml @@ -1,5 +1,6 @@ getParam('development', false); +$type = $this->getParam('type', 'general_server_error'); $code = $this->getParam('code', 500); $errorID = $this->getParam('errorID', 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'); $message = $this->getParam('message', ''); diff --git a/composer.json b/composer.json index 9fb86a5860..018de5dea3 100644 --- a/composer.json +++ b/composer.json @@ -66,7 +66,7 @@ "utopia-php/storage": "0.14.*", "utopia-php/swoole": "0.8.*", "utopia-php/websocket": "0.1.*", - "utopia-php/transfer": "dev-feat-improve-features", + "utopia-php/transfer": "0.2.*", "resque/php-resque": "1.3.6", "matomo/device-detector": "6.1.*", "dragonmantank/cron-expression": "3.3.2", @@ -81,10 +81,6 @@ { "url": "https://github.com/appwrite/runtimes.git", "type": "git" - }, - { - "url": "https://github.com/utopia-php/transfer.git", - "type": "vcs" } ], "require-dev": {