diff --git a/app/controllers/api/migrations.php b/app/controllers/api/migrations.php index 8d961a0450..a74b460e91 100644 --- a/app/controllers/api/migrations.php +++ b/app/controllers/api/migrations.php @@ -34,7 +34,7 @@ App::post('/v1/migrations/appwrite') ->groups(['api', 'migrations']) ->desc('Migrate Appwrite Data') ->label('scope', 'migrations.write') - ->label('event', 'migrations.create') + ->label('event', 'migrations.[migrationId].create') ->label('audits.event', 'migration.create') ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) ->label('sdk.namespace', 'migrations') @@ -88,7 +88,7 @@ App::post('/v1/migrations/firebase/oauth') ->groups(['api', 'migrations']) ->desc('Migrate Firebase Data (OAuth)') ->label('scope', 'migrations.write') - ->label('event', 'migrations.create') + ->label('event', 'migrations.[migrationId].create') ->label('audits.event', 'migration.create') ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) ->label('sdk.namespace', 'migrations') @@ -146,12 +146,13 @@ App::post('/v1/migrations/firebase/oauth') $dbForConsole->updateDocument('identities', $identity->getId(), $identity); } - if ($identity->getAttribute('secret')) { - $serviceAccount = $identity->getAttribute('secret'); + if ($identity->getAttribute('secrets')) { + $serviceAccount = $identity->getAttribute('secrets'); } else { + $firebase->cleanupServiceAccounts($accessToken, $projectId); $serviceAccount = $firebase->createServiceAccount($accessToken, $projectId); $identity = $identity - ->setAttribute('secret', $serviceAccount); + ->setAttribute('secrets', json_encode($serviceAccount)); $dbForConsole->updateDocument('identities', $identity->getId(), $identity); } @@ -189,7 +190,7 @@ App::post('/v1/migrations/firebase') ->groups(['api', 'migrations']) ->desc('Migrate Firebase Data (Service Account)') ->label('scope', 'migrations.write') - ->label('event', 'migrations.create') + ->label('event', 'migrations.[migrationId].create') ->label('audits.event', 'migration.create') ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) ->label('sdk.namespace', 'migrations') @@ -239,7 +240,7 @@ App::post('/v1/migrations/supabase') ->groups(['api', 'migrations']) ->desc('Migrate Supabase Data') ->label('scope', 'migrations.write') - ->label('event', 'migrations.create') + ->label('event', 'migrations.[migrationId].create') ->label('audits.event', 'migration.create') ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) ->label('sdk.namespace', 'migrations') @@ -299,7 +300,7 @@ App::post('/v1/migrations/nhost') ->groups(['api', 'migrations']) ->desc('Migrate NHost Data') ->label('scope', 'migrations.write') - ->label('event', 'migrations.create') + ->label('event', 'migrations.[migrationId].create') ->label('audits.event', 'migration.create') ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) ->label('sdk.namespace', 'migrations') @@ -309,7 +310,7 @@ App::post('/v1/migrations/nhost') ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->label('sdk.response.model', Response::MODEL_MIGRATION) ->param('resources', [], new ArrayList(new WhiteList(NHost::getSupportedResources())), 'List of resources to migrate') - ->param('subdomain', '', new URL(), 'Source\'s Subdomain') + ->param('subdomain', '', new Text(512), 'Source\'s Subdomain') ->param('region', '', new Text(512), 'Source\'s Region') ->param('adminSecret', '', new Text(512), 'Source\'s Admin Secret') ->param('database', '', new Text(512), 'Source\'s Database Name') @@ -452,8 +453,8 @@ App::get('/v1/migrations/appwrite/report') $response ->setStatusCode(Response::STATUS_CODE_OK) ->dynamic(new Document($appwrite->report($resources)), Response::MODEL_MIGRATION_REPORT); - } catch (\Exception $e) { - throw new Exception(Exception::GENERAL_SERVER_ERROR, $e->getMessage()); + } catch (\Throwable $e) { + throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Source Error: ' . $e->getMessage()); } }); @@ -479,7 +480,7 @@ App::get('/v1/migrations/firebase/report') ->setStatusCode(Response::STATUS_CODE_OK) ->dynamic(new Document($firebase->report($resources)), Response::MODEL_MIGRATION_REPORT); } catch (\Exception $e) { - throw new Exception(Exception::GENERAL_SERVER_ERROR, $e->getMessage()); + throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Source Error: ' . $e->getMessage()); } }); @@ -501,67 +502,77 @@ App::get('/v1/migrations/firebase/report/oauth') ->inject('user') ->inject('dbForConsole') ->action(function (array $resources, string $projectId, Response $response, Request $request, Document $user, Database $dbForConsole) { - try { - $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' - ); + $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' + ); - $identity = $dbForConsole->findOne('identities', [ - Query::equal('provider', ['firebase']), - Query::equal('userInternalId', [$user->getInternalId()]), - ]); - if ($identity === false || $identity->isEmpty()) { - throw new Exception(Exception::USER_IDENTITY_NOT_FOUND); - } + $identity = $dbForConsole->findOne('identities', [ + Query::equal('provider', ['firebase']), + Query::equal('userInternalId', [$user->getInternalId()]), + ]); - $accessToken = $identity->getAttribute('providerAccessToken'); - $refreshToken = $identity->getAttribute('providerRefreshToken'); - $accessTokenExpiry = $identity->getAttribute('providerAccessTokenExpiry'); - - $isExpired = new \DateTime($accessTokenExpiry) < new \DateTime('now'); - if ($isExpired) { - $firebase->refreshTokens($refreshToken); - - $accessToken = $firebase->getAccessToken(''); - $refreshToken = $firebase->getRefreshToken(''); - - $verificationId = $firebase->getUserID($accessToken); - - if (empty($verificationId)) { - throw new Exception(Exception::GENERAL_RATE_LIMIT_EXCEEDED, 'Another request is currently refreshing OAuth token. Please try again.'); - } - - $identity = $identity - ->setAttribute('providerAccessToken', $accessToken) - ->setAttribute('providerRefreshToken', $refreshToken) - ->setAttribute('providerAccessTokenExpiry', DateTime::addSeconds(new \DateTime(), (int)$firebase->getAccessTokenExpiry(''))); - - $dbForConsole->updateDocument('identities', $identity->getId(), $identity); - } - - // Get Service Account - if ($identity->getAttribute('secret')) { - $serviceAccount = $identity->getAttribute('secret'); - } else { - $serviceAccount = $firebase->createServiceAccount($accessToken, $projectId); - $identity = $identity - ->setAttribute('secret', $serviceAccount); - - $dbForConsole->updateDocument('identities', $identity->getId(), $identity); - } - - $firebase = new Firebase(array_merge($serviceAccount, ['project_id' => $projectId])); - - $report = $firebase->report($resources); - - $response - ->setStatusCode(Response::STATUS_CODE_OK) - ->dynamic(new Document($report), Response::MODEL_MIGRATION_REPORT); - } catch (\Exception $e) { - throw new Exception(Exception::GENERAL_SERVER_ERROR, $e->getMessage()); + if ($identity === false || $identity->isEmpty()) { + throw new Exception(Exception::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::USER_IDENTITY_NOT_FOUND); + } + + if (App::getEnv('_APP_MIGRATIONS_FIREBASE_CLIENT_ID', '') === '' || App::getEnv('_APP_MIGRATIONS_FIREBASE_CLIENT_SECRET', '') === '') { + throw new Exception(Exception::USER_IDENTITY_NOT_FOUND); + } + + $isExpired = new \DateTime($accessTokenExpiry) < new \DateTime('now'); + if ($isExpired) { + $firebase->refreshTokens($refreshToken); + + $accessToken = $firebase->getAccessToken(''); + $refreshToken = $firebase->getRefreshToken(''); + + $verificationId = $firebase->getUserID($accessToken); + + if (empty($verificationId)) { + throw new Exception(Exception::GENERAL_RATE_LIMIT_EXCEEDED, 'Another request is currently refreshing OAuth token. Please try again.'); + } + + $identity = $identity + ->setAttribute('providerAccessToken', $accessToken) + ->setAttribute('providerRefreshToken', $refreshToken) + ->setAttribute('providerAccessTokenExpiry', DateTime::addSeconds(new \DateTime(), (int)$firebase->getAccessTokenExpiry(''))); + + $dbForConsole->updateDocument('identities', $identity->getId(), $identity); + } + + // Get Service Account + if ($identity->getAttribute('secrets')) { + $serviceAccount = $identity->getAttribute('secrets'); + } else { + $firebase->cleanupServiceAccounts($accessToken, $projectId); + $serviceAccount = $firebase->createServiceAccount($accessToken, $projectId); + $identity = $identity + ->setAttribute('secrets', json_encode($serviceAccount)); + + $dbForConsole->updateDocument('identities', $identity->getId(), $identity); + } + + $firebase = new Firebase($serviceAccount); + + try { + $report = $firebase->report($resources); + } catch (\Exception $e) { + throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Source Error: ' . $e->getMessage()); + } + + $response + ->setStatusCode(Response::STATUS_CODE_OK) + ->dynamic(new Document($report), Response::MODEL_MIGRATION_REPORT); }); App::get('/v1/migrations/firebase/connect') @@ -745,6 +756,7 @@ App::get('/v1/migrations/firebase/projects') Query::equal('provider', ['firebase']), Query::equal('userInternalId', [$user->getInternalId()]), ]); + if ($identity === false || $identity->isEmpty()) { throw new Exception(Exception::USER_IDENTITY_NOT_FOUND); } @@ -754,46 +766,50 @@ App::get('/v1/migrations/firebase/projects') $accessTokenExpiry = $identity->getAttribute('providerAccessTokenExpiry'); if (empty($accessToken) || empty($refreshToken) || empty($accessTokenExpiry)) { - throw new Exception(Exception::GENERAL_ACCESS_FORBIDDEN, 'Not authenticated with Firebase'); + throw new Exception(Exception::USER_IDENTITY_NOT_FOUND); } 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'); + throw new Exception(Exception::USER_IDENTITY_NOT_FOUND); } - $isExpired = new \DateTime($accessTokenExpiry) < new \DateTime('now'); - if ($isExpired) { - try { - $firebase->refreshTokens($refreshToken); - } catch (\Exception $e) { - throw new Exception(Exception::GENERAL_ACCESS_FORBIDDEN, 'Failed to refresh Firebase access token'); + try { + $isExpired = new \DateTime($accessTokenExpiry) < new \DateTime('now'); + if ($isExpired) { + try { + $firebase->refreshTokens($refreshToken); + } catch (\Exception $e) { + throw new Exception(Exception::USER_IDENTITY_NOT_FOUND); + } + + $accessToken = $firebase->getAccessToken(''); + $refreshToken = $firebase->getRefreshToken(''); + + $verificationId = $firebase->getUserID($accessToken); + + if (empty($verificationId)) { + throw new Exception(Exception::GENERAL_RATE_LIMIT_EXCEEDED, 'Another request is currently refreshing OAuth token. Please try again.'); + } + + $identity = $identity + ->setAttribute('providerAccessToken', $accessToken) + ->setAttribute('providerRefreshToken', $refreshToken) + ->setAttribute('providerAccessTokenExpiry', DateTime::addSeconds(new \DateTime(), (int)$firebase->getAccessTokenExpiry(''))); + + $dbForConsole->updateDocument('identities', $identity->getId(), $identity); } - $accessToken = $firebase->getAccessToken(''); - $refreshToken = $firebase->getRefreshToken(''); + $projects = $firebase->getProjects($accessToken); - $verificationId = $firebase->getUserID($accessToken); - - if (empty($verificationId)) { - throw new Exception(Exception::GENERAL_RATE_LIMIT_EXCEEDED, 'Another request is currently refreshing OAuth token. Please try again.'); + $output = []; + foreach ($projects as $project) { + $output[] = [ + 'displayName' => $project['displayName'], + 'projectId' => $project['projectId'], + ]; } - - $identity = $identity - ->setAttribute('providerAccessToken', $accessToken) - ->setAttribute('providerRefreshToken', $refreshToken) - ->setAttribute('providerAccessTokenExpiry', DateTime::addSeconds(new \DateTime(), (int)$firebase->getAccessTokenExpiry(''))); - - $dbForConsole->updateDocument('identities', $identity->getId(), $identity); - } - - $projects = $firebase->getProjects($accessToken); - - $output = []; - foreach ($projects as $project) { - $output[] = [ - 'displayName' => $project['displayName'], - 'projectId' => $project['projectId'], - ]; + } catch (\Exception $e) { + throw new Exception(Exception::USER_IDENTITY_NOT_FOUND); } $response->dynamic(new Document([ @@ -857,7 +873,7 @@ App::get('/v1/migrations/supabase/report') ->setStatusCode(Response::STATUS_CODE_OK) ->dynamic(new Document($supabase->report($resources)), Response::MODEL_MIGRATION_REPORT); } catch (\Exception $e) { - throw new Exception(Exception::GENERAL_SERVER_ERROR, $e->getMessage()); + throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Source Error: ' . $e->getMessage()); } }); @@ -889,7 +905,7 @@ App::get('/v1/migrations/nhost/report') ->setStatusCode(Response::STATUS_CODE_OK) ->dynamic(new Document($nhost->report($resources)), Response::MODEL_MIGRATION_REPORT); } catch (\Exception $e) { - throw new Exception(Exception::GENERAL_SERVER_ERROR, $e->getMessage()); + throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Source Error: ' . $e->getMessage()); } }); @@ -955,9 +971,8 @@ App::delete('/v1/migrations/:migrationId') ->param('migrationId', '', new UID(), 'Migration ID.') ->inject('response') ->inject('dbForProject') - ->inject('deletes') ->inject('events') - ->action(function (string $migrationId, Response $response, Database $dbForProject, Delete $deletes, Event $events) { + ->action(function (string $migrationId, Response $response, Database $dbForProject, Event $events) { $migration = $dbForProject->getDocument('migrations', $migrationId); if ($migration->isEmpty()) { @@ -965,7 +980,7 @@ App::delete('/v1/migrations/:migrationId') } if (!$dbForProject->deleteDocument('migrations', $migration->getId())) { - throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to remove migration from DB', 500); + throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to remove migration from DB'); } $events->setParam('migrationId', $migration->getId()); diff --git a/app/controllers/general.php b/app/controllers/general.php index 7b3353446e..530f76584b 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -191,7 +191,7 @@ App::init() $host = $request->getHostname() ?? ''; $mainDomain = App::getEnv('_APP_DOMAIN', ''); // Only run Router when external domain - if ($host !== $mainDomain && $host !== 'localhost' && $host !== 'appwrite') { + if ($host !== $mainDomain && $host !== 'localhost' && $host !== APP_HOSTNAME_INTERNAL) { if (router($utopia, $dbForConsole, $swooleRequest, $request, $response)) { return; } @@ -489,7 +489,7 @@ App::options() $host = $request->getHostname() ?? ''; $mainDomain = App::getEnv('_APP_DOMAIN', ''); // Only run Router when external domain - if ($host !== $mainDomain && $host !== 'localhost' && $host !== 'appwrite') { + if ($host !== $mainDomain && $host !== 'localhost' && $host !== APP_HOSTNAME_INTERNAL) { if (router($utopia, $dbForConsole, $swooleRequest, $request, $response)) { return; } diff --git a/app/controllers/web/console.php b/app/controllers/web/console.php index 849ea85882..d7496b03bd 100644 --- a/app/controllers/web/console.php +++ b/app/controllers/web/console.php @@ -34,7 +34,7 @@ App::get('/console/*') // Serve static files (console) only for main domain $host = $request->getHostname() ?? ''; $mainDomain = App::getEnv('_APP_DOMAIN', ''); - if ($host !== $mainDomain && $host !== 'localhost' && $host !== 'appwrite') { + if ($host !== $mainDomain && $host !== 'localhost' && $host !== APP_HOSTNAME_INTERNAL) { throw new Exception(Exception::GENERAL_ROUTE_NOT_FOUND); } diff --git a/app/init.php b/app/init.php index 646a9d1cce..0ba8a08d2a 100644 --- a/app/init.php +++ b/app/init.php @@ -135,6 +135,7 @@ const APP_SOCIAL_DISCORD_CHANNEL = '564160730845151244'; const APP_SOCIAL_DEV = 'https://dev.to/appwrite'; const APP_SOCIAL_STACKSHARE = 'https://stackshare.io/appwrite'; const APP_SOCIAL_YOUTUBE = 'https://www.youtube.com/c/appwrite?sub_confirmation=1'; +const APP_HOSTNAME_INTERNAL = 'appwrite'; // Database Reconnect const DATABASE_RECONNECT_SLEEP = 2; const DATABASE_RECONNECT_MAX_ATTEMPTS = 10; diff --git a/app/workers/builds.php b/app/workers/builds.php index f9fc66f2f2..be303b449f 100644 --- a/app/workers/builds.php +++ b/app/workers/builds.php @@ -335,7 +335,7 @@ class BuildsV1 extends Worker Query::equal('resourceType', ['project']), Query::limit(APP_LIMIT_SUBQUERY) ]); - + foreach ($varsFromProject as $var) { $vars[$var->getAttribute('key')] = $var->getAttribute('value') ?? ''; } diff --git a/composer.json b/composer.json index 2a915f2d80..c5d11d7f4a 100644 --- a/composer.json +++ b/composer.json @@ -76,7 +76,7 @@ "webonyx/graphql-php": "14.11.*", "slickdeals/statsd": "3.1.0", "league/csv": "9.7.1", - "utopia-php/migration": "^0.2.0" + "utopia-php/migration": "^0.3.0" }, "repositories": [ { diff --git a/composer.lock b/composer.lock index b154e1854f..99ff7293a0 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": "98cd1f694dc31ab2091b55110664a311", + "content-hash": "e4934eff80bec5e9fe402528df07d72d", "packages": [ { "name": "adhocore/jwt", @@ -1963,16 +1963,16 @@ }, { "name": "utopia-php/migration", - "version": "0.2.0", + "version": "0.3.1", "source": { "type": "git", "url": "https://github.com/utopia-php/migration.git", - "reference": "9dc59bbe0d126e20434580a5aa7cae5793bab024" + "reference": "af4233f4ff6a37982dad294033199ce29cafc00c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/migration/zipball/9dc59bbe0d126e20434580a5aa7cae5793bab024", - "reference": "9dc59bbe0d126e20434580a5aa7cae5793bab024", + "url": "https://api.github.com/repos/utopia-php/migration/zipball/af4233f4ff6a37982dad294033199ce29cafc00c", + "reference": "af4233f4ff6a37982dad294033199ce29cafc00c", "shasum": "" }, "require": { @@ -2015,9 +2015,9 @@ ], "support": { "issues": "https://github.com/utopia-php/migration/issues", - "source": "https://github.com/utopia-php/migration/tree/0.2.0" + "source": "https://github.com/utopia-php/migration/tree/0.3.1" }, - "time": "2023-08-09T16:28:43+00:00" + "time": "2023-08-17T14:18:09+00:00" }, { "name": "utopia-php/mongo", @@ -5426,5 +5426,5 @@ "platform-overrides": { "php": "8.0" }, - "plugin-api-version": "2.3.0" + "plugin-api-version": "2.1.0" } diff --git a/docker-compose.yml b/docker-compose.yml index 567fb9b96e..64ecc0ae99 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -620,11 +620,13 @@ services: - ./app:/usr/src/code/app - ./src:/usr/src/code/src - ./tests:/usr/src/code/tests - - ./vendor:/usr/src/code/tests depends_on: - mariadb environment: - _APP_ENV + - _APP_WORKER_PER_CORE + - _APP_CONNECTIONS_MAX + - _APP_POOL_CLIENTS - _APP_OPENSSL_KEY_V1 - _APP_DOMAIN - _APP_DOMAIN_TARGET diff --git a/src/Appwrite/Auth/OAuth2/Firebase.php b/src/Appwrite/Auth/OAuth2/Firebase.php index 0a813881be..9c64038089 100644 --- a/src/Appwrite/Auth/OAuth2/Firebase.php +++ b/src/Appwrite/Auth/OAuth2/Firebase.php @@ -27,6 +27,38 @@ class Firebase extends OAuth2 'https://www.googleapis.com/auth/userinfo.profile' ]; + /** + * @var array + */ + protected array $iamPermissions = [ + // Database + 'datastore.databases.get', + 'datastore.databases.list', + 'datastore.entities.get', + 'datastore.entities.list', + 'datastore.indexes.get', + 'datastore.indexes.list', + // Generic Firebase permissions + 'firebase.projects.get', + + // Auth + 'firebaseauth.configs.get', + 'firebaseauth.configs.getHashConfig', + 'firebaseauth.configs.getSecret', + 'firebaseauth.users.get', + 'identitytoolkit.tenants.get', + 'identitytoolkit.tenants.list', + + // IAM Assignment + 'iam.serviceAccounts.list', + + // Storage + 'storage.buckets.get', + 'storage.buckets.list', + 'storage.objects.get', + 'storage.objects.list' + ]; + /** * @return string */ @@ -198,7 +230,7 @@ class Firebase extends OAuth2 /* Be careful with the setIAMPolicy method, it will overwrite all existing policies **/ - public function assignIAMRoles(string $accessToken, string $email, string $projectId) + public function assignIAMRole(string $accessToken, string $email, string $projectId, array $role) { // Get IAM Roles $iamRoles = $this->request('POST', 'https://cloudresourcemanager.googleapis.com/v1/projects/' . $projectId . ':getIamPolicy', [ @@ -209,14 +241,7 @@ class Firebase extends OAuth2 $iamRoles = \json_decode($iamRoles, true); $iamRoles['bindings'][] = [ - 'role' => 'roles/identitytoolkit.admin', - 'members' => [ - 'serviceAccount:' . $email - ] - ]; - - $iamRoles['bindings'][] = [ - 'role' => 'roles/firebase.admin', + 'role' => $role['name'], 'members' => [ 'serviceAccount:' . $email ] @@ -226,14 +251,69 @@ class Firebase extends OAuth2 $this->request('POST', 'https://cloudresourcemanager.googleapis.com/v1/projects/' . $projectId . ':setIamPolicy', [ 'Authorization: Bearer ' . \urlencode($accessToken), 'Content-Type: application/json' - ], json_encode([ + ], \json_encode([ 'policy' => $iamRoles ])); } + private function generateRandomString($length = 10): string + { + $characters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; + $charactersLength = strlen($characters); + $randomString = ''; + for ($i = 0; $i < $length; $i++) { + $randomString .= $characters[random_int(0, $charactersLength - 1)]; + } + return $randomString; + } + + private function createCustomRole(string $accessToken, string $projectId): array + { + // Check if role already exists + try { + $role = $this->request('GET', 'https://iam.googleapis.com/v1/projects/' . $projectId . '/roles/appwriteMigrations', [ + 'Content-Type: application/json', + 'Authorization: Bearer ' . \urlencode($accessToken), + ]); + + $role = \json_decode($role, true); + + return $role; + } catch (\Exception $e) { + if ($e->getCode() !== 404) { + throw $e; + } + } + + // Create role if doesn't exist or isn't correct + $role = $this->request( + 'POST', + 'https://iam.googleapis.com/v1/projects/' . $projectId . '/roles/', + [ + 'Content-Type: application/json', + 'Authorization: Bearer ' . \urlencode($accessToken), + ], + \json_encode( + [ + 'roleId' => 'appwriteMigrations', + 'role' => [ + 'title' => 'Appwrite Migrations', + 'description' => 'A helper role for Appwrite Migrations', + 'includedPermissions' => $this->iamPermissions, + 'stage' => 'GA' + ] + ] + ) + ); + + return json_decode($role, true); + } + public function createServiceAccount(string $accessToken, string $projectId): array { // Create Service Account + $uid = $this->generateRandomString(); + $response = $this->request( 'POST', 'https://iam.googleapis.com/v1/projects/' . $projectId . '/serviceAccounts', @@ -241,17 +321,22 @@ class Firebase extends OAuth2 'Authorization: Bearer ' . \urlencode($accessToken), 'Content-Type: application/json' ], - json_encode([ - 'accountId' => 'appwrite-migrations', + \json_encode([ + 'accountId' => 'appwrite-' . $uid, 'serviceAccount' => [ - 'displayName' => 'Appwrite Migrations' + 'displayName' => 'Appwrite Migrations ' . $uid ] ]) ); $response = json_decode($response, true); - $this->assignIAMRoles($accessToken, $response['email'], $projectId); + // Create and assign IAM Roles + $role = $this->createCustomRole($accessToken, $projectId); + + \sleep(1); // Wait for IAM to propagate changes. + + $this->assignIAMRole($accessToken, $response['email'], $projectId, $role); // Create Service Account Key $responseKey = $this->request( @@ -267,4 +352,38 @@ class Firebase extends OAuth2 return json_decode(base64_decode($responseKey['privateKeyData']), true); } + + public function cleanupServiceAccounts(string $accessToken, string $projectId) + { + // List Service Accounts + $response = $this->request( + 'GET', + 'https://iam.googleapis.com/v1/projects/' . $projectId . '/serviceAccounts', + [ + 'Authorization: Bearer ' . \urlencode($accessToken), + 'Content-Type: application/json' + ] + ); + + $response = json_decode($response, true); + + if (empty($response['accounts'])) { + return false; + } + + foreach ($response['accounts'] as $account) { + if (strpos($account['email'], 'appwrite-') !== false) { + $this->request( + 'DELETE', + 'https://iam.googleapis.com/v1/projects/' . $projectId . '/serviceAccounts/' . $account['email'], + [ + 'Authorization: Bearer ' . \urlencode($accessToken), + 'Content-Type: application/json' + ] + ); + } + } + + return true; + } } diff --git a/src/Appwrite/Messaging/Adapter/Realtime.php b/src/Appwrite/Messaging/Adapter/Realtime.php index f33f130818..1925944664 100644 --- a/src/Appwrite/Messaging/Adapter/Realtime.php +++ b/src/Appwrite/Messaging/Adapter/Realtime.php @@ -330,6 +330,11 @@ class Realtime extends Adapter $roles = [Role::team($project->getAttribute('teamId'))->toString()]; } + break; + case 'migrations': + $channels[] = 'console'; + $projectId = 'console'; + $roles = [Role::team($project->getAttribute('teamId'))->toString()]; break; }