diff --git a/app/config/collections.php b/app/config/collections.php index 3d9207f07..8817683db 100644 --- a/app/config/collections.php +++ b/app/config/collections.php @@ -2732,7 +2732,7 @@ $projectCollections = array_merge([ 'required' => false, 'default' => [], 'array' => false, - 'filters' => ['json'], + 'filters' => ['json', 'encrypt'], ], [ '$id' => ID::custom('resources'), diff --git a/app/controllers/api/migrations.php b/app/controllers/api/migrations.php index 0b7c1ba29..1592b9898 100644 --- a/app/controllers/api/migrations.php +++ b/app/controllers/api/migrations.php @@ -84,56 +84,6 @@ App::post('/v1/migrations/appwrite') ->dynamic($migration, Response::MODEL_MIGRATION); }); -App::post('/v1/migrations/firebase') - ->groups(['api', 'migrations']) - ->desc('Migrate Firebase Data (Service Account)') - ->label('scope', 'migrations.write') - ->label('event', 'migrations.create') - ->label('audits.event', 'migration.create') - ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) - ->label('sdk.namespace', 'migrations') - ->label('sdk.method', 'createFirebaseMigration') - ->label('sdk.description', '/docs/references/migrations/migration-firebase.md') - ->label('sdk.response.code', Response::STATUS_CODE_ACCEPTED) - ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) - ->label('sdk.response.model', Response::MODEL_MIGRATION) - ->param('resources', [], new ArrayList(new WhiteList(Firebase::getSupportedResources())), 'List of resources to migrate') - ->param('serviceAccount', '', new Text(65536), 'JSON of the Firebase service account credentials') - ->inject('response') - ->inject('dbForProject') - ->inject('project') - ->inject('user') - ->inject('events') - ->action(function (array $resources, string $serviceAccount, Response $response, Database $dbForProject, Document $project, Document $user, Event $events) { - $migration = $dbForProject->createDocument('migrations', new Document([ - '$id' => ID::unique(), - 'status' => 'pending', - 'stage' => 'init', - 'source' => Firebase::getName(), - 'credentials' => [ - 'serviceAccount' => $serviceAccount, - ], - 'resources' => $resources, - 'statusCounters' => '{}', - 'resourceData' => '{}', - 'errors' => [], - ])); - - $events->setParam('migrationId', $migration->getId()); - - // Trigger Transfer - $event = new Migration(); - $event - ->setMigration($migration) - ->setProject($project) - ->setUser($user) - ->trigger(); - - $response - ->setStatusCode(Response::STATUS_CODE_ACCEPTED) - ->dynamic($migration, Response::MODEL_MIGRATION); - }); - App::post('/v1/migrations/firebase/oauth') ->groups(['api', 'migrations']) ->desc('Migrate Firebase Data (OAuth)') @@ -168,8 +118,7 @@ App::post('/v1/migrations/firebase/oauth') 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); + throw new Exception(Exception::USER_IDENTITY_NOT_FOUND); } $accessToken = $identity->getAttribute('providerAccessToken'); @@ -213,7 +162,7 @@ App::post('/v1/migrations/firebase/oauth') 'stage' => 'init', 'source' => Firebase::getName(), 'credentials' => [ - 'serviceAccount' => $serviceAccount, + 'serviceAccount' => json_encode($serviceAccount), ], 'resources' => $resources, 'statusCounters' => '{}', @@ -224,7 +173,56 @@ App::post('/v1/migrations/firebase/oauth') $events->setParam('migrationId', $migration->getId()); // Trigger Transfer - var_dump($project); + $event = new Migration(); + $event + ->setMigration($migration) + ->setProject($project) + ->setUser($user) + ->trigger(); + + $response + ->setStatusCode(Response::STATUS_CODE_ACCEPTED) + ->dynamic($migration, Response::MODEL_MIGRATION); + }); + +App::post('/v1/migrations/firebase') + ->groups(['api', 'migrations']) + ->desc('Migrate Firebase Data (Service Account)') + ->label('scope', 'migrations.write') + ->label('event', 'migrations.create') + ->label('audits.event', 'migration.create') + ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) + ->label('sdk.namespace', 'migrations') + ->label('sdk.method', 'createFirebaseMigration') + ->label('sdk.description', '/docs/references/migrations/migration-firebase.md') + ->label('sdk.response.code', Response::STATUS_CODE_ACCEPTED) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_MIGRATION) + ->param('resources', [], new ArrayList(new WhiteList(Firebase::getSupportedResources())), 'List of resources to migrate') + ->param('serviceAccount', '', new Text(65536), 'JSON of the Firebase service account credentials') + ->inject('response') + ->inject('dbForProject') + ->inject('project') + ->inject('user') + ->inject('events') + ->action(function (array $resources, string $serviceAccount, Response $response, Database $dbForProject, Document $project, Document $user, Event $events) { + $migration = $dbForProject->createDocument('migrations', new Document([ + '$id' => ID::unique(), + 'status' => 'pending', + 'stage' => 'init', + 'source' => Firebase::getName(), + 'credentials' => [ + 'serviceAccount' => $serviceAccount, + ], + 'resources' => $resources, + 'statusCounters' => '{}', + 'resourceData' => '{}', + 'errors' => [], + ])); + + $events->setParam('migrationId', $migration->getId()); + + // Trigger Transfer $event = new Migration(); $event ->setMigration($migration) @@ -515,8 +513,7 @@ App::get('/v1/migrations/firebase/report/oauth') 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); + throw new Exception(Exception::USER_IDENTITY_NOT_FOUND); } $accessToken = $identity->getAttribute('providerAccessToken'); @@ -749,7 +746,7 @@ App::get('/v1/migrations/firebase/projects') 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 + throw new Exception(Exception::USER_IDENTITY_NOT_FOUND); } $accessToken = $identity->getAttribute('providerAccessToken'); @@ -766,7 +763,11 @@ App::get('/v1/migrations/firebase/projects') $isExpired = new \DateTime($accessTokenExpiry) < new \DateTime('now'); if ($isExpired) { - $firebase->refreshTokens($refreshToken); + try { + $firebase->refreshTokens($refreshToken); + } catch (\Exception $e) { + throw new Exception(Exception::GENERAL_ACCESS_FORBIDDEN, 'Failed to refresh Firebase access token'); + } $accessToken = $firebase->getAccessToken(''); $refreshToken = $firebase->getRefreshToken(''); diff --git a/app/workers/migrations.php b/app/workers/migrations.php index c8d8004ec..bd63a81e8 100644 --- a/app/workers/migrations.php +++ b/app/workers/migrations.php @@ -5,7 +5,6 @@ use Appwrite\Messaging\Adapter\Realtime; use Appwrite\Permission; use Appwrite\Resque\Worker; use Appwrite\Role; -use Appwrite\Utopia\Response\Model\Migration; use Utopia\CLI\Console; use Utopia\Database\Database; use Utopia\Database\Document; @@ -61,7 +60,7 @@ class MigrationsV1 extends Worker return; } - $this->dbForProject = $this->getProjectDB($this->args['project']['$id']); + $this->dbForProject = $this->getProjectDB(new Document($this->args['project'])); $this->processMigration(); } diff --git a/docker-compose.yml b/docker-compose.yml index 19f01b1b5..f62ea572a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -618,6 +618,11 @@ services: - _APP_LOGGING_CONFIG - _APP_MIGRATIONS_FIREBASE_CLIENT_ID - _APP_MIGRATIONS_FIREBASE_CLIENT_SECRET + - _APP_CONNECTIONS_DB_PROJECT + - _APP_CONNECTIONS_DB_CONSOLE + - _APP_CONNECTIONS_CACHE + - _APP_CONNECTIONS_QUEUE + - _APP_CONNECTIONS_PUBSUB appwrite-maintenance: entrypoint: maintenance diff --git a/src/Appwrite/Auth/OAuth2/Firebase.php b/src/Appwrite/Auth/OAuth2/Firebase.php index 95acf0e0b..2f02d7cb1 100644 --- a/src/Appwrite/Auth/OAuth2/Firebase.php +++ b/src/Appwrite/Auth/OAuth2/Firebase.php @@ -87,7 +87,7 @@ class Firebase extends OAuth2 { $response = $this->request( 'POST', - 'https://github.com/login/oauth/access_token', + 'https://oauth2.googleapis.com/token', [], \http_build_query([ 'client_id' => $this->appID, @@ -195,12 +195,47 @@ class Firebase extends OAuth2 return $projects['results']; } - public function createServiceAccount(string $accessToken, string $projectID): array + /* + Be careful with the setIAMPolicy method, it will overwrite all existing policies + **/ + public function assignIAMRoles(string $accessToken, string $email, string $projectId) { + // Get IAM Roles + $iamRoles = $this->request('POST', 'https://cloudresourcemanager.googleapis.com/v1/projects/'.$projectId.':getIamPolicy', [ + 'Authorization: Bearer ' . \urlencode($accessToken), + 'Content-Type: application/json' + ]); + + $iamRoles = \json_decode($iamRoles, true); + + $iamRoles['bindings'][] = [ + 'role' => 'roles/identitytoolkit.admin', + 'members' => [ + 'serviceAccount:'.$email + ] + ]; + + $iamRoles['bindings'][] = [ + 'role' => 'roles/firebase.admin', + 'members' => [ + 'serviceAccount:'.$email + ] + ]; + + // Set IAM Roles + $this->request('POST', 'https://cloudresourcemanager.googleapis.com/v1/projects/'.$projectId.':setIamPolicy', [ + 'Authorization: Bearer ' . \urlencode($accessToken), + 'Content-Type: application/json' + ], json_encode([ + 'policy' => $iamRoles + ])); + } + + public function createServiceAccount(string $accessToken, string $projectId): array { // Create Service Account $response = $this->request( 'POST', - 'https://iam.googleapis.com/v1/projects/' . $projectID . '/serviceAccounts', + 'https://iam.googleapis.com/v1/projects/' . $projectId . '/serviceAccounts', [ 'Authorization: Bearer ' . \urlencode($accessToken), 'Content-Type: application/json' @@ -215,10 +250,12 @@ class Firebase extends OAuth2 $response = json_decode($response, true); + $this->assignIAMRoles($accessToken, $response['email'], $projectId); + // Create Service Account Key $responseKey = $this->request( 'POST', - 'https://iam.googleapis.com/v1/projects/' . $projectID . '/serviceAccounts/' . $response['email'] . '/keys', + 'https://iam.googleapis.com/v1/projects/' . $projectId . '/serviceAccounts/' . $response['email'] . '/keys', [ 'Authorization: Bearer ' . \urlencode($accessToken), 'Content-Type: application/json'