From 45926dc407faede6b9d45afb9dfdf74c7edebfdb Mon Sep 17 00:00:00 2001 From: Prem Palanisamy Date: Tue, 26 May 2026 16:19:03 +0100 Subject: [PATCH 01/11] Add OAuth providers migration --- src/Migration/Destinations/Appwrite.php | 36 ++++++++ src/Migration/Resource.php | 3 + .../Resources/Auth/OAuthProviders.php | 82 +++++++++++++++++++ src/Migration/Sources/Appwrite.php | 44 ++++++++++ src/Migration/Transfer.php | 2 + 5 files changed, 167 insertions(+) create mode 100644 src/Migration/Resources/Auth/OAuthProviders.php diff --git a/src/Migration/Destinations/Appwrite.php b/src/Migration/Destinations/Appwrite.php index cd1d800d..a35c0735 100644 --- a/src/Migration/Destinations/Appwrite.php +++ b/src/Migration/Destinations/Appwrite.php @@ -46,6 +46,7 @@ use Utopia\Migration\Resources\Auth\AuthMethods; use Utopia\Migration\Resources\Auth\Hash; use Utopia\Migration\Resources\Auth\Membership; +use Utopia\Migration\Resources\Auth\OAuthProviders; use Utopia\Migration\Resources\Auth\Policies; use Utopia\Migration\Resources\Auth\Team; use Utopia\Migration\Resources\Auth\User; @@ -260,6 +261,7 @@ public static function getSupportedResources(): array Resource::TYPE_TEAM, Resource::TYPE_MEMBERSHIP, Resource::TYPE_AUTH_METHODS, + Resource::TYPE_OAUTH_PROVIDERS, Resource::TYPE_POLICIES, // Database @@ -2196,6 +2198,10 @@ public function importAuthResource(Resource $resource): Resource /** @var AuthMethods $resource */ $this->createAuthMethods($resource); break; + case Resource::TYPE_OAUTH_PROVIDERS: + /** @var OAuthProviders $resource */ + $this->createOAuthProviders($resource); + break; case Resource::TYPE_POLICIES: /** @var Policies $resource */ $this->createPolicies($resource); @@ -3581,6 +3587,36 @@ protected function createAuthMethods(AuthMethods $resource): bool return true; } + /** + * Read-then-merge the project's `oAuthProviders` map. Each provider expands + * into `{key}Enabled` and `{key}Appid` flat entries; the `{key}Secret` + * entry is left untouched so the destination user can re-enter it post-migration. + */ + protected function createOAuthProviders(OAuthProviders $resource): bool + { + $project = $this->dbForPlatform->getDocument('projects', $this->projectId); + $oAuthProviders = $project->getAttribute('oAuthProviders', []); + + foreach ($resource->getProviders() as $provider) { + $key = $provider['key']; + if ($key === '') { + continue; + } + $oAuthProviders[$key . 'Enabled'] = $provider['enabled']; + $oAuthProviders[$key . 'Appid'] = $provider['appId']; + } + + $this->dbForPlatform->getAuthorization()->skip(fn () => $this->dbForPlatform->updateDocument( + 'projects', + $this->projectId, + new UtopiaDocument(['oAuthProviders' => $oAuthProviders]), + )); + + $this->dbForPlatform->purgeCachedDocument('projects', $this->projectId); + + return true; + } + /** * Direct DB write — SDK policy setters reject `total: 0` but `0` is the * storage value for "disabled". Shares the `auths` map with diff --git a/src/Migration/Resource.php b/src/Migration/Resource.php index 38304ca3..8ff9a855 100644 --- a/src/Migration/Resource.php +++ b/src/Migration/Resource.php @@ -71,6 +71,8 @@ abstract class Resource implements \JsonSerializable public const TYPE_AUTH_METHODS = 'auth-methods'; + public const TYPE_OAUTH_PROVIDERS = 'oauth-providers'; + public const TYPE_POLICIES = 'policies'; public const TYPE_ENVIRONMENT_VARIABLE = 'environment-variable'; @@ -132,6 +134,7 @@ abstract class Resource implements \JsonSerializable self::TYPE_TEAM, self::TYPE_MEMBERSHIP, self::TYPE_AUTH_METHODS, + self::TYPE_OAUTH_PROVIDERS, self::TYPE_POLICIES, self::TYPE_PLATFORM, self::TYPE_API_KEY, diff --git a/src/Migration/Resources/Auth/OAuthProviders.php b/src/Migration/Resources/Auth/OAuthProviders.php new file mode 100644 index 00000000..e91055a7 --- /dev/null +++ b/src/Migration/Resources/Auth/OAuthProviders.php @@ -0,0 +1,82 @@ + $providers + */ + public function __construct( + string $id, + private readonly array $providers = [], + string $createdAt = '', + string $updatedAt = '', + ) { + $this->id = $id; + $this->createdAt = $createdAt; + $this->updatedAt = $updatedAt; + } + + /** + * @param array $array + */ + public static function fromArray(array $array): self + { + $providers = []; + foreach ($array['providers'] ?? [] as $provider) { + $providers[] = [ + 'key' => (string) ($provider['key'] ?? ''), + 'enabled' => (bool) ($provider['enabled'] ?? false), + 'appId' => (string) ($provider['appId'] ?? ''), + ]; + } + + return new self( + $array['id'], + $providers, + createdAt: $array['createdAt'] ?? '', + updatedAt: $array['updatedAt'] ?? '', + ); + } + + /** + * @return array + */ + public function jsonSerialize(): array + { + return [ + 'id' => $this->id, + 'providers' => $this->providers, + 'createdAt' => $this->createdAt, + 'updatedAt' => $this->updatedAt, + ]; + } + + public static function getName(): string + { + return Resource::TYPE_OAUTH_PROVIDERS; + } + + public function getGroup(): string + { + return Transfer::GROUP_AUTH; + } + + /** + * @return array + */ + public function getProviders(): array + { + return $this->providers; + } +} diff --git a/src/Migration/Sources/Appwrite.php b/src/Migration/Sources/Appwrite.php index cd7cd9ef..dbb1829c 100644 --- a/src/Migration/Sources/Appwrite.php +++ b/src/Migration/Sources/Appwrite.php @@ -27,6 +27,7 @@ use Utopia\Migration\Resources\Auth\AuthMethods; use Utopia\Migration\Resources\Auth\Hash; use Utopia\Migration\Resources\Auth\Membership; +use Utopia\Migration\Resources\Auth\OAuthProviders; use Utopia\Migration\Resources\Auth\Policies; use Utopia\Migration\Resources\Auth\Team; use Utopia\Migration\Resources\Auth\User; @@ -191,6 +192,7 @@ public static function getSupportedResources(): array Resource::TYPE_TEAM, Resource::TYPE_MEMBERSHIP, Resource::TYPE_AUTH_METHODS, + Resource::TYPE_OAUTH_PROVIDERS, Resource::TYPE_POLICIES, // Database @@ -395,6 +397,11 @@ private function reportAuth(array $resources, array &$report, array $resourceIds $report[Resource::TYPE_AUTH_METHODS] = 1; } + if (\in_array(Resource::TYPE_OAUTH_PROVIDERS, $resources)) { + // Singleton — one OAuth providers config map per project. + $report[Resource::TYPE_OAUTH_PROVIDERS] = 1; + } + if (\in_array(Resource::TYPE_POLICIES, $resources)) { // Singleton — one policies config per project. $report[Resource::TYPE_POLICIES] = 1; @@ -667,6 +674,20 @@ protected function exportGroupAuth(int $batchSize, array $resources): void previous: $e )); } + + try { + if (\in_array(Resource::TYPE_OAUTH_PROVIDERS, $resources)) { + $this->exportOAuthProviders(); + } + } catch (\Throwable $e) { + $this->addError(new Exception( + Resource::TYPE_OAUTH_PROVIDERS, + Transfer::GROUP_AUTH, + message: $e->getMessage(), + code: (int) $e->getCode() ?: Exception::CODE_INTERNAL, + previous: $e + )); + } } private function exportPolicies(): void @@ -726,6 +747,29 @@ private function exportAuthMethods(): void $this->callback([$authMethods]); } + private function exportOAuthProviders(): void + { + $project = $this->project->get(); + + $providers = []; + foreach ($project->oAuthProviders as $provider) { + $providers[] = [ + 'key' => (string) $provider->key, + 'enabled' => (bool) $provider->enabled, + 'appId' => (string) $provider->appId, + ]; + } + + $oAuthProviders = new OAuthProviders( + $this->projectId, + $providers, + createdAt: $project->createdAt, + updatedAt: $project->updatedAt, + ); + + $this->callback([$oAuthProviders]); + } + /** * @throws AppwriteException */ diff --git a/src/Migration/Transfer.php b/src/Migration/Transfer.php index 3d72a695..beb1734d 100644 --- a/src/Migration/Transfer.php +++ b/src/Migration/Transfer.php @@ -40,6 +40,7 @@ class Transfer Resource::TYPE_MEMBERSHIP, Resource::TYPE_HASH, Resource::TYPE_AUTH_METHODS, + Resource::TYPE_OAUTH_PROVIDERS, Resource::TYPE_POLICIES, ]; @@ -133,6 +134,7 @@ class Transfer Resource::TYPE_TEAM, Resource::TYPE_MEMBERSHIP, Resource::TYPE_AUTH_METHODS, + Resource::TYPE_OAUTH_PROVIDERS, Resource::TYPE_POLICIES, Resource::TYPE_FILE, Resource::TYPE_BUCKET, From f60dc3b886a3e655149f6ce0a83f5cfefe020eb0 Mon Sep 17 00:00:00 2001 From: Prem Palanisamy Date: Thu, 28 May 2026 17:41:22 +0100 Subject: [PATCH 02/11] Use listOAuth2Providers SDK call; migrate enabled flag only (SDK 24 moved oAuthProviders off Models\Project) --- src/Migration/Sources/Appwrite.php | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/Migration/Sources/Appwrite.php b/src/Migration/Sources/Appwrite.php index 4594f04f..9b7f144d 100644 --- a/src/Migration/Sources/Appwrite.php +++ b/src/Migration/Sources/Appwrite.php @@ -745,22 +745,26 @@ private function exportAuthMethods(): void private function exportOAuthProviders(): void { - $project = $this->project->get(); + // listOAuth2Providers returns a heterogeneous list — each entry is a typed + // OAuth2{Provider} payload with provider-specific field names (Google's + // `clientId` vs Apple's `serviceId`). We only migrate the `enabled` toggle; + // credential fields (clientId/secret/serviceId/keyId/...) are intentionally + // not migrated — destination user must re-register the OAuth app and + // re-enter credentials, same caveat as the SMTP password. + $response = $this->project->listOAuth2Providers(); $providers = []; - foreach ($project->oAuthProviders as $provider) { + foreach ($response->providers as $provider) { $providers[] = [ - 'key' => (string) $provider->key, - 'enabled' => (bool) $provider->enabled, - 'appId' => (string) $provider->appId, + 'key' => (string) ($provider['$id'] ?? ''), + 'enabled' => (bool) ($provider['enabled'] ?? false), + 'appId' => '', ]; } $oAuthProviders = new OAuthProviders( $this->projectId, $providers, - createdAt: $project->createdAt, - updatedAt: $project->updatedAt, ); $this->callback([$oAuthProviders]); From 72324bd3357acd8f47abf21e054750756fcead8a Mon Sep 17 00:00:00 2001 From: Prem Palanisamy Date: Thu, 28 May 2026 17:48:44 +0100 Subject: [PATCH 03/11] Address Greptile review: reorder OAuth export block; guard empty appId on destination --- src/Migration/Destinations/Appwrite.php | 10 +++++++--- src/Migration/Sources/Appwrite.php | 12 ++++++------ 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/src/Migration/Destinations/Appwrite.php b/src/Migration/Destinations/Appwrite.php index 9a0b5a58..be0725b3 100644 --- a/src/Migration/Destinations/Appwrite.php +++ b/src/Migration/Destinations/Appwrite.php @@ -3559,8 +3559,10 @@ protected function createAuthMethods(AuthMethods $resource): bool /** * Read-then-merge the project's `oAuthProviders` map. Each provider expands - * into `{key}Enabled` and `{key}Appid` flat entries; the `{key}Secret` - * entry is left untouched so the destination user can re-enter it post-migration. + * into `{key}Enabled` and (when carried) `{key}Appid` flat entries; the + * `{key}Secret` entry is left untouched so the destination user can re-enter + * it post-migration. Empty `appId` is skipped rather than overwritten so + * pre-existing destination credentials aren't clobbered. */ protected function createOAuthProviders(OAuthProviders $resource): bool { @@ -3573,7 +3575,9 @@ protected function createOAuthProviders(OAuthProviders $resource): bool continue; } $oAuthProviders[$key . 'Enabled'] = $provider['enabled']; - $oAuthProviders[$key . 'Appid'] = $provider['appId']; + if ($provider['appId'] !== '') { + $oAuthProviders[$key . 'Appid'] = $provider['appId']; + } } $this->dbForPlatform->getAuthorization()->skip(fn () => $this->dbForPlatform->updateDocument( diff --git a/src/Migration/Sources/Appwrite.php b/src/Migration/Sources/Appwrite.php index 9b7f144d..7ff8c643 100644 --- a/src/Migration/Sources/Appwrite.php +++ b/src/Migration/Sources/Appwrite.php @@ -658,12 +658,12 @@ protected function exportGroupAuth(int $batchSize, array $resources): void } try { - if (\in_array(Resource::TYPE_POLICIES, $resources)) { - $this->exportPolicies(); + if (\in_array(Resource::TYPE_OAUTH_PROVIDERS, $resources)) { + $this->exportOAuthProviders(); } } catch (\Throwable $e) { $this->addError(new Exception( - Resource::TYPE_POLICIES, + Resource::TYPE_OAUTH_PROVIDERS, Transfer::GROUP_AUTH, message: $e->getMessage(), code: (int) $e->getCode() ?: Exception::CODE_INTERNAL, @@ -672,12 +672,12 @@ protected function exportGroupAuth(int $batchSize, array $resources): void } try { - if (\in_array(Resource::TYPE_OAUTH_PROVIDERS, $resources)) { - $this->exportOAuthProviders(); + if (\in_array(Resource::TYPE_POLICIES, $resources)) { + $this->exportPolicies(); } } catch (\Throwable $e) { $this->addError(new Exception( - Resource::TYPE_OAUTH_PROVIDERS, + Resource::TYPE_POLICIES, Transfer::GROUP_AUTH, message: $e->getMessage(), code: (int) $e->getCode() ?: Exception::CODE_INTERNAL, From b0820ded331d4dc437f183f3ca088df9f2397a5e Mon Sep 17 00:00:00 2001 From: Prem Palanisamy Date: Thu, 28 May 2026 18:09:03 +0100 Subject: [PATCH 04/11] OAuth: skip enabling providers without destination credentials (avoid broken sign-in flow) --- src/Migration/Destinations/Appwrite.php | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/src/Migration/Destinations/Appwrite.php b/src/Migration/Destinations/Appwrite.php index be0725b3..30b4b352 100644 --- a/src/Migration/Destinations/Appwrite.php +++ b/src/Migration/Destinations/Appwrite.php @@ -3558,11 +3558,15 @@ protected function createAuthMethods(AuthMethods $resource): bool } /** - * Read-then-merge the project's `oAuthProviders` map. Each provider expands - * into `{key}Enabled` and (when carried) `{key}Appid` flat entries; the - * `{key}Secret` entry is left untouched so the destination user can re-enter - * it post-migration. Empty `appId` is skipped rather than overwritten so - * pre-existing destination credentials aren't clobbered. + * Read-then-merge the project's `oAuthProviders` map. Migration only flips + * `{key}Enabled` — credentials (`{key}Appid`, `{key}Secret`) are never + * migrated because the source API masks them on read. + * + * Enabling a provider that has no `{key}Appid` configured on the destination + * would redirect end users to the OAuth server with an empty client_id and + * break sign-in at runtime. To avoid that, `enabled = true` is only applied + * if the destination already has credentials registered for the provider. + * Disables are always applied — they can never produce a broken sign-in. */ protected function createOAuthProviders(OAuthProviders $resource): bool { @@ -3574,10 +3578,12 @@ protected function createOAuthProviders(OAuthProviders $resource): bool if ($key === '') { continue; } - $oAuthProviders[$key . 'Enabled'] = $provider['enabled']; - if ($provider['appId'] !== '') { - $oAuthProviders[$key . 'Appid'] = $provider['appId']; + if ($provider['enabled'] && empty($oAuthProviders[$key . 'Appid'])) { + // Destination has no credentials for this provider — skip the + // enable to keep sign-in functional. + continue; } + $oAuthProviders[$key . 'Enabled'] = $provider['enabled']; } $this->dbForPlatform->getAuthorization()->skip(fn () => $this->dbForPlatform->updateDocument( From 3269cf3e588d4847bcc358ea8ea0e3067afda36a Mon Sep 17 00:00:00 2001 From: Prem Palanisamy Date: Fri, 29 May 2026 08:19:55 +0100 Subject: [PATCH 05/11] OAuth2: switch to per-provider Resource classes; migrate all readable non-secret fields --- src/Migration/Destinations/Appwrite.php | 129 ++++++++++++--- src/Migration/Resource.php | 88 +++++++++- .../Resources/Auth/OAuth2/Amazon.php | 18 +++ src/Migration/Resources/Auth/OAuth2/Apple.php | 82 ++++++++++ src/Migration/Resources/Auth/OAuth2/Auth0.php | 18 +++ .../Resources/Auth/OAuth2/Authentik.php | 18 +++ .../Resources/Auth/OAuth2/Autodesk.php | 18 +++ .../Resources/Auth/OAuth2/Bitbucket.php | 18 +++ src/Migration/Resources/Auth/OAuth2/Bitly.php | 18 +++ src/Migration/Resources/Auth/OAuth2/Box.php | 18 +++ .../Resources/Auth/OAuth2/Dailymotion.php | 18 +++ .../Resources/Auth/OAuth2/Discord.php | 18 +++ .../Resources/Auth/OAuth2/Disqus.php | 18 +++ .../Resources/Auth/OAuth2/Dropbox.php | 18 +++ src/Migration/Resources/Auth/OAuth2/Etsy.php | 18 +++ .../Resources/Auth/OAuth2/Facebook.php | 18 +++ src/Migration/Resources/Auth/OAuth2/Figma.php | 18 +++ .../Resources/Auth/OAuth2/FusionAuth.php | 18 +++ .../Resources/Auth/OAuth2/Github.php | 18 +++ .../Resources/Auth/OAuth2/Gitlab.php | 18 +++ .../Resources/Auth/OAuth2/Google.php | 74 +++++++++ .../Resources/Auth/OAuth2/Keycloak.php | 18 +++ src/Migration/Resources/Auth/OAuth2/Kick.php | 18 +++ .../Resources/Auth/OAuth2/Linkedin.php | 18 +++ .../Resources/Auth/OAuth2/Microsoft.php | 68 ++++++++ .../Resources/Auth/OAuth2/Notion.php | 18 +++ .../Resources/Auth/OAuth2/OAuth2Provider.php | 55 +++++++ src/Migration/Resources/Auth/OAuth2/Oidc.php | 18 +++ src/Migration/Resources/Auth/OAuth2/Okta.php | 18 +++ .../Resources/Auth/OAuth2/Paypal.php | 18 +++ src/Migration/Resources/Auth/OAuth2/Podio.php | 18 +++ .../Resources/Auth/OAuth2/Salesforce.php | 18 +++ src/Migration/Resources/Auth/OAuth2/Slack.php | 18 +++ .../Resources/Auth/OAuth2/Spotify.php | 18 +++ .../Auth/OAuth2/StandardProvider.php | 56 +++++++ .../Resources/Auth/OAuth2/Stripe.php | 18 +++ .../Resources/Auth/OAuth2/Tradeshift.php | 18 +++ .../Resources/Auth/OAuth2/Twitch.php | 18 +++ .../Auth/OAuth2/WithEndpointProvider.php | 60 +++++++ .../Resources/Auth/OAuth2/Wordpress.php | 18 +++ src/Migration/Resources/Auth/OAuth2/X.php | 18 +++ src/Migration/Resources/Auth/OAuth2/Yahoo.php | 18 +++ .../Resources/Auth/OAuth2/Yandex.php | 18 +++ src/Migration/Resources/Auth/OAuth2/Zoho.php | 18 +++ src/Migration/Resources/Auth/OAuth2/Zoom.php | 18 +++ .../Resources/Auth/OAuthProviders.php | 82 ---------- src/Migration/Sources/Appwrite.php | 151 ++++++++++++++---- src/Migration/Transfer.php | 47 +++++- 48 files changed, 1412 insertions(+), 146 deletions(-) create mode 100644 src/Migration/Resources/Auth/OAuth2/Amazon.php create mode 100644 src/Migration/Resources/Auth/OAuth2/Apple.php create mode 100644 src/Migration/Resources/Auth/OAuth2/Auth0.php create mode 100644 src/Migration/Resources/Auth/OAuth2/Authentik.php create mode 100644 src/Migration/Resources/Auth/OAuth2/Autodesk.php create mode 100644 src/Migration/Resources/Auth/OAuth2/Bitbucket.php create mode 100644 src/Migration/Resources/Auth/OAuth2/Bitly.php create mode 100644 src/Migration/Resources/Auth/OAuth2/Box.php create mode 100644 src/Migration/Resources/Auth/OAuth2/Dailymotion.php create mode 100644 src/Migration/Resources/Auth/OAuth2/Discord.php create mode 100644 src/Migration/Resources/Auth/OAuth2/Disqus.php create mode 100644 src/Migration/Resources/Auth/OAuth2/Dropbox.php create mode 100644 src/Migration/Resources/Auth/OAuth2/Etsy.php create mode 100644 src/Migration/Resources/Auth/OAuth2/Facebook.php create mode 100644 src/Migration/Resources/Auth/OAuth2/Figma.php create mode 100644 src/Migration/Resources/Auth/OAuth2/FusionAuth.php create mode 100644 src/Migration/Resources/Auth/OAuth2/Github.php create mode 100644 src/Migration/Resources/Auth/OAuth2/Gitlab.php create mode 100644 src/Migration/Resources/Auth/OAuth2/Google.php create mode 100644 src/Migration/Resources/Auth/OAuth2/Keycloak.php create mode 100644 src/Migration/Resources/Auth/OAuth2/Kick.php create mode 100644 src/Migration/Resources/Auth/OAuth2/Linkedin.php create mode 100644 src/Migration/Resources/Auth/OAuth2/Microsoft.php create mode 100644 src/Migration/Resources/Auth/OAuth2/Notion.php create mode 100644 src/Migration/Resources/Auth/OAuth2/OAuth2Provider.php create mode 100644 src/Migration/Resources/Auth/OAuth2/Oidc.php create mode 100644 src/Migration/Resources/Auth/OAuth2/Okta.php create mode 100644 src/Migration/Resources/Auth/OAuth2/Paypal.php create mode 100644 src/Migration/Resources/Auth/OAuth2/Podio.php create mode 100644 src/Migration/Resources/Auth/OAuth2/Salesforce.php create mode 100644 src/Migration/Resources/Auth/OAuth2/Slack.php create mode 100644 src/Migration/Resources/Auth/OAuth2/Spotify.php create mode 100644 src/Migration/Resources/Auth/OAuth2/StandardProvider.php create mode 100644 src/Migration/Resources/Auth/OAuth2/Stripe.php create mode 100644 src/Migration/Resources/Auth/OAuth2/Tradeshift.php create mode 100644 src/Migration/Resources/Auth/OAuth2/Twitch.php create mode 100644 src/Migration/Resources/Auth/OAuth2/WithEndpointProvider.php create mode 100644 src/Migration/Resources/Auth/OAuth2/Wordpress.php create mode 100644 src/Migration/Resources/Auth/OAuth2/X.php create mode 100644 src/Migration/Resources/Auth/OAuth2/Yahoo.php create mode 100644 src/Migration/Resources/Auth/OAuth2/Yandex.php create mode 100644 src/Migration/Resources/Auth/OAuth2/Zoho.php create mode 100644 src/Migration/Resources/Auth/OAuth2/Zoom.php delete mode 100644 src/Migration/Resources/Auth/OAuthProviders.php diff --git a/src/Migration/Destinations/Appwrite.php b/src/Migration/Destinations/Appwrite.php index 30b4b352..16caf4cf 100644 --- a/src/Migration/Destinations/Appwrite.php +++ b/src/Migration/Destinations/Appwrite.php @@ -45,7 +45,12 @@ use Utopia\Migration\Resources\Auth\AuthMethods; use Utopia\Migration\Resources\Auth\Hash; use Utopia\Migration\Resources\Auth\Membership; -use Utopia\Migration\Resources\Auth\OAuthProviders; +use Utopia\Migration\Resources\Auth\OAuth2\Apple as OAuth2Apple; +use Utopia\Migration\Resources\Auth\OAuth2\Google as OAuth2Google; +use Utopia\Migration\Resources\Auth\OAuth2\Microsoft as OAuth2Microsoft; +use Utopia\Migration\Resources\Auth\OAuth2\OAuth2Provider; +use Utopia\Migration\Resources\Auth\OAuth2\StandardProvider as OAuth2Standard; +use Utopia\Migration\Resources\Auth\OAuth2\WithEndpointProvider as OAuth2WithEndpoint; use Utopia\Migration\Resources\Auth\Policies; use Utopia\Migration\Resources\Auth\Team; use Utopia\Migration\Resources\Auth\User; @@ -260,8 +265,8 @@ public static function getSupportedResources(): array Resource::TYPE_TEAM, Resource::TYPE_MEMBERSHIP, Resource::TYPE_AUTH_METHODS, - Resource::TYPE_OAUTH_PROVIDERS, Resource::TYPE_POLICIES, + ...Transfer::GROUP_AUTH_OAUTH2_RESOURCES, // Database Resource::TYPE_DATABASE, @@ -2194,14 +2199,15 @@ public function importAuthResource(Resource $resource): Resource /** @var AuthMethods $resource */ $this->createAuthMethods($resource); break; - case Resource::TYPE_OAUTH_PROVIDERS: - /** @var OAuthProviders $resource */ - $this->createOAuthProviders($resource); - break; case Resource::TYPE_POLICIES: /** @var Policies $resource */ $this->createPolicies($resource); break; + default: + if ($resource instanceof OAuth2Provider) { + $this->createOAuth2Provider($resource); + } + break; } $resource->setStatus(Resource::STATUS_SUCCESS); @@ -3558,32 +3564,70 @@ protected function createAuthMethods(AuthMethods $resource): bool } /** - * Read-then-merge the project's `oAuthProviders` map. Migration only flips - * `{key}Enabled` — credentials (`{key}Appid`, `{key}Secret`) are never - * migrated because the source API masks them on read. + * Read-then-merge a single OAuth2 provider's entries on the project's + * `oAuthProviders` map. The same storage shape covers every provider — + * `{providerKey}Appid` for the readable client identifier, `{providerKey}Secret` + * for the credential blob (write-only, never overwritten), and + * `{providerKey}Enabled` for the toggle. Per-provider extras (Apple's + * keyId/teamId merged into the secret JSON, Microsoft's tenant, OIDC's + * endpoint, Google's prompt) are handled in the per-shape branches. * - * Enabling a provider that has no `{key}Appid` configured on the destination - * would redirect end users to the OAuth server with an empty client_id and - * break sign-in at runtime. To avoid that, `enabled = true` is only applied - * if the destination already has credentials registered for the provider. - * Disables are always applied — they can never produce a broken sign-in. + * Safety guard: `enabled = true` is only applied if the destination already + * has a `{providerKey}Secret` set. Otherwise sign-in would redirect users to + * the OAuth server with no credentials and fail at runtime. Disables are + * always applied — they can never produce a broken sign-in. */ - protected function createOAuthProviders(OAuthProviders $resource): bool + protected function createOAuth2Provider(OAuth2Provider $resource): bool { + $key = $resource::getProviderKey(); $project = $this->dbForPlatform->getDocument('projects', $this->projectId); $oAuthProviders = $project->getAttribute('oAuthProviders', []); - foreach ($resource->getProviders() as $provider) { - $key = $provider['key']; - if ($key === '') { - continue; + // Common: write the readable client identifier (clientId / serviceId). + if ($resource instanceof OAuth2Apple) { + if ($resource->getServiceId() !== '') { + $oAuthProviders[$key . 'Appid'] = $resource->getServiceId(); + } + $oAuthProviders[$key . 'Secret'] = $this->mergeAppleSecret( + $oAuthProviders[$key . 'Secret'] ?? '', + $resource->getKeyId(), + $resource->getTeamId(), + ); + } elseif ($resource instanceof OAuth2Standard) { + if ($resource->getClientId() !== '') { + $oAuthProviders[$key . 'Appid'] = $resource->getClientId(); + } + if ($resource instanceof OAuth2WithEndpoint && $resource->getEndpoint() !== '') { + // Endpoint providers (Auth0/Authentik/FusionAuth/Gitlab/Keycloak/Okta/OIDC) + // bundle the endpoint URL inside the JSON secret blob alongside + // the client secret on the destination. + $oAuthProviders[$key . 'Secret'] = $this->mergeJsonSecret( + $oAuthProviders[$key . 'Secret'] ?? '', + ['endpoint' => $resource->getEndpoint()], + ); } - if ($provider['enabled'] && empty($oAuthProviders[$key . 'Appid'])) { - // Destination has no credentials for this provider — skip the - // enable to keep sign-in functional. - continue; + if ($resource instanceof OAuth2Microsoft && $resource->getTenant() !== '') { + $oAuthProviders[$key . 'Secret'] = $this->mergeJsonSecret( + $oAuthProviders[$key . 'Secret'] ?? '', + ['tenant' => $resource->getTenant()], + ); + } + if ($resource instanceof OAuth2Google && !empty($resource->getPrompt())) { + $oAuthProviders[$key . 'Secret'] = $this->mergeJsonSecret( + $oAuthProviders[$key . 'Secret'] ?? '', + ['prompt' => $resource->getPrompt()], + ); + } + } + + if ($resource->getEnabled()) { + // Don't flip enabled = true unless the destination already has a + // secret configured — see method doc. + if (!empty($oAuthProviders[$key . 'Secret'])) { + $oAuthProviders[$key . 'Enabled'] = true; } - $oAuthProviders[$key . 'Enabled'] = $provider['enabled']; + } else { + $oAuthProviders[$key . 'Enabled'] = false; } $this->dbForPlatform->getAuthorization()->skip(fn () => $this->dbForPlatform->updateDocument( @@ -3597,6 +3641,43 @@ protected function createOAuthProviders(OAuthProviders $resource): bool return true; } + /** + * Apple stores its credential as a JSON blob of `{keyID, teamID, p8}`. + * Migration carries keyID/teamID (readable) but not p8 (write-only). + * Read the destination's existing blob, overlay the migrated fields, keep + * the destination's `p8` untouched. + */ + private function mergeAppleSecret(string $existing, string $keyId, string $teamId): string + { + $decoded = $existing === '' ? [] : (\json_decode($existing, true) ?: []); + if ($keyId !== '') { + $decoded['keyID'] = $keyId; + } + if ($teamId !== '') { + $decoded['teamID'] = $teamId; + } + + return \json_encode($decoded) ?: ''; + } + + /** + * Merge a partial fields map into a JSON-encoded secret blob (used for + * Microsoft tenant, OIDC/Auth0/etc. endpoint, Google prompt). Preserves + * any existing keys on the destination — only overrides the ones we + * carry from the source. + * + * @param array $fields + */ + private function mergeJsonSecret(string $existing, array $fields): string + { + $decoded = $existing === '' ? [] : (\json_decode($existing, true) ?: []); + foreach ($fields as $name => $value) { + $decoded[$name] = $value; + } + + return \json_encode($decoded) ?: ''; + } + /** * Direct DB write — SDK policy setters reject `total: 0` but `0` is the * storage value for "disabled". Shares the `auths` map with diff --git a/src/Migration/Resource.php b/src/Migration/Resource.php index 776c3a49..10643414 100644 --- a/src/Migration/Resource.php +++ b/src/Migration/Resource.php @@ -71,10 +71,53 @@ abstract class Resource implements \JsonSerializable public const TYPE_AUTH_METHODS = 'auth-methods'; - public const TYPE_OAUTH_PROVIDERS = 'oauth-providers'; - public const TYPE_POLICIES = 'policies'; + // OAuth2 providers — one type constant per provider. Each maps to its own + // typed Resource subclass under Resources/Auth/OAuth2/. Credential fields + // (clientSecret, p8File) are never migrated; only the readable + // non-secret configuration crosses over. + public const TYPE_OAUTH2_AMAZON = 'oauth2-amazon'; + public const TYPE_OAUTH2_APPLE = 'oauth2-apple'; + public const TYPE_OAUTH2_AUTH0 = 'oauth2-auth0'; + public const TYPE_OAUTH2_AUTHENTIK = 'oauth2-authentik'; + public const TYPE_OAUTH2_AUTODESK = 'oauth2-autodesk'; + public const TYPE_OAUTH2_BITBUCKET = 'oauth2-bitbucket'; + public const TYPE_OAUTH2_BITLY = 'oauth2-bitly'; + public const TYPE_OAUTH2_BOX = 'oauth2-box'; + public const TYPE_OAUTH2_DAILYMOTION = 'oauth2-dailymotion'; + public const TYPE_OAUTH2_DISCORD = 'oauth2-discord'; + public const TYPE_OAUTH2_DISQUS = 'oauth2-disqus'; + public const TYPE_OAUTH2_DROPBOX = 'oauth2-dropbox'; + public const TYPE_OAUTH2_ETSY = 'oauth2-etsy'; + public const TYPE_OAUTH2_FACEBOOK = 'oauth2-facebook'; + public const TYPE_OAUTH2_FIGMA = 'oauth2-figma'; + public const TYPE_OAUTH2_FUSIONAUTH = 'oauth2-fusionauth'; + public const TYPE_OAUTH2_GITHUB = 'oauth2-github'; + public const TYPE_OAUTH2_GITLAB = 'oauth2-gitlab'; + public const TYPE_OAUTH2_GOOGLE = 'oauth2-google'; + public const TYPE_OAUTH2_KEYCLOAK = 'oauth2-keycloak'; + public const TYPE_OAUTH2_KICK = 'oauth2-kick'; + public const TYPE_OAUTH2_LINKEDIN = 'oauth2-linkedin'; + public const TYPE_OAUTH2_MICROSOFT = 'oauth2-microsoft'; + public const TYPE_OAUTH2_NOTION = 'oauth2-notion'; + public const TYPE_OAUTH2_OIDC = 'oauth2-oidc'; + public const TYPE_OAUTH2_OKTA = 'oauth2-okta'; + public const TYPE_OAUTH2_PAYPAL = 'oauth2-paypal'; + public const TYPE_OAUTH2_PODIO = 'oauth2-podio'; + public const TYPE_OAUTH2_SALESFORCE = 'oauth2-salesforce'; + public const TYPE_OAUTH2_SLACK = 'oauth2-slack'; + public const TYPE_OAUTH2_SPOTIFY = 'oauth2-spotify'; + public const TYPE_OAUTH2_STRIPE = 'oauth2-stripe'; + public const TYPE_OAUTH2_TRADESHIFT = 'oauth2-tradeshift'; + public const TYPE_OAUTH2_TWITCH = 'oauth2-twitch'; + public const TYPE_OAUTH2_WORDPRESS = 'oauth2-wordpress'; + public const TYPE_OAUTH2_X = 'oauth2-x'; + public const TYPE_OAUTH2_YAHOO = 'oauth2-yahoo'; + public const TYPE_OAUTH2_YANDEX = 'oauth2-yandex'; + public const TYPE_OAUTH2_ZOHO = 'oauth2-zoho'; + public const TYPE_OAUTH2_ZOOM = 'oauth2-zoom'; + public const TYPE_ENVIRONMENT_VARIABLE = 'environment-variable'; // Integrations @@ -132,8 +175,47 @@ abstract class Resource implements \JsonSerializable self::TYPE_TEAM, self::TYPE_MEMBERSHIP, self::TYPE_AUTH_METHODS, - self::TYPE_OAUTH_PROVIDERS, self::TYPE_POLICIES, + self::TYPE_OAUTH2_AMAZON, + self::TYPE_OAUTH2_APPLE, + self::TYPE_OAUTH2_AUTH0, + self::TYPE_OAUTH2_AUTHENTIK, + self::TYPE_OAUTH2_AUTODESK, + self::TYPE_OAUTH2_BITBUCKET, + self::TYPE_OAUTH2_BITLY, + self::TYPE_OAUTH2_BOX, + self::TYPE_OAUTH2_DAILYMOTION, + self::TYPE_OAUTH2_DISCORD, + self::TYPE_OAUTH2_DISQUS, + self::TYPE_OAUTH2_DROPBOX, + self::TYPE_OAUTH2_ETSY, + self::TYPE_OAUTH2_FACEBOOK, + self::TYPE_OAUTH2_FIGMA, + self::TYPE_OAUTH2_FUSIONAUTH, + self::TYPE_OAUTH2_GITHUB, + self::TYPE_OAUTH2_GITLAB, + self::TYPE_OAUTH2_GOOGLE, + self::TYPE_OAUTH2_KEYCLOAK, + self::TYPE_OAUTH2_KICK, + self::TYPE_OAUTH2_LINKEDIN, + self::TYPE_OAUTH2_MICROSOFT, + self::TYPE_OAUTH2_NOTION, + self::TYPE_OAUTH2_OIDC, + self::TYPE_OAUTH2_OKTA, + self::TYPE_OAUTH2_PAYPAL, + self::TYPE_OAUTH2_PODIO, + self::TYPE_OAUTH2_SALESFORCE, + self::TYPE_OAUTH2_SLACK, + self::TYPE_OAUTH2_SPOTIFY, + self::TYPE_OAUTH2_STRIPE, + self::TYPE_OAUTH2_TRADESHIFT, + self::TYPE_OAUTH2_TWITCH, + self::TYPE_OAUTH2_WORDPRESS, + self::TYPE_OAUTH2_X, + self::TYPE_OAUTH2_YAHOO, + self::TYPE_OAUTH2_YANDEX, + self::TYPE_OAUTH2_ZOHO, + self::TYPE_OAUTH2_ZOOM, self::TYPE_PLATFORM, self::TYPE_API_KEY, self::TYPE_WEBHOOK, diff --git a/src/Migration/Resources/Auth/OAuth2/Amazon.php b/src/Migration/Resources/Auth/OAuth2/Amazon.php new file mode 100644 index 00000000..754a502b --- /dev/null +++ b/src/Migration/Resources/Auth/OAuth2/Amazon.php @@ -0,0 +1,18 @@ + $array + */ + public static function fromArray(array $array): self + { + return new self( + $array['id'], + (bool) ($array['enabled'] ?? false), + (string) ($array['serviceId'] ?? ''), + (string) ($array['keyId'] ?? ''), + (string) ($array['teamId'] ?? ''), + createdAt: $array['createdAt'] ?? '', + updatedAt: $array['updatedAt'] ?? '', + ); + } + + /** + * @return array + */ + public function jsonSerialize(): array + { + return [ + 'id' => $this->id, + 'enabled' => $this->enabled, + 'serviceId' => $this->serviceId, + 'keyId' => $this->keyId, + 'teamId' => $this->teamId, + 'createdAt' => $this->createdAt, + 'updatedAt' => $this->updatedAt, + ]; + } + + public static function getName(): string + { + return Resource::TYPE_OAUTH2_APPLE; + } + + public static function getProviderKey(): string + { + return 'apple'; + } + + public function getServiceId(): string + { + return $this->serviceId; + } + + public function getKeyId(): string + { + return $this->keyId; + } + + public function getTeamId(): string + { + return $this->teamId; + } +} diff --git a/src/Migration/Resources/Auth/OAuth2/Auth0.php b/src/Migration/Resources/Auth/OAuth2/Auth0.php new file mode 100644 index 00000000..97f4fe96 --- /dev/null +++ b/src/Migration/Resources/Auth/OAuth2/Auth0.php @@ -0,0 +1,18 @@ + $prompt + */ + public function __construct( + string $id, + bool $enabled, + string $clientId = '', + private readonly array $prompt = [], + string $createdAt = '', + string $updatedAt = '', + ) { + parent::__construct($id, $enabled, $clientId, $createdAt, $updatedAt); + } + + /** + * @param array $array + */ + public static function fromArray(array $array): self + { + return new self( + $array['id'], + (bool) ($array['enabled'] ?? false), + (string) ($array['clientId'] ?? ''), + (array) ($array['prompt'] ?? []), + createdAt: $array['createdAt'] ?? '', + updatedAt: $array['updatedAt'] ?? '', + ); + } + + /** + * @return array + */ + public function jsonSerialize(): array + { + return [ + 'id' => $this->id, + 'enabled' => $this->enabled, + 'clientId' => $this->clientId, + 'prompt' => $this->prompt, + 'createdAt' => $this->createdAt, + 'updatedAt' => $this->updatedAt, + ]; + } + + public static function getName(): string + { + return Resource::TYPE_OAUTH2_GOOGLE; + } + + public static function getProviderKey(): string + { + return 'google'; + } + + /** + * @return array + */ + public function getPrompt(): array + { + return $this->prompt; + } +} diff --git a/src/Migration/Resources/Auth/OAuth2/Keycloak.php b/src/Migration/Resources/Auth/OAuth2/Keycloak.php new file mode 100644 index 00000000..c949d882 --- /dev/null +++ b/src/Migration/Resources/Auth/OAuth2/Keycloak.php @@ -0,0 +1,18 @@ + $array + */ + public static function fromArray(array $array): self + { + return new self( + $array['id'], + (bool) ($array['enabled'] ?? false), + (string) ($array['clientId'] ?? ''), + (string) ($array['tenant'] ?? ''), + createdAt: $array['createdAt'] ?? '', + updatedAt: $array['updatedAt'] ?? '', + ); + } + + /** + * @return array + */ + public function jsonSerialize(): array + { + return [ + 'id' => $this->id, + 'enabled' => $this->enabled, + 'clientId' => $this->clientId, + 'tenant' => $this->tenant, + 'createdAt' => $this->createdAt, + 'updatedAt' => $this->updatedAt, + ]; + } + + public static function getName(): string + { + return Resource::TYPE_OAUTH2_MICROSOFT; + } + + public static function getProviderKey(): string + { + return 'microsoft'; + } + + public function getTenant(): string + { + return $this->tenant; + } +} diff --git a/src/Migration/Resources/Auth/OAuth2/Notion.php b/src/Migration/Resources/Auth/OAuth2/Notion.php new file mode 100644 index 00000000..8ec7cb0f --- /dev/null +++ b/src/Migration/Resources/Auth/OAuth2/Notion.php @@ -0,0 +1,18 @@ + $array + */ + abstract public static function fromArray(array $array): self; + + public function __construct( + string $id, + protected readonly bool $enabled = false, + string $createdAt = '', + string $updatedAt = '', + ) { + $this->id = $id; + $this->createdAt = $createdAt; + $this->updatedAt = $updatedAt; + } + + public function getGroup(): string + { + return Transfer::GROUP_AUTH; + } + + /** + * The OAuth2 provider key as stored on the project doc (e.g. 'google', + * 'apple', 'github'). Used by the destination to compute the + * `{providerKey}Enabled`/`{providerKey}Appid`/`{providerKey}Secret` + * storage attribute names. + */ + abstract public static function getProviderKey(): string; + + public function getEnabled(): bool + { + return $this->enabled; + } +} diff --git a/src/Migration/Resources/Auth/OAuth2/Oidc.php b/src/Migration/Resources/Auth/OAuth2/Oidc.php new file mode 100644 index 00000000..77136b8c --- /dev/null +++ b/src/Migration/Resources/Auth/OAuth2/Oidc.php @@ -0,0 +1,18 @@ + $array + */ + public static function fromArray(array $array): self + { + return new static( + $array['id'], + (bool) ($array['enabled'] ?? false), + (string) ($array['clientId'] ?? ''), + createdAt: $array['createdAt'] ?? '', + updatedAt: $array['updatedAt'] ?? '', + ); + } + + /** + * @return array + */ + public function jsonSerialize(): array + { + return [ + 'id' => $this->id, + 'enabled' => $this->enabled, + 'clientId' => $this->clientId, + 'createdAt' => $this->createdAt, + 'updatedAt' => $this->updatedAt, + ]; + } + + public function getClientId(): string + { + return $this->clientId; + } +} diff --git a/src/Migration/Resources/Auth/OAuth2/Stripe.php b/src/Migration/Resources/Auth/OAuth2/Stripe.php new file mode 100644 index 00000000..be058d77 --- /dev/null +++ b/src/Migration/Resources/Auth/OAuth2/Stripe.php @@ -0,0 +1,18 @@ + $array + */ + public static function fromArray(array $array): self + { + return new static( + $array['id'], + (bool) ($array['enabled'] ?? false), + (string) ($array['clientId'] ?? ''), + (string) ($array['endpoint'] ?? ''), + createdAt: $array['createdAt'] ?? '', + updatedAt: $array['updatedAt'] ?? '', + ); + } + + /** + * @return array + */ + public function jsonSerialize(): array + { + return [ + 'id' => $this->id, + 'enabled' => $this->enabled, + 'clientId' => $this->clientId, + 'endpoint' => $this->endpoint, + 'createdAt' => $this->createdAt, + 'updatedAt' => $this->updatedAt, + ]; + } + + public function getEndpoint(): string + { + return $this->endpoint; + } +} diff --git a/src/Migration/Resources/Auth/OAuth2/Wordpress.php b/src/Migration/Resources/Auth/OAuth2/Wordpress.php new file mode 100644 index 00000000..2f4cbd72 --- /dev/null +++ b/src/Migration/Resources/Auth/OAuth2/Wordpress.php @@ -0,0 +1,18 @@ + $providers - */ - public function __construct( - string $id, - private readonly array $providers = [], - string $createdAt = '', - string $updatedAt = '', - ) { - $this->id = $id; - $this->createdAt = $createdAt; - $this->updatedAt = $updatedAt; - } - - /** - * @param array $array - */ - public static function fromArray(array $array): self - { - $providers = []; - foreach ($array['providers'] ?? [] as $provider) { - $providers[] = [ - 'key' => (string) ($provider['key'] ?? ''), - 'enabled' => (bool) ($provider['enabled'] ?? false), - 'appId' => (string) ($provider['appId'] ?? ''), - ]; - } - - return new self( - $array['id'], - $providers, - createdAt: $array['createdAt'] ?? '', - updatedAt: $array['updatedAt'] ?? '', - ); - } - - /** - * @return array - */ - public function jsonSerialize(): array - { - return [ - 'id' => $this->id, - 'providers' => $this->providers, - 'createdAt' => $this->createdAt, - 'updatedAt' => $this->updatedAt, - ]; - } - - public static function getName(): string - { - return Resource::TYPE_OAUTH_PROVIDERS; - } - - public function getGroup(): string - { - return Transfer::GROUP_AUTH; - } - - /** - * @return array - */ - public function getProviders(): array - { - return $this->providers; - } -} diff --git a/src/Migration/Sources/Appwrite.php b/src/Migration/Sources/Appwrite.php index 7ff8c643..95e61111 100644 --- a/src/Migration/Sources/Appwrite.php +++ b/src/Migration/Sources/Appwrite.php @@ -26,7 +26,7 @@ use Utopia\Migration\Resources\Auth\AuthMethods; use Utopia\Migration\Resources\Auth\Hash; use Utopia\Migration\Resources\Auth\Membership; -use Utopia\Migration\Resources\Auth\OAuthProviders; +use Utopia\Migration\Resources\Auth\OAuth2\OAuth2Provider; use Utopia\Migration\Resources\Auth\Policies; use Utopia\Migration\Resources\Auth\Team; use Utopia\Migration\Resources\Auth\User; @@ -191,8 +191,8 @@ public static function getSupportedResources(): array Resource::TYPE_TEAM, Resource::TYPE_MEMBERSHIP, Resource::TYPE_AUTH_METHODS, - Resource::TYPE_OAUTH_PROVIDERS, Resource::TYPE_POLICIES, + ...Transfer::GROUP_AUTH_OAUTH2_RESOURCES, // Database Resource::TYPE_DATABASE, @@ -393,9 +393,13 @@ private function reportAuth(array $resources, array &$report, array $resourceIds $report[Resource::TYPE_AUTH_METHODS] = 1; } - if (\in_array(Resource::TYPE_OAUTH_PROVIDERS, $resources)) { - // Singleton — one OAuth providers config map per project. - $report[Resource::TYPE_OAUTH_PROVIDERS] = 1; + // OAuth2 providers — one resource per provider type. Each is a + // singleton config (one map entry per project) but emitted as its own + // typed Resource so per-provider status/failures are visible. + foreach (Transfer::GROUP_AUTH_OAUTH2_RESOURCES as $oauth2Type) { + if (\in_array($oauth2Type, $resources)) { + $report[$oauth2Type] = 1; + } } if (\in_array(Resource::TYPE_POLICIES, $resources)) { @@ -657,18 +661,26 @@ protected function exportGroupAuth(int $batchSize, array $resources): void )); } - try { - if (\in_array(Resource::TYPE_OAUTH_PROVIDERS, $resources)) { - $this->exportOAuthProviders(); + // A single `listOAuth2Providers` call returns the full per-provider + // payload; dispatch is by type. We catch and tag errors per-provider + // so one provider's failure doesn't suppress the others. + if (\count(\array_intersect(Transfer::GROUP_AUTH_OAUTH2_RESOURCES, $resources)) > 0) { + try { + $this->exportOAuth2Providers($resources); + } catch (\Throwable $e) { + foreach (Transfer::GROUP_AUTH_OAUTH2_RESOURCES as $oauth2Type) { + if (!\in_array($oauth2Type, $resources)) { + continue; + } + $this->addError(new Exception( + $oauth2Type, + Transfer::GROUP_AUTH, + message: $e->getMessage(), + code: (int) $e->getCode() ?: Exception::CODE_INTERNAL, + previous: $e + )); + } } - } catch (\Throwable $e) { - $this->addError(new Exception( - Resource::TYPE_OAUTH_PROVIDERS, - Transfer::GROUP_AUTH, - message: $e->getMessage(), - code: (int) $e->getCode() ?: Exception::CODE_INTERNAL, - previous: $e - )); } try { @@ -743,31 +755,100 @@ private function exportAuthMethods(): void $this->callback([$authMethods]); } - private function exportOAuthProviders(): void + /** + * Map of provider `$id` → fully-qualified migration Resource class. + * Each class extends `OAuth2Provider` and knows how to deserialize its + * provider's payload via `fromArray()`. + */ + private const OAUTH2_PROVIDER_CLASSES = [ + 'amazon' => \Utopia\Migration\Resources\Auth\OAuth2\Amazon::class, + 'apple' => \Utopia\Migration\Resources\Auth\OAuth2\Apple::class, + 'auth0' => \Utopia\Migration\Resources\Auth\OAuth2\Auth0::class, + 'authentik' => \Utopia\Migration\Resources\Auth\OAuth2\Authentik::class, + 'autodesk' => \Utopia\Migration\Resources\Auth\OAuth2\Autodesk::class, + 'bitbucket' => \Utopia\Migration\Resources\Auth\OAuth2\Bitbucket::class, + 'bitly' => \Utopia\Migration\Resources\Auth\OAuth2\Bitly::class, + 'box' => \Utopia\Migration\Resources\Auth\OAuth2\Box::class, + 'dailymotion' => \Utopia\Migration\Resources\Auth\OAuth2\Dailymotion::class, + 'discord' => \Utopia\Migration\Resources\Auth\OAuth2\Discord::class, + 'disqus' => \Utopia\Migration\Resources\Auth\OAuth2\Disqus::class, + 'dropbox' => \Utopia\Migration\Resources\Auth\OAuth2\Dropbox::class, + 'etsy' => \Utopia\Migration\Resources\Auth\OAuth2\Etsy::class, + 'facebook' => \Utopia\Migration\Resources\Auth\OAuth2\Facebook::class, + 'figma' => \Utopia\Migration\Resources\Auth\OAuth2\Figma::class, + 'fusionauth' => \Utopia\Migration\Resources\Auth\OAuth2\FusionAuth::class, + 'github' => \Utopia\Migration\Resources\Auth\OAuth2\Github::class, + 'gitlab' => \Utopia\Migration\Resources\Auth\OAuth2\Gitlab::class, + 'google' => \Utopia\Migration\Resources\Auth\OAuth2\Google::class, + 'keycloak' => \Utopia\Migration\Resources\Auth\OAuth2\Keycloak::class, + 'kick' => \Utopia\Migration\Resources\Auth\OAuth2\Kick::class, + 'linkedin' => \Utopia\Migration\Resources\Auth\OAuth2\Linkedin::class, + 'microsoft' => \Utopia\Migration\Resources\Auth\OAuth2\Microsoft::class, + 'notion' => \Utopia\Migration\Resources\Auth\OAuth2\Notion::class, + 'oidc' => \Utopia\Migration\Resources\Auth\OAuth2\Oidc::class, + 'okta' => \Utopia\Migration\Resources\Auth\OAuth2\Okta::class, + 'paypal' => \Utopia\Migration\Resources\Auth\OAuth2\Paypal::class, + 'podio' => \Utopia\Migration\Resources\Auth\OAuth2\Podio::class, + 'salesforce' => \Utopia\Migration\Resources\Auth\OAuth2\Salesforce::class, + 'slack' => \Utopia\Migration\Resources\Auth\OAuth2\Slack::class, + 'spotify' => \Utopia\Migration\Resources\Auth\OAuth2\Spotify::class, + 'stripe' => \Utopia\Migration\Resources\Auth\OAuth2\Stripe::class, + 'tradeshift' => \Utopia\Migration\Resources\Auth\OAuth2\Tradeshift::class, + 'twitch' => \Utopia\Migration\Resources\Auth\OAuth2\Twitch::class, + 'wordpress' => \Utopia\Migration\Resources\Auth\OAuth2\Wordpress::class, + 'x' => \Utopia\Migration\Resources\Auth\OAuth2\X::class, + 'yahoo' => \Utopia\Migration\Resources\Auth\OAuth2\Yahoo::class, + 'yandex' => \Utopia\Migration\Resources\Auth\OAuth2\Yandex::class, + 'zoho' => \Utopia\Migration\Resources\Auth\OAuth2\Zoho::class, + 'zoom' => \Utopia\Migration\Resources\Auth\OAuth2\Zoom::class, + ]; + + /** + * `listOAuth2Providers` returns a heterogeneous list — each entry is a + * typed `OAuth2{Provider}` payload with provider-specific fields. We + * route each one through its concrete migration Resource class + * (`Resources/Auth/OAuth2/{Provider}.php`), which extracts the readable + * non-secret fields. Credential fields (`clientSecret`, `p8File`) come + * back blanked from the server and are intentionally not migrated — + * destination admin must re-enter them per provider. + * + * @param array $resources Resources selected for migration. + */ + private function exportOAuth2Providers(array $resources): void { - // listOAuth2Providers returns a heterogeneous list — each entry is a typed - // OAuth2{Provider} payload with provider-specific field names (Google's - // `clientId` vs Apple's `serviceId`). We only migrate the `enabled` toggle; - // credential fields (clientId/secret/serviceId/keyId/...) are intentionally - // not migrated — destination user must re-register the OAuth app and - // re-enter credentials, same caveat as the SMTP password. $response = $this->project->listOAuth2Providers(); - $providers = []; + $emitted = []; foreach ($response->providers as $provider) { - $providers[] = [ - 'key' => (string) ($provider['$id'] ?? ''), - 'enabled' => (bool) ($provider['enabled'] ?? false), - 'appId' => '', - ]; - } + $key = (string) ($provider['$id'] ?? ''); + if ($key === '') { + continue; + } - $oAuthProviders = new OAuthProviders( - $this->projectId, - $providers, - ); + $class = self::OAUTH2_PROVIDER_CLASSES[$key] ?? null; + if ($class === null) { + // Server exposes a provider we don't have a Resource class for + // yet (e.g. a new provider added upstream after this lib was + // released). Skip silently — adding it is a one-file change. + continue; + } - $this->callback([$oAuthProviders]); + /** @var class-string $class */ + $type = $class::getName(); + if (!\in_array($type, $resources, true)) { + continue; + } + + // Hand the raw payload to the per-provider fromArray, which knows + // which fields it cares about. + $payload = $provider; + $payload['id'] = $this->projectId . '-' . $key; + $emitted[] = $class::fromArray($payload); + } + + if (!empty($emitted)) { + $this->callback($emitted); + } } /** diff --git a/src/Migration/Transfer.php b/src/Migration/Transfer.php index 4f9534b4..9596cb75 100644 --- a/src/Migration/Transfer.php +++ b/src/Migration/Transfer.php @@ -32,14 +32,57 @@ class Transfer public const GROUP_DOMAINS = 'domains'; + public const GROUP_AUTH_OAUTH2_RESOURCES = [ + Resource::TYPE_OAUTH2_AMAZON, + Resource::TYPE_OAUTH2_APPLE, + Resource::TYPE_OAUTH2_AUTH0, + Resource::TYPE_OAUTH2_AUTHENTIK, + Resource::TYPE_OAUTH2_AUTODESK, + Resource::TYPE_OAUTH2_BITBUCKET, + Resource::TYPE_OAUTH2_BITLY, + Resource::TYPE_OAUTH2_BOX, + Resource::TYPE_OAUTH2_DAILYMOTION, + Resource::TYPE_OAUTH2_DISCORD, + Resource::TYPE_OAUTH2_DISQUS, + Resource::TYPE_OAUTH2_DROPBOX, + Resource::TYPE_OAUTH2_ETSY, + Resource::TYPE_OAUTH2_FACEBOOK, + Resource::TYPE_OAUTH2_FIGMA, + Resource::TYPE_OAUTH2_FUSIONAUTH, + Resource::TYPE_OAUTH2_GITHUB, + Resource::TYPE_OAUTH2_GITLAB, + Resource::TYPE_OAUTH2_GOOGLE, + Resource::TYPE_OAUTH2_KEYCLOAK, + Resource::TYPE_OAUTH2_KICK, + Resource::TYPE_OAUTH2_LINKEDIN, + Resource::TYPE_OAUTH2_MICROSOFT, + Resource::TYPE_OAUTH2_NOTION, + Resource::TYPE_OAUTH2_OIDC, + Resource::TYPE_OAUTH2_OKTA, + Resource::TYPE_OAUTH2_PAYPAL, + Resource::TYPE_OAUTH2_PODIO, + Resource::TYPE_OAUTH2_SALESFORCE, + Resource::TYPE_OAUTH2_SLACK, + Resource::TYPE_OAUTH2_SPOTIFY, + Resource::TYPE_OAUTH2_STRIPE, + Resource::TYPE_OAUTH2_TRADESHIFT, + Resource::TYPE_OAUTH2_TWITCH, + Resource::TYPE_OAUTH2_WORDPRESS, + Resource::TYPE_OAUTH2_X, + Resource::TYPE_OAUTH2_YAHOO, + Resource::TYPE_OAUTH2_YANDEX, + Resource::TYPE_OAUTH2_ZOHO, + Resource::TYPE_OAUTH2_ZOOM, + ]; + public const GROUP_AUTH_RESOURCES = [ Resource::TYPE_USER, Resource::TYPE_TEAM, Resource::TYPE_MEMBERSHIP, Resource::TYPE_HASH, Resource::TYPE_AUTH_METHODS, - Resource::TYPE_OAUTH_PROVIDERS, Resource::TYPE_POLICIES, + ...self::GROUP_AUTH_OAUTH2_RESOURCES, ]; public const GROUP_STORAGE_RESOURCES = [ @@ -129,8 +172,8 @@ class Transfer Resource::TYPE_TEAM, Resource::TYPE_MEMBERSHIP, Resource::TYPE_AUTH_METHODS, - Resource::TYPE_OAUTH_PROVIDERS, Resource::TYPE_POLICIES, + ...self::GROUP_AUTH_OAUTH2_RESOURCES, Resource::TYPE_FILE, Resource::TYPE_BUCKET, Resource::TYPE_FUNCTION, From e809d361b633040002bc284bac09dd987a04cd59 Mon Sep 17 00:00:00 2001 From: Prem Palanisamy Date: Fri, 29 May 2026 11:20:55 +0100 Subject: [PATCH 06/11] OAuth2: collapse 40 TYPE constants to single TYPE_OAUTH2_PROVIDER (status counter size limit) --- src/Migration/Destinations/Appwrite.php | 2 +- src/Migration/Resource.php | 92 ++----------------- .../Resources/Auth/OAuth2/Amazon.php | 7 -- src/Migration/Resources/Auth/OAuth2/Apple.php | 7 -- src/Migration/Resources/Auth/OAuth2/Auth0.php | 7 -- .../Resources/Auth/OAuth2/Authentik.php | 7 -- .../Resources/Auth/OAuth2/Autodesk.php | 7 -- .../Resources/Auth/OAuth2/Bitbucket.php | 7 -- src/Migration/Resources/Auth/OAuth2/Bitly.php | 7 -- src/Migration/Resources/Auth/OAuth2/Box.php | 7 -- .../Resources/Auth/OAuth2/Dailymotion.php | 7 -- .../Resources/Auth/OAuth2/Discord.php | 7 -- .../Resources/Auth/OAuth2/Disqus.php | 7 -- .../Resources/Auth/OAuth2/Dropbox.php | 7 -- src/Migration/Resources/Auth/OAuth2/Etsy.php | 7 -- .../Resources/Auth/OAuth2/Facebook.php | 7 -- src/Migration/Resources/Auth/OAuth2/Figma.php | 7 -- .../Resources/Auth/OAuth2/FusionAuth.php | 7 -- .../Resources/Auth/OAuth2/Github.php | 7 -- .../Resources/Auth/OAuth2/Gitlab.php | 7 -- .../Resources/Auth/OAuth2/Google.php | 7 -- .../Resources/Auth/OAuth2/Keycloak.php | 7 -- src/Migration/Resources/Auth/OAuth2/Kick.php | 7 -- .../Resources/Auth/OAuth2/Linkedin.php | 7 -- .../Resources/Auth/OAuth2/Microsoft.php | 7 -- .../Resources/Auth/OAuth2/Notion.php | 7 -- .../Resources/Auth/OAuth2/OAuth2Provider.php | 13 +++ src/Migration/Resources/Auth/OAuth2/Oidc.php | 7 -- src/Migration/Resources/Auth/OAuth2/Okta.php | 7 -- .../Resources/Auth/OAuth2/Paypal.php | 7 -- src/Migration/Resources/Auth/OAuth2/Podio.php | 7 -- .../Resources/Auth/OAuth2/Salesforce.php | 7 -- src/Migration/Resources/Auth/OAuth2/Slack.php | 7 -- .../Resources/Auth/OAuth2/Spotify.php | 7 -- .../Resources/Auth/OAuth2/Stripe.php | 7 -- .../Resources/Auth/OAuth2/Tradeshift.php | 7 -- .../Resources/Auth/OAuth2/Twitch.php | 7 -- .../Resources/Auth/OAuth2/Wordpress.php | 7 -- src/Migration/Resources/Auth/OAuth2/X.php | 7 -- src/Migration/Resources/Auth/OAuth2/Yahoo.php | 7 -- .../Resources/Auth/OAuth2/Yandex.php | 7 -- src/Migration/Resources/Auth/OAuth2/Zoho.php | 7 -- src/Migration/Resources/Auth/OAuth2/Zoom.php | 7 -- src/Migration/Sources/Appwrite.php | 56 +++++------ src/Migration/Transfer.php | 47 +--------- 45 files changed, 47 insertions(+), 443 deletions(-) diff --git a/src/Migration/Destinations/Appwrite.php b/src/Migration/Destinations/Appwrite.php index 16caf4cf..c498dde4 100644 --- a/src/Migration/Destinations/Appwrite.php +++ b/src/Migration/Destinations/Appwrite.php @@ -266,7 +266,7 @@ public static function getSupportedResources(): array Resource::TYPE_MEMBERSHIP, Resource::TYPE_AUTH_METHODS, Resource::TYPE_POLICIES, - ...Transfer::GROUP_AUTH_OAUTH2_RESOURCES, + Resource::TYPE_OAUTH2_PROVIDER, // Database Resource::TYPE_DATABASE, diff --git a/src/Migration/Resource.php b/src/Migration/Resource.php index 10643414..4bf42096 100644 --- a/src/Migration/Resource.php +++ b/src/Migration/Resource.php @@ -73,50 +73,13 @@ abstract class Resource implements \JsonSerializable public const TYPE_POLICIES = 'policies'; - // OAuth2 providers — one type constant per provider. Each maps to its own - // typed Resource subclass under Resources/Auth/OAuth2/. Credential fields - // (clientSecret, p8File) are never migrated; only the readable - // non-secret configuration crosses over. - public const TYPE_OAUTH2_AMAZON = 'oauth2-amazon'; - public const TYPE_OAUTH2_APPLE = 'oauth2-apple'; - public const TYPE_OAUTH2_AUTH0 = 'oauth2-auth0'; - public const TYPE_OAUTH2_AUTHENTIK = 'oauth2-authentik'; - public const TYPE_OAUTH2_AUTODESK = 'oauth2-autodesk'; - public const TYPE_OAUTH2_BITBUCKET = 'oauth2-bitbucket'; - public const TYPE_OAUTH2_BITLY = 'oauth2-bitly'; - public const TYPE_OAUTH2_BOX = 'oauth2-box'; - public const TYPE_OAUTH2_DAILYMOTION = 'oauth2-dailymotion'; - public const TYPE_OAUTH2_DISCORD = 'oauth2-discord'; - public const TYPE_OAUTH2_DISQUS = 'oauth2-disqus'; - public const TYPE_OAUTH2_DROPBOX = 'oauth2-dropbox'; - public const TYPE_OAUTH2_ETSY = 'oauth2-etsy'; - public const TYPE_OAUTH2_FACEBOOK = 'oauth2-facebook'; - public const TYPE_OAUTH2_FIGMA = 'oauth2-figma'; - public const TYPE_OAUTH2_FUSIONAUTH = 'oauth2-fusionauth'; - public const TYPE_OAUTH2_GITHUB = 'oauth2-github'; - public const TYPE_OAUTH2_GITLAB = 'oauth2-gitlab'; - public const TYPE_OAUTH2_GOOGLE = 'oauth2-google'; - public const TYPE_OAUTH2_KEYCLOAK = 'oauth2-keycloak'; - public const TYPE_OAUTH2_KICK = 'oauth2-kick'; - public const TYPE_OAUTH2_LINKEDIN = 'oauth2-linkedin'; - public const TYPE_OAUTH2_MICROSOFT = 'oauth2-microsoft'; - public const TYPE_OAUTH2_NOTION = 'oauth2-notion'; - public const TYPE_OAUTH2_OIDC = 'oauth2-oidc'; - public const TYPE_OAUTH2_OKTA = 'oauth2-okta'; - public const TYPE_OAUTH2_PAYPAL = 'oauth2-paypal'; - public const TYPE_OAUTH2_PODIO = 'oauth2-podio'; - public const TYPE_OAUTH2_SALESFORCE = 'oauth2-salesforce'; - public const TYPE_OAUTH2_SLACK = 'oauth2-slack'; - public const TYPE_OAUTH2_SPOTIFY = 'oauth2-spotify'; - public const TYPE_OAUTH2_STRIPE = 'oauth2-stripe'; - public const TYPE_OAUTH2_TRADESHIFT = 'oauth2-tradeshift'; - public const TYPE_OAUTH2_TWITCH = 'oauth2-twitch'; - public const TYPE_OAUTH2_WORDPRESS = 'oauth2-wordpress'; - public const TYPE_OAUTH2_X = 'oauth2-x'; - public const TYPE_OAUTH2_YAHOO = 'oauth2-yahoo'; - public const TYPE_OAUTH2_YANDEX = 'oauth2-yandex'; - public const TYPE_OAUTH2_ZOHO = 'oauth2-zoho'; - public const TYPE_OAUTH2_ZOOM = 'oauth2-zoom'; + // OAuth2 providers — one type constant shared by all 40 provider + // Resource classes under Resources/Auth/OAuth2/. Per-provider dispatch on + // the destination uses `instanceof` on the concrete subclass; the single + // type constant keeps status counters compact (a per-provider constant + // explosion would push the OSS migration document's `statusCounters` JSON + // past its 3KB column limit when OAuth migration is selected). + public const TYPE_OAUTH2_PROVIDER = 'oauth2-provider'; public const TYPE_ENVIRONMENT_VARIABLE = 'environment-variable'; @@ -176,46 +139,7 @@ abstract class Resource implements \JsonSerializable self::TYPE_MEMBERSHIP, self::TYPE_AUTH_METHODS, self::TYPE_POLICIES, - self::TYPE_OAUTH2_AMAZON, - self::TYPE_OAUTH2_APPLE, - self::TYPE_OAUTH2_AUTH0, - self::TYPE_OAUTH2_AUTHENTIK, - self::TYPE_OAUTH2_AUTODESK, - self::TYPE_OAUTH2_BITBUCKET, - self::TYPE_OAUTH2_BITLY, - self::TYPE_OAUTH2_BOX, - self::TYPE_OAUTH2_DAILYMOTION, - self::TYPE_OAUTH2_DISCORD, - self::TYPE_OAUTH2_DISQUS, - self::TYPE_OAUTH2_DROPBOX, - self::TYPE_OAUTH2_ETSY, - self::TYPE_OAUTH2_FACEBOOK, - self::TYPE_OAUTH2_FIGMA, - self::TYPE_OAUTH2_FUSIONAUTH, - self::TYPE_OAUTH2_GITHUB, - self::TYPE_OAUTH2_GITLAB, - self::TYPE_OAUTH2_GOOGLE, - self::TYPE_OAUTH2_KEYCLOAK, - self::TYPE_OAUTH2_KICK, - self::TYPE_OAUTH2_LINKEDIN, - self::TYPE_OAUTH2_MICROSOFT, - self::TYPE_OAUTH2_NOTION, - self::TYPE_OAUTH2_OIDC, - self::TYPE_OAUTH2_OKTA, - self::TYPE_OAUTH2_PAYPAL, - self::TYPE_OAUTH2_PODIO, - self::TYPE_OAUTH2_SALESFORCE, - self::TYPE_OAUTH2_SLACK, - self::TYPE_OAUTH2_SPOTIFY, - self::TYPE_OAUTH2_STRIPE, - self::TYPE_OAUTH2_TRADESHIFT, - self::TYPE_OAUTH2_TWITCH, - self::TYPE_OAUTH2_WORDPRESS, - self::TYPE_OAUTH2_X, - self::TYPE_OAUTH2_YAHOO, - self::TYPE_OAUTH2_YANDEX, - self::TYPE_OAUTH2_ZOHO, - self::TYPE_OAUTH2_ZOOM, + self::TYPE_OAUTH2_PROVIDER, self::TYPE_PLATFORM, self::TYPE_API_KEY, self::TYPE_WEBHOOK, diff --git a/src/Migration/Resources/Auth/OAuth2/Amazon.php b/src/Migration/Resources/Auth/OAuth2/Amazon.php index 754a502b..d580b2c0 100644 --- a/src/Migration/Resources/Auth/OAuth2/Amazon.php +++ b/src/Migration/Resources/Auth/OAuth2/Amazon.php @@ -2,15 +2,8 @@ namespace Utopia\Migration\Resources\Auth\OAuth2; -use Utopia\Migration\Resource; - class Amazon extends StandardProvider { - public static function getName(): string - { - return Resource::TYPE_OAUTH2_AMAZON; - } - public static function getProviderKey(): string { return 'amazon'; diff --git a/src/Migration/Resources/Auth/OAuth2/Apple.php b/src/Migration/Resources/Auth/OAuth2/Apple.php index 638eb2a2..0f80f317 100644 --- a/src/Migration/Resources/Auth/OAuth2/Apple.php +++ b/src/Migration/Resources/Auth/OAuth2/Apple.php @@ -2,8 +2,6 @@ namespace Utopia\Migration\Resources\Auth\OAuth2; -use Utopia\Migration\Resource; - /** * Apple OAuth2 provider. Bespoke shape — the credential is split across four * fields. `serviceId`/`keyId`/`teamId` are readable on the source and @@ -55,11 +53,6 @@ public function jsonSerialize(): array ]; } - public static function getName(): string - { - return Resource::TYPE_OAUTH2_APPLE; - } - public static function getProviderKey(): string { return 'apple'; diff --git a/src/Migration/Resources/Auth/OAuth2/Auth0.php b/src/Migration/Resources/Auth/OAuth2/Auth0.php index 97f4fe96..6fb20fce 100644 --- a/src/Migration/Resources/Auth/OAuth2/Auth0.php +++ b/src/Migration/Resources/Auth/OAuth2/Auth0.php @@ -2,15 +2,8 @@ namespace Utopia\Migration\Resources\Auth\OAuth2; -use Utopia\Migration\Resource; - class Auth0 extends WithEndpointProvider { - public static function getName(): string - { - return Resource::TYPE_OAUTH2_AUTH0; - } - public static function getProviderKey(): string { return 'auth0'; diff --git a/src/Migration/Resources/Auth/OAuth2/Authentik.php b/src/Migration/Resources/Auth/OAuth2/Authentik.php index 10d12686..86016ff6 100644 --- a/src/Migration/Resources/Auth/OAuth2/Authentik.php +++ b/src/Migration/Resources/Auth/OAuth2/Authentik.php @@ -2,15 +2,8 @@ namespace Utopia\Migration\Resources\Auth\OAuth2; -use Utopia\Migration\Resource; - class Authentik extends WithEndpointProvider { - public static function getName(): string - { - return Resource::TYPE_OAUTH2_AUTHENTIK; - } - public static function getProviderKey(): string { return 'authentik'; diff --git a/src/Migration/Resources/Auth/OAuth2/Autodesk.php b/src/Migration/Resources/Auth/OAuth2/Autodesk.php index 339b7da9..0f1615c2 100644 --- a/src/Migration/Resources/Auth/OAuth2/Autodesk.php +++ b/src/Migration/Resources/Auth/OAuth2/Autodesk.php @@ -2,15 +2,8 @@ namespace Utopia\Migration\Resources\Auth\OAuth2; -use Utopia\Migration\Resource; - class Autodesk extends StandardProvider { - public static function getName(): string - { - return Resource::TYPE_OAUTH2_AUTODESK; - } - public static function getProviderKey(): string { return 'autodesk'; diff --git a/src/Migration/Resources/Auth/OAuth2/Bitbucket.php b/src/Migration/Resources/Auth/OAuth2/Bitbucket.php index b7ae4719..ad4bdaf0 100644 --- a/src/Migration/Resources/Auth/OAuth2/Bitbucket.php +++ b/src/Migration/Resources/Auth/OAuth2/Bitbucket.php @@ -2,15 +2,8 @@ namespace Utopia\Migration\Resources\Auth\OAuth2; -use Utopia\Migration\Resource; - class Bitbucket extends StandardProvider { - public static function getName(): string - { - return Resource::TYPE_OAUTH2_BITBUCKET; - } - public static function getProviderKey(): string { return 'bitbucket'; diff --git a/src/Migration/Resources/Auth/OAuth2/Bitly.php b/src/Migration/Resources/Auth/OAuth2/Bitly.php index 7e3203ea..3555925f 100644 --- a/src/Migration/Resources/Auth/OAuth2/Bitly.php +++ b/src/Migration/Resources/Auth/OAuth2/Bitly.php @@ -2,15 +2,8 @@ namespace Utopia\Migration\Resources\Auth\OAuth2; -use Utopia\Migration\Resource; - class Bitly extends StandardProvider { - public static function getName(): string - { - return Resource::TYPE_OAUTH2_BITLY; - } - public static function getProviderKey(): string { return 'bitly'; diff --git a/src/Migration/Resources/Auth/OAuth2/Box.php b/src/Migration/Resources/Auth/OAuth2/Box.php index ae3b32e2..3d7a8518 100644 --- a/src/Migration/Resources/Auth/OAuth2/Box.php +++ b/src/Migration/Resources/Auth/OAuth2/Box.php @@ -2,15 +2,8 @@ namespace Utopia\Migration\Resources\Auth\OAuth2; -use Utopia\Migration\Resource; - class Box extends StandardProvider { - public static function getName(): string - { - return Resource::TYPE_OAUTH2_BOX; - } - public static function getProviderKey(): string { return 'box'; diff --git a/src/Migration/Resources/Auth/OAuth2/Dailymotion.php b/src/Migration/Resources/Auth/OAuth2/Dailymotion.php index 01aaf781..712bd010 100644 --- a/src/Migration/Resources/Auth/OAuth2/Dailymotion.php +++ b/src/Migration/Resources/Auth/OAuth2/Dailymotion.php @@ -2,15 +2,8 @@ namespace Utopia\Migration\Resources\Auth\OAuth2; -use Utopia\Migration\Resource; - class Dailymotion extends StandardProvider { - public static function getName(): string - { - return Resource::TYPE_OAUTH2_DAILYMOTION; - } - public static function getProviderKey(): string { return 'dailymotion'; diff --git a/src/Migration/Resources/Auth/OAuth2/Discord.php b/src/Migration/Resources/Auth/OAuth2/Discord.php index bc07a36f..6adee9f6 100644 --- a/src/Migration/Resources/Auth/OAuth2/Discord.php +++ b/src/Migration/Resources/Auth/OAuth2/Discord.php @@ -2,15 +2,8 @@ namespace Utopia\Migration\Resources\Auth\OAuth2; -use Utopia\Migration\Resource; - class Discord extends StandardProvider { - public static function getName(): string - { - return Resource::TYPE_OAUTH2_DISCORD; - } - public static function getProviderKey(): string { return 'discord'; diff --git a/src/Migration/Resources/Auth/OAuth2/Disqus.php b/src/Migration/Resources/Auth/OAuth2/Disqus.php index 0b503cf8..56fbb090 100644 --- a/src/Migration/Resources/Auth/OAuth2/Disqus.php +++ b/src/Migration/Resources/Auth/OAuth2/Disqus.php @@ -2,15 +2,8 @@ namespace Utopia\Migration\Resources\Auth\OAuth2; -use Utopia\Migration\Resource; - class Disqus extends StandardProvider { - public static function getName(): string - { - return Resource::TYPE_OAUTH2_DISQUS; - } - public static function getProviderKey(): string { return 'disqus'; diff --git a/src/Migration/Resources/Auth/OAuth2/Dropbox.php b/src/Migration/Resources/Auth/OAuth2/Dropbox.php index 076442a2..0e185642 100644 --- a/src/Migration/Resources/Auth/OAuth2/Dropbox.php +++ b/src/Migration/Resources/Auth/OAuth2/Dropbox.php @@ -2,15 +2,8 @@ namespace Utopia\Migration\Resources\Auth\OAuth2; -use Utopia\Migration\Resource; - class Dropbox extends StandardProvider { - public static function getName(): string - { - return Resource::TYPE_OAUTH2_DROPBOX; - } - public static function getProviderKey(): string { return 'dropbox'; diff --git a/src/Migration/Resources/Auth/OAuth2/Etsy.php b/src/Migration/Resources/Auth/OAuth2/Etsy.php index ccb0a11a..c723b3a0 100644 --- a/src/Migration/Resources/Auth/OAuth2/Etsy.php +++ b/src/Migration/Resources/Auth/OAuth2/Etsy.php @@ -2,15 +2,8 @@ namespace Utopia\Migration\Resources\Auth\OAuth2; -use Utopia\Migration\Resource; - class Etsy extends StandardProvider { - public static function getName(): string - { - return Resource::TYPE_OAUTH2_ETSY; - } - public static function getProviderKey(): string { return 'etsy'; diff --git a/src/Migration/Resources/Auth/OAuth2/Facebook.php b/src/Migration/Resources/Auth/OAuth2/Facebook.php index 36843aeb..180fe0ba 100644 --- a/src/Migration/Resources/Auth/OAuth2/Facebook.php +++ b/src/Migration/Resources/Auth/OAuth2/Facebook.php @@ -2,15 +2,8 @@ namespace Utopia\Migration\Resources\Auth\OAuth2; -use Utopia\Migration\Resource; - class Facebook extends StandardProvider { - public static function getName(): string - { - return Resource::TYPE_OAUTH2_FACEBOOK; - } - public static function getProviderKey(): string { return 'facebook'; diff --git a/src/Migration/Resources/Auth/OAuth2/Figma.php b/src/Migration/Resources/Auth/OAuth2/Figma.php index 4287d81d..fb579f3b 100644 --- a/src/Migration/Resources/Auth/OAuth2/Figma.php +++ b/src/Migration/Resources/Auth/OAuth2/Figma.php @@ -2,15 +2,8 @@ namespace Utopia\Migration\Resources\Auth\OAuth2; -use Utopia\Migration\Resource; - class Figma extends StandardProvider { - public static function getName(): string - { - return Resource::TYPE_OAUTH2_FIGMA; - } - public static function getProviderKey(): string { return 'figma'; diff --git a/src/Migration/Resources/Auth/OAuth2/FusionAuth.php b/src/Migration/Resources/Auth/OAuth2/FusionAuth.php index 75ff5b55..caf48a6a 100644 --- a/src/Migration/Resources/Auth/OAuth2/FusionAuth.php +++ b/src/Migration/Resources/Auth/OAuth2/FusionAuth.php @@ -2,15 +2,8 @@ namespace Utopia\Migration\Resources\Auth\OAuth2; -use Utopia\Migration\Resource; - class FusionAuth extends WithEndpointProvider { - public static function getName(): string - { - return Resource::TYPE_OAUTH2_FUSIONAUTH; - } - public static function getProviderKey(): string { return 'fusionauth'; diff --git a/src/Migration/Resources/Auth/OAuth2/Github.php b/src/Migration/Resources/Auth/OAuth2/Github.php index 2ebd7994..8c6ce4e6 100644 --- a/src/Migration/Resources/Auth/OAuth2/Github.php +++ b/src/Migration/Resources/Auth/OAuth2/Github.php @@ -2,15 +2,8 @@ namespace Utopia\Migration\Resources\Auth\OAuth2; -use Utopia\Migration\Resource; - class Github extends StandardProvider { - public static function getName(): string - { - return Resource::TYPE_OAUTH2_GITHUB; - } - public static function getProviderKey(): string { return 'github'; diff --git a/src/Migration/Resources/Auth/OAuth2/Gitlab.php b/src/Migration/Resources/Auth/OAuth2/Gitlab.php index 145eb68d..8a4fca2a 100644 --- a/src/Migration/Resources/Auth/OAuth2/Gitlab.php +++ b/src/Migration/Resources/Auth/OAuth2/Gitlab.php @@ -2,15 +2,8 @@ namespace Utopia\Migration\Resources\Auth\OAuth2; -use Utopia\Migration\Resource; - class Gitlab extends WithEndpointProvider { - public static function getName(): string - { - return Resource::TYPE_OAUTH2_GITLAB; - } - public static function getProviderKey(): string { return 'gitlab'; diff --git a/src/Migration/Resources/Auth/OAuth2/Google.php b/src/Migration/Resources/Auth/OAuth2/Google.php index 876c7eb8..68b6020d 100644 --- a/src/Migration/Resources/Auth/OAuth2/Google.php +++ b/src/Migration/Resources/Auth/OAuth2/Google.php @@ -2,8 +2,6 @@ namespace Utopia\Migration\Resources\Auth\OAuth2; -use Utopia\Migration\Resource; - /** * Google OAuth2 provider. Standard `clientId` plus an array of OAuth `prompt` * modes (consent / none / select_account). @@ -54,11 +52,6 @@ public function jsonSerialize(): array ]; } - public static function getName(): string - { - return Resource::TYPE_OAUTH2_GOOGLE; - } - public static function getProviderKey(): string { return 'google'; diff --git a/src/Migration/Resources/Auth/OAuth2/Keycloak.php b/src/Migration/Resources/Auth/OAuth2/Keycloak.php index c949d882..f64967af 100644 --- a/src/Migration/Resources/Auth/OAuth2/Keycloak.php +++ b/src/Migration/Resources/Auth/OAuth2/Keycloak.php @@ -2,15 +2,8 @@ namespace Utopia\Migration\Resources\Auth\OAuth2; -use Utopia\Migration\Resource; - class Keycloak extends WithEndpointProvider { - public static function getName(): string - { - return Resource::TYPE_OAUTH2_KEYCLOAK; - } - public static function getProviderKey(): string { return 'keycloak'; diff --git a/src/Migration/Resources/Auth/OAuth2/Kick.php b/src/Migration/Resources/Auth/OAuth2/Kick.php index 2c0427fe..de9162a8 100644 --- a/src/Migration/Resources/Auth/OAuth2/Kick.php +++ b/src/Migration/Resources/Auth/OAuth2/Kick.php @@ -2,15 +2,8 @@ namespace Utopia\Migration\Resources\Auth\OAuth2; -use Utopia\Migration\Resource; - class Kick extends StandardProvider { - public static function getName(): string - { - return Resource::TYPE_OAUTH2_KICK; - } - public static function getProviderKey(): string { return 'kick'; diff --git a/src/Migration/Resources/Auth/OAuth2/Linkedin.php b/src/Migration/Resources/Auth/OAuth2/Linkedin.php index 6ab1fc67..ed6e25d0 100644 --- a/src/Migration/Resources/Auth/OAuth2/Linkedin.php +++ b/src/Migration/Resources/Auth/OAuth2/Linkedin.php @@ -2,15 +2,8 @@ namespace Utopia\Migration\Resources\Auth\OAuth2; -use Utopia\Migration\Resource; - class Linkedin extends StandardProvider { - public static function getName(): string - { - return Resource::TYPE_OAUTH2_LINKEDIN; - } - public static function getProviderKey(): string { return 'linkedin'; diff --git a/src/Migration/Resources/Auth/OAuth2/Microsoft.php b/src/Migration/Resources/Auth/OAuth2/Microsoft.php index 352a9633..fd0a0ee9 100644 --- a/src/Migration/Resources/Auth/OAuth2/Microsoft.php +++ b/src/Migration/Resources/Auth/OAuth2/Microsoft.php @@ -2,8 +2,6 @@ namespace Utopia\Migration\Resources\Auth\OAuth2; -use Utopia\Migration\Resource; - /** * Microsoft OAuth2 provider. Standard `clientId` plus a `tenant` field * identifying the Azure AD tenant. @@ -51,11 +49,6 @@ public function jsonSerialize(): array ]; } - public static function getName(): string - { - return Resource::TYPE_OAUTH2_MICROSOFT; - } - public static function getProviderKey(): string { return 'microsoft'; diff --git a/src/Migration/Resources/Auth/OAuth2/Notion.php b/src/Migration/Resources/Auth/OAuth2/Notion.php index 8ec7cb0f..bf559817 100644 --- a/src/Migration/Resources/Auth/OAuth2/Notion.php +++ b/src/Migration/Resources/Auth/OAuth2/Notion.php @@ -2,15 +2,8 @@ namespace Utopia\Migration\Resources\Auth\OAuth2; -use Utopia\Migration\Resource; - class Notion extends StandardProvider { - public static function getName(): string - { - return Resource::TYPE_OAUTH2_NOTION; - } - public static function getProviderKey(): string { return 'notion'; diff --git a/src/Migration/Resources/Auth/OAuth2/OAuth2Provider.php b/src/Migration/Resources/Auth/OAuth2/OAuth2Provider.php index 8634fa52..324b267a 100644 --- a/src/Migration/Resources/Auth/OAuth2/OAuth2Provider.php +++ b/src/Migration/Resources/Auth/OAuth2/OAuth2Provider.php @@ -24,6 +24,19 @@ abstract class OAuth2Provider extends Resource */ abstract public static function fromArray(array $array): self; + /** + * Every OAuth2 provider Resource shares one type name. Per-provider + * dispatch happens via `instanceof` on the concrete subclass — the type + * constant exists only to bucket all OAuth2 resources under one status + * counter (a per-provider TYPE explosion would blow past the 3KB cap on + * the OSS migration document's `statusCounters` column for projects that + * select OAuth migration). + */ + public static function getName(): string + { + return Resource::TYPE_OAUTH2_PROVIDER; + } + public function __construct( string $id, protected readonly bool $enabled = false, diff --git a/src/Migration/Resources/Auth/OAuth2/Oidc.php b/src/Migration/Resources/Auth/OAuth2/Oidc.php index 77136b8c..65f788fc 100644 --- a/src/Migration/Resources/Auth/OAuth2/Oidc.php +++ b/src/Migration/Resources/Auth/OAuth2/Oidc.php @@ -2,15 +2,8 @@ namespace Utopia\Migration\Resources\Auth\OAuth2; -use Utopia\Migration\Resource; - class Oidc extends WithEndpointProvider { - public static function getName(): string - { - return Resource::TYPE_OAUTH2_OIDC; - } - public static function getProviderKey(): string { return 'oidc'; diff --git a/src/Migration/Resources/Auth/OAuth2/Okta.php b/src/Migration/Resources/Auth/OAuth2/Okta.php index 52449694..d4f11ac3 100644 --- a/src/Migration/Resources/Auth/OAuth2/Okta.php +++ b/src/Migration/Resources/Auth/OAuth2/Okta.php @@ -2,15 +2,8 @@ namespace Utopia\Migration\Resources\Auth\OAuth2; -use Utopia\Migration\Resource; - class Okta extends WithEndpointProvider { - public static function getName(): string - { - return Resource::TYPE_OAUTH2_OKTA; - } - public static function getProviderKey(): string { return 'okta'; diff --git a/src/Migration/Resources/Auth/OAuth2/Paypal.php b/src/Migration/Resources/Auth/OAuth2/Paypal.php index 36a796fa..1852cbfd 100644 --- a/src/Migration/Resources/Auth/OAuth2/Paypal.php +++ b/src/Migration/Resources/Auth/OAuth2/Paypal.php @@ -2,15 +2,8 @@ namespace Utopia\Migration\Resources\Auth\OAuth2; -use Utopia\Migration\Resource; - class Paypal extends StandardProvider { - public static function getName(): string - { - return Resource::TYPE_OAUTH2_PAYPAL; - } - public static function getProviderKey(): string { return 'paypal'; diff --git a/src/Migration/Resources/Auth/OAuth2/Podio.php b/src/Migration/Resources/Auth/OAuth2/Podio.php index 2c4c2d22..d317d36a 100644 --- a/src/Migration/Resources/Auth/OAuth2/Podio.php +++ b/src/Migration/Resources/Auth/OAuth2/Podio.php @@ -2,15 +2,8 @@ namespace Utopia\Migration\Resources\Auth\OAuth2; -use Utopia\Migration\Resource; - class Podio extends StandardProvider { - public static function getName(): string - { - return Resource::TYPE_OAUTH2_PODIO; - } - public static function getProviderKey(): string { return 'podio'; diff --git a/src/Migration/Resources/Auth/OAuth2/Salesforce.php b/src/Migration/Resources/Auth/OAuth2/Salesforce.php index a1f8bbeb..68875a64 100644 --- a/src/Migration/Resources/Auth/OAuth2/Salesforce.php +++ b/src/Migration/Resources/Auth/OAuth2/Salesforce.php @@ -2,15 +2,8 @@ namespace Utopia\Migration\Resources\Auth\OAuth2; -use Utopia\Migration\Resource; - class Salesforce extends StandardProvider { - public static function getName(): string - { - return Resource::TYPE_OAUTH2_SALESFORCE; - } - public static function getProviderKey(): string { return 'salesforce'; diff --git a/src/Migration/Resources/Auth/OAuth2/Slack.php b/src/Migration/Resources/Auth/OAuth2/Slack.php index d3696b23..29eb27d5 100644 --- a/src/Migration/Resources/Auth/OAuth2/Slack.php +++ b/src/Migration/Resources/Auth/OAuth2/Slack.php @@ -2,15 +2,8 @@ namespace Utopia\Migration\Resources\Auth\OAuth2; -use Utopia\Migration\Resource; - class Slack extends StandardProvider { - public static function getName(): string - { - return Resource::TYPE_OAUTH2_SLACK; - } - public static function getProviderKey(): string { return 'slack'; diff --git a/src/Migration/Resources/Auth/OAuth2/Spotify.php b/src/Migration/Resources/Auth/OAuth2/Spotify.php index 8386b459..6bbc1feb 100644 --- a/src/Migration/Resources/Auth/OAuth2/Spotify.php +++ b/src/Migration/Resources/Auth/OAuth2/Spotify.php @@ -2,15 +2,8 @@ namespace Utopia\Migration\Resources\Auth\OAuth2; -use Utopia\Migration\Resource; - class Spotify extends StandardProvider { - public static function getName(): string - { - return Resource::TYPE_OAUTH2_SPOTIFY; - } - public static function getProviderKey(): string { return 'spotify'; diff --git a/src/Migration/Resources/Auth/OAuth2/Stripe.php b/src/Migration/Resources/Auth/OAuth2/Stripe.php index be058d77..b702efb1 100644 --- a/src/Migration/Resources/Auth/OAuth2/Stripe.php +++ b/src/Migration/Resources/Auth/OAuth2/Stripe.php @@ -2,15 +2,8 @@ namespace Utopia\Migration\Resources\Auth\OAuth2; -use Utopia\Migration\Resource; - class Stripe extends StandardProvider { - public static function getName(): string - { - return Resource::TYPE_OAUTH2_STRIPE; - } - public static function getProviderKey(): string { return 'stripe'; diff --git a/src/Migration/Resources/Auth/OAuth2/Tradeshift.php b/src/Migration/Resources/Auth/OAuth2/Tradeshift.php index 1f6b4753..003d8f65 100644 --- a/src/Migration/Resources/Auth/OAuth2/Tradeshift.php +++ b/src/Migration/Resources/Auth/OAuth2/Tradeshift.php @@ -2,15 +2,8 @@ namespace Utopia\Migration\Resources\Auth\OAuth2; -use Utopia\Migration\Resource; - class Tradeshift extends StandardProvider { - public static function getName(): string - { - return Resource::TYPE_OAUTH2_TRADESHIFT; - } - public static function getProviderKey(): string { return 'tradeshift'; diff --git a/src/Migration/Resources/Auth/OAuth2/Twitch.php b/src/Migration/Resources/Auth/OAuth2/Twitch.php index b91bd9ee..57ac63a1 100644 --- a/src/Migration/Resources/Auth/OAuth2/Twitch.php +++ b/src/Migration/Resources/Auth/OAuth2/Twitch.php @@ -2,15 +2,8 @@ namespace Utopia\Migration\Resources\Auth\OAuth2; -use Utopia\Migration\Resource; - class Twitch extends StandardProvider { - public static function getName(): string - { - return Resource::TYPE_OAUTH2_TWITCH; - } - public static function getProviderKey(): string { return 'twitch'; diff --git a/src/Migration/Resources/Auth/OAuth2/Wordpress.php b/src/Migration/Resources/Auth/OAuth2/Wordpress.php index 2f4cbd72..99e1a348 100644 --- a/src/Migration/Resources/Auth/OAuth2/Wordpress.php +++ b/src/Migration/Resources/Auth/OAuth2/Wordpress.php @@ -2,15 +2,8 @@ namespace Utopia\Migration\Resources\Auth\OAuth2; -use Utopia\Migration\Resource; - class Wordpress extends StandardProvider { - public static function getName(): string - { - return Resource::TYPE_OAUTH2_WORDPRESS; - } - public static function getProviderKey(): string { return 'wordpress'; diff --git a/src/Migration/Resources/Auth/OAuth2/X.php b/src/Migration/Resources/Auth/OAuth2/X.php index 38f890b3..3d9230ae 100644 --- a/src/Migration/Resources/Auth/OAuth2/X.php +++ b/src/Migration/Resources/Auth/OAuth2/X.php @@ -2,15 +2,8 @@ namespace Utopia\Migration\Resources\Auth\OAuth2; -use Utopia\Migration\Resource; - class X extends StandardProvider { - public static function getName(): string - { - return Resource::TYPE_OAUTH2_X; - } - public static function getProviderKey(): string { return 'x'; diff --git a/src/Migration/Resources/Auth/OAuth2/Yahoo.php b/src/Migration/Resources/Auth/OAuth2/Yahoo.php index 69f001b0..397e3bf4 100644 --- a/src/Migration/Resources/Auth/OAuth2/Yahoo.php +++ b/src/Migration/Resources/Auth/OAuth2/Yahoo.php @@ -2,15 +2,8 @@ namespace Utopia\Migration\Resources\Auth\OAuth2; -use Utopia\Migration\Resource; - class Yahoo extends StandardProvider { - public static function getName(): string - { - return Resource::TYPE_OAUTH2_YAHOO; - } - public static function getProviderKey(): string { return 'yahoo'; diff --git a/src/Migration/Resources/Auth/OAuth2/Yandex.php b/src/Migration/Resources/Auth/OAuth2/Yandex.php index d248ae21..2748ea67 100644 --- a/src/Migration/Resources/Auth/OAuth2/Yandex.php +++ b/src/Migration/Resources/Auth/OAuth2/Yandex.php @@ -2,15 +2,8 @@ namespace Utopia\Migration\Resources\Auth\OAuth2; -use Utopia\Migration\Resource; - class Yandex extends StandardProvider { - public static function getName(): string - { - return Resource::TYPE_OAUTH2_YANDEX; - } - public static function getProviderKey(): string { return 'yandex'; diff --git a/src/Migration/Resources/Auth/OAuth2/Zoho.php b/src/Migration/Resources/Auth/OAuth2/Zoho.php index 26cee251..76348b98 100644 --- a/src/Migration/Resources/Auth/OAuth2/Zoho.php +++ b/src/Migration/Resources/Auth/OAuth2/Zoho.php @@ -2,15 +2,8 @@ namespace Utopia\Migration\Resources\Auth\OAuth2; -use Utopia\Migration\Resource; - class Zoho extends StandardProvider { - public static function getName(): string - { - return Resource::TYPE_OAUTH2_ZOHO; - } - public static function getProviderKey(): string { return 'zoho'; diff --git a/src/Migration/Resources/Auth/OAuth2/Zoom.php b/src/Migration/Resources/Auth/OAuth2/Zoom.php index 00f42040..4ebc7ccc 100644 --- a/src/Migration/Resources/Auth/OAuth2/Zoom.php +++ b/src/Migration/Resources/Auth/OAuth2/Zoom.php @@ -2,15 +2,8 @@ namespace Utopia\Migration\Resources\Auth\OAuth2; -use Utopia\Migration\Resource; - class Zoom extends StandardProvider { - public static function getName(): string - { - return Resource::TYPE_OAUTH2_ZOOM; - } - public static function getProviderKey(): string { return 'zoom'; diff --git a/src/Migration/Sources/Appwrite.php b/src/Migration/Sources/Appwrite.php index 95e61111..49567154 100644 --- a/src/Migration/Sources/Appwrite.php +++ b/src/Migration/Sources/Appwrite.php @@ -192,7 +192,7 @@ public static function getSupportedResources(): array Resource::TYPE_MEMBERSHIP, Resource::TYPE_AUTH_METHODS, Resource::TYPE_POLICIES, - ...Transfer::GROUP_AUTH_OAUTH2_RESOURCES, + Resource::TYPE_OAUTH2_PROVIDER, // Database Resource::TYPE_DATABASE, @@ -393,12 +393,15 @@ private function reportAuth(array $resources, array &$report, array $resourceIds $report[Resource::TYPE_AUTH_METHODS] = 1; } - // OAuth2 providers — one resource per provider type. Each is a - // singleton config (one map entry per project) but emitted as its own - // typed Resource so per-provider status/failures are visible. - foreach (Transfer::GROUP_AUTH_OAUTH2_RESOURCES as $oauth2Type) { - if (\in_array($oauth2Type, $resources)) { - $report[$oauth2Type] = 1; + // OAuth2 providers — all 40 share one TYPE constant; the report + // counts one entry per enabled provider on the source. + if (\in_array(Resource::TYPE_OAUTH2_PROVIDER, $resources)) { + try { + $report[Resource::TYPE_OAUTH2_PROVIDER] = \count( + $this->project->listOAuth2Providers()->providers ?? [] + ); + } catch (\Throwable) { + $report[Resource::TYPE_OAUTH2_PROVIDER] = 0; } } @@ -661,25 +664,20 @@ protected function exportGroupAuth(int $batchSize, array $resources): void )); } - // A single `listOAuth2Providers` call returns the full per-provider - // payload; dispatch is by type. We catch and tag errors per-provider - // so one provider's failure doesn't suppress the others. - if (\count(\array_intersect(Transfer::GROUP_AUTH_OAUTH2_RESOURCES, $resources)) > 0) { + // A single `listOAuth2Providers` call returns every provider; each one + // is emitted as its own typed Resource (with shared TYPE_OAUTH2_PROVIDER). + // Per-provider failures surface in `$this->errors[]`. + if (\in_array(Resource::TYPE_OAUTH2_PROVIDER, $resources)) { try { - $this->exportOAuth2Providers($resources); + $this->exportOAuth2Providers(); } catch (\Throwable $e) { - foreach (Transfer::GROUP_AUTH_OAUTH2_RESOURCES as $oauth2Type) { - if (!\in_array($oauth2Type, $resources)) { - continue; - } - $this->addError(new Exception( - $oauth2Type, - Transfer::GROUP_AUTH, - message: $e->getMessage(), - code: (int) $e->getCode() ?: Exception::CODE_INTERNAL, - previous: $e - )); - } + $this->addError(new Exception( + Resource::TYPE_OAUTH2_PROVIDER, + Transfer::GROUP_AUTH, + message: $e->getMessage(), + code: (int) $e->getCode() ?: Exception::CODE_INTERNAL, + previous: $e + )); } } @@ -811,10 +809,8 @@ private function exportAuthMethods(): void * non-secret fields. Credential fields (`clientSecret`, `p8File`) come * back blanked from the server and are intentionally not migrated — * destination admin must re-enter them per provider. - * - * @param array $resources Resources selected for migration. */ - private function exportOAuth2Providers(array $resources): void + private function exportOAuth2Providers(): void { $response = $this->project->listOAuth2Providers(); @@ -833,12 +829,6 @@ private function exportOAuth2Providers(array $resources): void continue; } - /** @var class-string $class */ - $type = $class::getName(); - if (!\in_array($type, $resources, true)) { - continue; - } - // Hand the raw payload to the per-provider fromArray, which knows // which fields it cares about. $payload = $provider; diff --git a/src/Migration/Transfer.php b/src/Migration/Transfer.php index 9596cb75..05e6a242 100644 --- a/src/Migration/Transfer.php +++ b/src/Migration/Transfer.php @@ -32,49 +32,6 @@ class Transfer public const GROUP_DOMAINS = 'domains'; - public const GROUP_AUTH_OAUTH2_RESOURCES = [ - Resource::TYPE_OAUTH2_AMAZON, - Resource::TYPE_OAUTH2_APPLE, - Resource::TYPE_OAUTH2_AUTH0, - Resource::TYPE_OAUTH2_AUTHENTIK, - Resource::TYPE_OAUTH2_AUTODESK, - Resource::TYPE_OAUTH2_BITBUCKET, - Resource::TYPE_OAUTH2_BITLY, - Resource::TYPE_OAUTH2_BOX, - Resource::TYPE_OAUTH2_DAILYMOTION, - Resource::TYPE_OAUTH2_DISCORD, - Resource::TYPE_OAUTH2_DISQUS, - Resource::TYPE_OAUTH2_DROPBOX, - Resource::TYPE_OAUTH2_ETSY, - Resource::TYPE_OAUTH2_FACEBOOK, - Resource::TYPE_OAUTH2_FIGMA, - Resource::TYPE_OAUTH2_FUSIONAUTH, - Resource::TYPE_OAUTH2_GITHUB, - Resource::TYPE_OAUTH2_GITLAB, - Resource::TYPE_OAUTH2_GOOGLE, - Resource::TYPE_OAUTH2_KEYCLOAK, - Resource::TYPE_OAUTH2_KICK, - Resource::TYPE_OAUTH2_LINKEDIN, - Resource::TYPE_OAUTH2_MICROSOFT, - Resource::TYPE_OAUTH2_NOTION, - Resource::TYPE_OAUTH2_OIDC, - Resource::TYPE_OAUTH2_OKTA, - Resource::TYPE_OAUTH2_PAYPAL, - Resource::TYPE_OAUTH2_PODIO, - Resource::TYPE_OAUTH2_SALESFORCE, - Resource::TYPE_OAUTH2_SLACK, - Resource::TYPE_OAUTH2_SPOTIFY, - Resource::TYPE_OAUTH2_STRIPE, - Resource::TYPE_OAUTH2_TRADESHIFT, - Resource::TYPE_OAUTH2_TWITCH, - Resource::TYPE_OAUTH2_WORDPRESS, - Resource::TYPE_OAUTH2_X, - Resource::TYPE_OAUTH2_YAHOO, - Resource::TYPE_OAUTH2_YANDEX, - Resource::TYPE_OAUTH2_ZOHO, - Resource::TYPE_OAUTH2_ZOOM, - ]; - public const GROUP_AUTH_RESOURCES = [ Resource::TYPE_USER, Resource::TYPE_TEAM, @@ -82,7 +39,7 @@ class Transfer Resource::TYPE_HASH, Resource::TYPE_AUTH_METHODS, Resource::TYPE_POLICIES, - ...self::GROUP_AUTH_OAUTH2_RESOURCES, + Resource::TYPE_OAUTH2_PROVIDER, ]; public const GROUP_STORAGE_RESOURCES = [ @@ -173,7 +130,7 @@ class Transfer Resource::TYPE_MEMBERSHIP, Resource::TYPE_AUTH_METHODS, Resource::TYPE_POLICIES, - ...self::GROUP_AUTH_OAUTH2_RESOURCES, + Resource::TYPE_OAUTH2_PROVIDER, Resource::TYPE_FILE, Resource::TYPE_BUCKET, Resource::TYPE_FUNCTION, From 4829ace12f43f3d97020661660d0d4abb312eaad Mon Sep 17 00:00:00 2001 From: Prem Palanisamy Date: Fri, 29 May 2026 15:33:29 +0100 Subject: [PATCH 07/11] OAuth2: drop conditional-enable guard; propagate enabled as-is across all providers --- src/Migration/Destinations/Appwrite.php | 28 +++++++++---------------- 1 file changed, 10 insertions(+), 18 deletions(-) diff --git a/src/Migration/Destinations/Appwrite.php b/src/Migration/Destinations/Appwrite.php index c498dde4..93722602 100644 --- a/src/Migration/Destinations/Appwrite.php +++ b/src/Migration/Destinations/Appwrite.php @@ -3567,15 +3567,16 @@ protected function createAuthMethods(AuthMethods $resource): bool * Read-then-merge a single OAuth2 provider's entries on the project's * `oAuthProviders` map. The same storage shape covers every provider — * `{providerKey}Appid` for the readable client identifier, `{providerKey}Secret` - * for the credential blob (write-only, never overwritten), and - * `{providerKey}Enabled` for the toggle. Per-provider extras (Apple's - * keyId/teamId merged into the secret JSON, Microsoft's tenant, OIDC's - * endpoint, Google's prompt) are handled in the per-shape branches. + * for the credential blob, and `{providerKey}Enabled` for the toggle. + * Per-provider extras (Apple's keyId/teamId merged into the secret JSON, + * Microsoft's tenant, OIDC's endpoint, Google's prompt) are handled in the + * per-shape branches. * - * Safety guard: `enabled = true` is only applied if the destination already - * has a `{providerKey}Secret` set. Otherwise sign-in would redirect users to - * the OAuth server with no credentials and fail at runtime. Disables are - * always applied — they can never produce a broken sign-in. + * The actual secret material the OAuth handshake needs (`clientSecret` for + * standard providers, `p8File` for Apple) is write-only on the source API, + * so it never makes it to the destination — the admin must re-enter it + * post-migration. `enabled` is propagated as-is; until the admin enters the + * secret, sign-in attempts for that provider will fail at runtime. */ protected function createOAuth2Provider(OAuth2Provider $resource): bool { @@ -3583,7 +3584,6 @@ protected function createOAuth2Provider(OAuth2Provider $resource): bool $project = $this->dbForPlatform->getDocument('projects', $this->projectId); $oAuthProviders = $project->getAttribute('oAuthProviders', []); - // Common: write the readable client identifier (clientId / serviceId). if ($resource instanceof OAuth2Apple) { if ($resource->getServiceId() !== '') { $oAuthProviders[$key . 'Appid'] = $resource->getServiceId(); @@ -3620,15 +3620,7 @@ protected function createOAuth2Provider(OAuth2Provider $resource): bool } } - if ($resource->getEnabled()) { - // Don't flip enabled = true unless the destination already has a - // secret configured — see method doc. - if (!empty($oAuthProviders[$key . 'Secret'])) { - $oAuthProviders[$key . 'Enabled'] = true; - } - } else { - $oAuthProviders[$key . 'Enabled'] = false; - } + $oAuthProviders[$key . 'Enabled'] = $resource->getEnabled(); $this->dbForPlatform->getAuthorization()->skip(fn () => $this->dbForPlatform->updateDocument( 'projects', From 69dabe56225116b34fd2f41d250fd0835bcfd61e Mon Sep 17 00:00:00 2001 From: Prem Palanisamy Date: Fri, 29 May 2026 16:01:14 +0100 Subject: [PATCH 08/11] OAuth2: derive provider key from class instead of duplicating in source map --- src/Migration/Sources/Appwrite.php | 117 ++++++++++++++++++----------- 1 file changed, 73 insertions(+), 44 deletions(-) diff --git a/src/Migration/Sources/Appwrite.php b/src/Migration/Sources/Appwrite.php index 49567154..733559db 100644 --- a/src/Migration/Sources/Appwrite.php +++ b/src/Migration/Sources/Appwrite.php @@ -26,6 +26,7 @@ use Utopia\Migration\Resources\Auth\AuthMethods; use Utopia\Migration\Resources\Auth\Hash; use Utopia\Migration\Resources\Auth\Membership; +use Utopia\Migration\Resources\Auth\OAuth2; use Utopia\Migration\Resources\Auth\OAuth2\OAuth2Provider; use Utopia\Migration\Resources\Auth\Policies; use Utopia\Migration\Resources\Auth\Team; @@ -754,53 +755,81 @@ private function exportAuthMethods(): void } /** - * Map of provider `$id` → fully-qualified migration Resource class. - * Each class extends `OAuth2Provider` and knows how to deserialize its - * provider's payload via `fromArray()`. + * Migration Resource classes for every OAuth2 provider exposed by + * `listOAuth2Providers`. The provider key (`'github'`, `'apple'`, …) + * lives only on the class via `OAuth2Provider::getProviderKey()` — + * `oauth2ClassFor()` lazily builds the key→class lookup. + * + * Adding a provider: drop one new file under Resources/Auth/OAuth2/ and + * append one line below. + * + * @var array> */ private const OAUTH2_PROVIDER_CLASSES = [ - 'amazon' => \Utopia\Migration\Resources\Auth\OAuth2\Amazon::class, - 'apple' => \Utopia\Migration\Resources\Auth\OAuth2\Apple::class, - 'auth0' => \Utopia\Migration\Resources\Auth\OAuth2\Auth0::class, - 'authentik' => \Utopia\Migration\Resources\Auth\OAuth2\Authentik::class, - 'autodesk' => \Utopia\Migration\Resources\Auth\OAuth2\Autodesk::class, - 'bitbucket' => \Utopia\Migration\Resources\Auth\OAuth2\Bitbucket::class, - 'bitly' => \Utopia\Migration\Resources\Auth\OAuth2\Bitly::class, - 'box' => \Utopia\Migration\Resources\Auth\OAuth2\Box::class, - 'dailymotion' => \Utopia\Migration\Resources\Auth\OAuth2\Dailymotion::class, - 'discord' => \Utopia\Migration\Resources\Auth\OAuth2\Discord::class, - 'disqus' => \Utopia\Migration\Resources\Auth\OAuth2\Disqus::class, - 'dropbox' => \Utopia\Migration\Resources\Auth\OAuth2\Dropbox::class, - 'etsy' => \Utopia\Migration\Resources\Auth\OAuth2\Etsy::class, - 'facebook' => \Utopia\Migration\Resources\Auth\OAuth2\Facebook::class, - 'figma' => \Utopia\Migration\Resources\Auth\OAuth2\Figma::class, - 'fusionauth' => \Utopia\Migration\Resources\Auth\OAuth2\FusionAuth::class, - 'github' => \Utopia\Migration\Resources\Auth\OAuth2\Github::class, - 'gitlab' => \Utopia\Migration\Resources\Auth\OAuth2\Gitlab::class, - 'google' => \Utopia\Migration\Resources\Auth\OAuth2\Google::class, - 'keycloak' => \Utopia\Migration\Resources\Auth\OAuth2\Keycloak::class, - 'kick' => \Utopia\Migration\Resources\Auth\OAuth2\Kick::class, - 'linkedin' => \Utopia\Migration\Resources\Auth\OAuth2\Linkedin::class, - 'microsoft' => \Utopia\Migration\Resources\Auth\OAuth2\Microsoft::class, - 'notion' => \Utopia\Migration\Resources\Auth\OAuth2\Notion::class, - 'oidc' => \Utopia\Migration\Resources\Auth\OAuth2\Oidc::class, - 'okta' => \Utopia\Migration\Resources\Auth\OAuth2\Okta::class, - 'paypal' => \Utopia\Migration\Resources\Auth\OAuth2\Paypal::class, - 'podio' => \Utopia\Migration\Resources\Auth\OAuth2\Podio::class, - 'salesforce' => \Utopia\Migration\Resources\Auth\OAuth2\Salesforce::class, - 'slack' => \Utopia\Migration\Resources\Auth\OAuth2\Slack::class, - 'spotify' => \Utopia\Migration\Resources\Auth\OAuth2\Spotify::class, - 'stripe' => \Utopia\Migration\Resources\Auth\OAuth2\Stripe::class, - 'tradeshift' => \Utopia\Migration\Resources\Auth\OAuth2\Tradeshift::class, - 'twitch' => \Utopia\Migration\Resources\Auth\OAuth2\Twitch::class, - 'wordpress' => \Utopia\Migration\Resources\Auth\OAuth2\Wordpress::class, - 'x' => \Utopia\Migration\Resources\Auth\OAuth2\X::class, - 'yahoo' => \Utopia\Migration\Resources\Auth\OAuth2\Yahoo::class, - 'yandex' => \Utopia\Migration\Resources\Auth\OAuth2\Yandex::class, - 'zoho' => \Utopia\Migration\Resources\Auth\OAuth2\Zoho::class, - 'zoom' => \Utopia\Migration\Resources\Auth\OAuth2\Zoom::class, + OAuth2\Amazon::class, + OAuth2\Apple::class, + OAuth2\Auth0::class, + OAuth2\Authentik::class, + OAuth2\Autodesk::class, + OAuth2\Bitbucket::class, + OAuth2\Bitly::class, + OAuth2\Box::class, + OAuth2\Dailymotion::class, + OAuth2\Discord::class, + OAuth2\Disqus::class, + OAuth2\Dropbox::class, + OAuth2\Etsy::class, + OAuth2\Facebook::class, + OAuth2\Figma::class, + OAuth2\FusionAuth::class, + OAuth2\Github::class, + OAuth2\Gitlab::class, + OAuth2\Google::class, + OAuth2\Keycloak::class, + OAuth2\Kick::class, + OAuth2\Linkedin::class, + OAuth2\Microsoft::class, + OAuth2\Notion::class, + OAuth2\Oidc::class, + OAuth2\Okta::class, + OAuth2\Paypal::class, + OAuth2\Podio::class, + OAuth2\Salesforce::class, + OAuth2\Slack::class, + OAuth2\Spotify::class, + OAuth2\Stripe::class, + OAuth2\Tradeshift::class, + OAuth2\Twitch::class, + OAuth2\Wordpress::class, + OAuth2\X::class, + OAuth2\Yahoo::class, + OAuth2\Yandex::class, + OAuth2\Zoho::class, + OAuth2\Zoom::class, ]; + /** @var array>|null */ + private static ?array $oauth2ClassByKey = null; + + /** + * Resolve a provider key (from the SDK list response's `$id`) to its + * Resource class. Returns `null` when the server lists a provider this + * lib has no class for yet (e.g. a newly added upstream provider). + * + * @return class-string|null + */ + private static function oauth2ClassFor(string $key): ?string + { + if (self::$oauth2ClassByKey === null) { + self::$oauth2ClassByKey = []; + foreach (self::OAUTH2_PROVIDER_CLASSES as $class) { + self::$oauth2ClassByKey[$class::getProviderKey()] = $class; + } + } + + return self::$oauth2ClassByKey[$key] ?? null; + } + /** * `listOAuth2Providers` returns a heterogeneous list — each entry is a * typed `OAuth2{Provider}` payload with provider-specific fields. We @@ -821,7 +850,7 @@ private function exportOAuth2Providers(): void continue; } - $class = self::OAUTH2_PROVIDER_CLASSES[$key] ?? null; + $class = self::oauth2ClassFor($key); if ($class === null) { // Server exposes a provider we don't have a Resource class for // yet (e.g. a new provider added upstream after this lib was From ce19cddc5e5cae015e91be25ea907c03a5e628f9 Mon Sep 17 00:00:00 2001 From: Prem Palanisamy Date: Sun, 31 May 2026 06:02:09 +0100 Subject: [PATCH 09/11] OAuth2: match sibling migration style for dispatch, report, and export - Destination: dispatch via explicit case Resource::TYPE_OAUTH2_PROVIDER instead of default + instanceof - Source: count in report() directly like sibling resources (drop try/catch), and move the in_array guard inside the export try - Harden mergeAppleSecret/mergeJsonSecret against non-array decoded JSON - Fix stale OAuth2Provider docblock (single shared TYPE, not per-subclass) --- src/Migration/Destinations/Appwrite.php | 13 +++++--- .../Resources/Auth/OAuth2/OAuth2Provider.php | 5 +++- src/Migration/Sources/Appwrite.php | 30 ++++++++----------- 3 files changed, 26 insertions(+), 22 deletions(-) diff --git a/src/Migration/Destinations/Appwrite.php b/src/Migration/Destinations/Appwrite.php index 93722602..3fc73015 100644 --- a/src/Migration/Destinations/Appwrite.php +++ b/src/Migration/Destinations/Appwrite.php @@ -2203,10 +2203,9 @@ public function importAuthResource(Resource $resource): Resource /** @var Policies $resource */ $this->createPolicies($resource); break; - default: - if ($resource instanceof OAuth2Provider) { - $this->createOAuth2Provider($resource); - } + case Resource::TYPE_OAUTH2_PROVIDER: + /** @var OAuth2Provider $resource */ + $this->createOAuth2Provider($resource); break; } @@ -3642,6 +3641,9 @@ protected function createOAuth2Provider(OAuth2Provider $resource): bool private function mergeAppleSecret(string $existing, string $keyId, string $teamId): string { $decoded = $existing === '' ? [] : (\json_decode($existing, true) ?: []); + if (!\is_array($decoded)) { + $decoded = []; + } if ($keyId !== '') { $decoded['keyID'] = $keyId; } @@ -3663,6 +3665,9 @@ private function mergeAppleSecret(string $existing, string $keyId, string $teamI private function mergeJsonSecret(string $existing, array $fields): string { $decoded = $existing === '' ? [] : (\json_decode($existing, true) ?: []); + if (!\is_array($decoded)) { + $decoded = []; + } foreach ($fields as $name => $value) { $decoded[$name] = $value; } diff --git a/src/Migration/Resources/Auth/OAuth2/OAuth2Provider.php b/src/Migration/Resources/Auth/OAuth2/OAuth2Provider.php index 324b267a..15a135a8 100644 --- a/src/Migration/Resources/Auth/OAuth2/OAuth2Provider.php +++ b/src/Migration/Resources/Auth/OAuth2/OAuth2Provider.php @@ -9,12 +9,15 @@ * Base class for per-provider OAuth2 migration resources. One concrete subclass * per provider id (Google, Apple, GitHub, …). Each subclass: * - * - Declares its own `Resource::TYPE_OAUTH2_*` type constant via getName() * - Carries the provider-specific non-secret fields readable from the source * (clientId/serviceId/endpoint/tenant/prompt/keyId/teamId/…) * - Leaves the actual secret (clientSecret / p8File) unmigrated — destination * admin must re-enter it post-migration * + * All subclasses share the single `Resource::TYPE_OAUTH2_PROVIDER` type (see + * getName() below); per-provider dispatch on the destination is by `instanceof` + * on the concrete subclass. + * * @phpstan-consistent-constructor */ abstract class OAuth2Provider extends Resource diff --git a/src/Migration/Sources/Appwrite.php b/src/Migration/Sources/Appwrite.php index 733559db..b635316d 100644 --- a/src/Migration/Sources/Appwrite.php +++ b/src/Migration/Sources/Appwrite.php @@ -397,13 +397,9 @@ private function reportAuth(array $resources, array &$report, array $resourceIds // OAuth2 providers — all 40 share one TYPE constant; the report // counts one entry per enabled provider on the source. if (\in_array(Resource::TYPE_OAUTH2_PROVIDER, $resources)) { - try { - $report[Resource::TYPE_OAUTH2_PROVIDER] = \count( - $this->project->listOAuth2Providers()->providers ?? [] - ); - } catch (\Throwable) { - $report[Resource::TYPE_OAUTH2_PROVIDER] = 0; - } + $report[Resource::TYPE_OAUTH2_PROVIDER] = \count( + $this->project->listOAuth2Providers()->providers ?? [] + ); } if (\in_array(Resource::TYPE_POLICIES, $resources)) { @@ -668,18 +664,18 @@ protected function exportGroupAuth(int $batchSize, array $resources): void // A single `listOAuth2Providers` call returns every provider; each one // is emitted as its own typed Resource (with shared TYPE_OAUTH2_PROVIDER). // Per-provider failures surface in `$this->errors[]`. - if (\in_array(Resource::TYPE_OAUTH2_PROVIDER, $resources)) { - try { + try { + if (\in_array(Resource::TYPE_OAUTH2_PROVIDER, $resources)) { $this->exportOAuth2Providers(); - } catch (\Throwable $e) { - $this->addError(new Exception( - Resource::TYPE_OAUTH2_PROVIDER, - Transfer::GROUP_AUTH, - message: $e->getMessage(), - code: (int) $e->getCode() ?: Exception::CODE_INTERNAL, - previous: $e - )); } + } catch (\Throwable $e) { + $this->addError(new Exception( + Resource::TYPE_OAUTH2_PROVIDER, + Transfer::GROUP_AUTH, + message: $e->getMessage(), + code: (int) $e->getCode() ?: Exception::CODE_INTERNAL, + previous: $e + )); } try { From d13d2d52785283f85be00018a2d7e740c524c5e5 Mon Sep 17 00:00:00 2001 From: Prem Palanisamy Date: Sun, 31 May 2026 08:55:25 +0100 Subject: [PATCH 10/11] OAuth2: DRY secret merge, surface unmapped providers, add tests - mergeAppleSecret now delegates to mergeJsonSecret (one merge implementation) - exportOAuth2Providers surfaces providers with no Resource class as non-fatal errors instead of dropping them silently - report() counts only migratable providers; fix the misleading enabled comment - use elseif for the mutually-exclusive provider-shape branches - add AppwriteOAuth2SecretTest (secret-merge) and OAuth2ProviderTransferTest (transfer round-trip via MockSource/MockDestination) --- src/Migration/Destinations/Appwrite.php | 19 ++- src/Migration/Sources/Appwrite.php | 30 +++-- .../Destinations/AppwriteOAuth2SecretTest.php | 109 ++++++++++++++++++ .../General/OAuth2ProviderTransferTest.php | 98 ++++++++++++++++ 4 files changed, 237 insertions(+), 19 deletions(-) create mode 100644 tests/Migration/Unit/Destinations/AppwriteOAuth2SecretTest.php create mode 100644 tests/Migration/Unit/General/OAuth2ProviderTransferTest.php diff --git a/src/Migration/Destinations/Appwrite.php b/src/Migration/Destinations/Appwrite.php index 3fc73015..1baeb8f9 100644 --- a/src/Migration/Destinations/Appwrite.php +++ b/src/Migration/Destinations/Appwrite.php @@ -3596,6 +3596,8 @@ protected function createOAuth2Provider(OAuth2Provider $resource): bool if ($resource->getClientId() !== '') { $oAuthProviders[$key . 'Appid'] = $resource->getClientId(); } + // A provider is at most one of these shapes — the per-shape extras + // (endpoint/tenant/prompt) are folded into the JSON secret blob. if ($resource instanceof OAuth2WithEndpoint && $resource->getEndpoint() !== '') { // Endpoint providers (Auth0/Authentik/FusionAuth/Gitlab/Keycloak/Okta/OIDC) // bundle the endpoint URL inside the JSON secret blob alongside @@ -3604,14 +3606,12 @@ protected function createOAuth2Provider(OAuth2Provider $resource): bool $oAuthProviders[$key . 'Secret'] ?? '', ['endpoint' => $resource->getEndpoint()], ); - } - if ($resource instanceof OAuth2Microsoft && $resource->getTenant() !== '') { + } elseif ($resource instanceof OAuth2Microsoft && $resource->getTenant() !== '') { $oAuthProviders[$key . 'Secret'] = $this->mergeJsonSecret( $oAuthProviders[$key . 'Secret'] ?? '', ['tenant' => $resource->getTenant()], ); - } - if ($resource instanceof OAuth2Google && !empty($resource->getPrompt())) { + } elseif ($resource instanceof OAuth2Google && !empty($resource->getPrompt())) { $oAuthProviders[$key . 'Secret'] = $this->mergeJsonSecret( $oAuthProviders[$key . 'Secret'] ?? '', ['prompt' => $resource->getPrompt()], @@ -3640,18 +3640,15 @@ protected function createOAuth2Provider(OAuth2Provider $resource): bool */ private function mergeAppleSecret(string $existing, string $keyId, string $teamId): string { - $decoded = $existing === '' ? [] : (\json_decode($existing, true) ?: []); - if (!\is_array($decoded)) { - $decoded = []; - } + $fields = []; if ($keyId !== '') { - $decoded['keyID'] = $keyId; + $fields['keyID'] = $keyId; } if ($teamId !== '') { - $decoded['teamID'] = $teamId; + $fields['teamID'] = $teamId; } - return \json_encode($decoded) ?: ''; + return $this->mergeJsonSecret($existing, $fields); } /** diff --git a/src/Migration/Sources/Appwrite.php b/src/Migration/Sources/Appwrite.php index b635316d..1e7a9ebc 100644 --- a/src/Migration/Sources/Appwrite.php +++ b/src/Migration/Sources/Appwrite.php @@ -394,12 +394,19 @@ private function reportAuth(array $resources, array &$report, array $resourceIds $report[Resource::TYPE_AUTH_METHODS] = 1; } - // OAuth2 providers — all 40 share one TYPE constant; the report - // counts one entry per enabled provider on the source. + // OAuth2 providers — every provider shares one TYPE constant. Count + // only the providers we can actually migrate (those with a matching + // Resource class), mirroring exportOAuth2Providers(); providers the + // source lists but this lib can't map are reported as errors there. if (\in_array(Resource::TYPE_OAUTH2_PROVIDER, $resources)) { - $report[Resource::TYPE_OAUTH2_PROVIDER] = \count( - $this->project->listOAuth2Providers()->providers ?? [] - ); + $count = 0; + foreach ($this->project->listOAuth2Providers()->providers ?? [] as $provider) { + $key = (string) ($provider['$id'] ?? ''); + if ($key !== '' && self::oauth2ClassFor($key) !== null) { + $count++; + } + } + $report[Resource::TYPE_OAUTH2_PROVIDER] = $count; } if (\in_array(Resource::TYPE_POLICIES, $resources)) { @@ -848,9 +855,16 @@ private function exportOAuth2Providers(): void $class = self::oauth2ClassFor($key); if ($class === null) { - // Server exposes a provider we don't have a Resource class for - // yet (e.g. a new provider added upstream after this lib was - // released). Skip silently — adding it is a one-file change. + // Server exposes a provider this lib has no Resource class for + // (e.g. one added upstream after this release). Surface it as a + // non-fatal error so it shows in the migration report instead of + // vanishing — adding coverage is a one-file change. + $this->addError(new Exception( + Resource::TYPE_OAUTH2_PROVIDER, + Transfer::GROUP_AUTH, + message: "No migration resource for OAuth2 provider '{$key}'; skipped.", + code: Exception::CODE_INTERNAL, + )); continue; } diff --git a/tests/Migration/Unit/Destinations/AppwriteOAuth2SecretTest.php b/tests/Migration/Unit/Destinations/AppwriteOAuth2SecretTest.php new file mode 100644 index 00000000..440f6b12 --- /dev/null +++ b/tests/Migration/Unit/Destinations/AppwriteOAuth2SecretTest.php @@ -0,0 +1,109 @@ +invoke('mergeAppleSecret', ['', 'KEY123', 'TEAM456']); + + $this->assertSame( + ['keyID' => 'KEY123', 'teamID' => 'TEAM456'], + \json_decode($merged, true), + ); + } + + public function testAppleSecretPreservesExistingP8(): void + { + $existing = \json_encode(['p8' => 'PRIVATE', 'keyID' => 'OLD']); + $merged = $this->invoke('mergeAppleSecret', [$existing, 'NEW', 'TEAM']); + + $this->assertSame( + ['p8' => 'PRIVATE', 'keyID' => 'NEW', 'teamID' => 'TEAM'], + \json_decode($merged, true), + 'Destination p8 must survive; migrated keyID/teamID overlay the rest.', + ); + } + + public function testAppleSecretSkipsBlankFields(): void + { + $existing = \json_encode(['p8' => 'PRIVATE']); + $merged = $this->invoke('mergeAppleSecret', [$existing, '', '']); + + $this->assertSame(['p8' => 'PRIVATE'], \json_decode($merged, true)); + } + + public function testAppleSecretTreatsNonArrayExistingAsEmpty(): void + { + // A malformed/scalar secret on the destination must not break the merge. + $merged = $this->invoke('mergeAppleSecret', ['"scalar"', 'KEY', 'TEAM']); + + $this->assertSame(['keyID' => 'KEY', 'teamID' => 'TEAM'], \json_decode($merged, true)); + } + + public function testJsonSecretMergesAndPreservesExistingKeys(): void + { + $existing = \json_encode(['clientSecret' => 'KEEP']); + $merged = $this->invoke('mergeJsonSecret', [$existing, ['tenant' => 'contoso']]); + + $this->assertSame( + ['clientSecret' => 'KEEP', 'tenant' => 'contoso'], + \json_decode($merged, true), + ); + } + + public function testJsonSecretTreatsNonArrayExistingAsEmpty(): void + { + $merged = $this->invoke('mergeJsonSecret', ['5', ['endpoint' => 'https://idp.example']]); + + $this->assertSame(['endpoint' => 'https://idp.example'], \json_decode($merged, true)); + } + + /** + * Build a destination with stubbed DB dependencies (the secret-merge + * helpers touch none of them) and invoke the private method by reflection, + * matching AppwriteDestinationDsnTest's approach. + * + * @param array $args + */ + private function invoke(string $method, array $args): string + { + $destination = new AppwriteDestination( + project: 'destination-project', + endpoint: 'http://example.test/v1', + key: 'test-key', + dbForProject: $this->createStub(UtopiaDatabase::class), + getDatabasesDB: fn (UtopiaDocument $database): UtopiaDatabase => $this->createStub(UtopiaDatabase::class), + collectionStructure: ['attributes' => [], 'indexes' => []], + dbForPlatform: $this->createStub(UtopiaDatabase::class), + projectInternalId: '1', + onDuplicate: OnDuplicate::Fail, + ); + + $reflection = (new ReflectionClass(AppwriteDestination::class))->getMethod($method); + /** @var string $value */ + $value = $reflection->invoke($destination, ...$args); + + return $value; + } +} diff --git a/tests/Migration/Unit/General/OAuth2ProviderTransferTest.php b/tests/Migration/Unit/General/OAuth2ProviderTransferTest.php new file mode 100644 index 00000000..af309518 --- /dev/null +++ b/tests/Migration/Unit/General/OAuth2ProviderTransferTest.php @@ -0,0 +1,98 @@ +source = new MockSource(); + $this->destination = new MockDestination(); + $this->transfer = new Transfer($this->source, $this->destination); + } + + public function testProvidersTransferUnderAuthGroupWithFieldsIntact(): void + { + $this->source->pushMockResource(Google::fromArray([ + 'id' => 'project-google', + 'enabled' => true, + 'clientId' => 'g-client', + 'prompt' => ['consent', 'select_account'], + ])); + $this->source->pushMockResource(Apple::fromArray([ + 'id' => 'project-apple', + 'enabled' => false, + 'serviceId' => 'svc', + 'keyId' => 'KEY', + 'teamId' => 'TEAM', + ])); + $this->source->pushMockResource(Microsoft::fromArray([ + 'id' => 'project-microsoft', + 'enabled' => true, + 'clientId' => 'm-client', + 'tenant' => 'contoso', + ])); + $this->source->pushMockResource(Github::fromArray([ + 'id' => 'project-github', + 'enabled' => true, + 'clientId' => 'gh-client', + ])); + + $this->transfer->run([Resource::TYPE_OAUTH2_PROVIDER], function () { + }); + + // All four land under the single shared type in the auth group. + $ids = $this->destination->getResourceTypeData(Transfer::GROUP_AUTH, Resource::TYPE_OAUTH2_PROVIDER); + $this->assertCount(4, $ids); + + /** @var Google $google */ + $google = $this->destination->getResourceById(Transfer::GROUP_AUTH, Resource::TYPE_OAUTH2_PROVIDER, 'project-google'); + $this->assertInstanceOf(Google::class, $google); + $this->assertSame('g-client', $google->getClientId()); + $this->assertSame(['consent', 'select_account'], $google->getPrompt()); + $this->assertTrue($google->getEnabled()); + + /** @var Apple $apple */ + $apple = $this->destination->getResourceById(Transfer::GROUP_AUTH, Resource::TYPE_OAUTH2_PROVIDER, 'project-apple'); + $this->assertInstanceOf(Apple::class, $apple); + $this->assertSame('svc', $apple->getServiceId()); + $this->assertSame('KEY', $apple->getKeyId()); + $this->assertSame('TEAM', $apple->getTeamId()); + $this->assertFalse($apple->getEnabled()); + + /** @var Microsoft $microsoft */ + $microsoft = $this->destination->getResourceById(Transfer::GROUP_AUTH, Resource::TYPE_OAUTH2_PROVIDER, 'project-microsoft'); + $this->assertInstanceOf(Microsoft::class, $microsoft); + $this->assertSame('m-client', $microsoft->getClientId()); + $this->assertSame('contoso', $microsoft->getTenant()); + + /** @var Github $github */ + $github = $this->destination->getResourceById(Transfer::GROUP_AUTH, Resource::TYPE_OAUTH2_PROVIDER, 'project-github'); + $this->assertInstanceOf(Github::class, $github); + $this->assertSame('gh-client', $github->getClientId()); + $this->assertSame(Transfer::GROUP_AUTH, $github->getGroup()); + } +} From 4168789ace53c97ba22c244b273def86a1d306da Mon Sep 17 00:00:00 2001 From: Prem Palanisamy Date: Sun, 31 May 2026 11:42:22 +0100 Subject: [PATCH 11/11] Drop OAuth2 tests to match the lib's existing migration-resource coverage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Other migration resources (auth methods, policies, …) ship no per-resource tests in this library; keep OAuth2 consistent with that baseline. --- .../Destinations/AppwriteOAuth2SecretTest.php | 109 ------------------ .../General/OAuth2ProviderTransferTest.php | 98 ---------------- 2 files changed, 207 deletions(-) delete mode 100644 tests/Migration/Unit/Destinations/AppwriteOAuth2SecretTest.php delete mode 100644 tests/Migration/Unit/General/OAuth2ProviderTransferTest.php diff --git a/tests/Migration/Unit/Destinations/AppwriteOAuth2SecretTest.php b/tests/Migration/Unit/Destinations/AppwriteOAuth2SecretTest.php deleted file mode 100644 index 440f6b12..00000000 --- a/tests/Migration/Unit/Destinations/AppwriteOAuth2SecretTest.php +++ /dev/null @@ -1,109 +0,0 @@ -invoke('mergeAppleSecret', ['', 'KEY123', 'TEAM456']); - - $this->assertSame( - ['keyID' => 'KEY123', 'teamID' => 'TEAM456'], - \json_decode($merged, true), - ); - } - - public function testAppleSecretPreservesExistingP8(): void - { - $existing = \json_encode(['p8' => 'PRIVATE', 'keyID' => 'OLD']); - $merged = $this->invoke('mergeAppleSecret', [$existing, 'NEW', 'TEAM']); - - $this->assertSame( - ['p8' => 'PRIVATE', 'keyID' => 'NEW', 'teamID' => 'TEAM'], - \json_decode($merged, true), - 'Destination p8 must survive; migrated keyID/teamID overlay the rest.', - ); - } - - public function testAppleSecretSkipsBlankFields(): void - { - $existing = \json_encode(['p8' => 'PRIVATE']); - $merged = $this->invoke('mergeAppleSecret', [$existing, '', '']); - - $this->assertSame(['p8' => 'PRIVATE'], \json_decode($merged, true)); - } - - public function testAppleSecretTreatsNonArrayExistingAsEmpty(): void - { - // A malformed/scalar secret on the destination must not break the merge. - $merged = $this->invoke('mergeAppleSecret', ['"scalar"', 'KEY', 'TEAM']); - - $this->assertSame(['keyID' => 'KEY', 'teamID' => 'TEAM'], \json_decode($merged, true)); - } - - public function testJsonSecretMergesAndPreservesExistingKeys(): void - { - $existing = \json_encode(['clientSecret' => 'KEEP']); - $merged = $this->invoke('mergeJsonSecret', [$existing, ['tenant' => 'contoso']]); - - $this->assertSame( - ['clientSecret' => 'KEEP', 'tenant' => 'contoso'], - \json_decode($merged, true), - ); - } - - public function testJsonSecretTreatsNonArrayExistingAsEmpty(): void - { - $merged = $this->invoke('mergeJsonSecret', ['5', ['endpoint' => 'https://idp.example']]); - - $this->assertSame(['endpoint' => 'https://idp.example'], \json_decode($merged, true)); - } - - /** - * Build a destination with stubbed DB dependencies (the secret-merge - * helpers touch none of them) and invoke the private method by reflection, - * matching AppwriteDestinationDsnTest's approach. - * - * @param array $args - */ - private function invoke(string $method, array $args): string - { - $destination = new AppwriteDestination( - project: 'destination-project', - endpoint: 'http://example.test/v1', - key: 'test-key', - dbForProject: $this->createStub(UtopiaDatabase::class), - getDatabasesDB: fn (UtopiaDocument $database): UtopiaDatabase => $this->createStub(UtopiaDatabase::class), - collectionStructure: ['attributes' => [], 'indexes' => []], - dbForPlatform: $this->createStub(UtopiaDatabase::class), - projectInternalId: '1', - onDuplicate: OnDuplicate::Fail, - ); - - $reflection = (new ReflectionClass(AppwriteDestination::class))->getMethod($method); - /** @var string $value */ - $value = $reflection->invoke($destination, ...$args); - - return $value; - } -} diff --git a/tests/Migration/Unit/General/OAuth2ProviderTransferTest.php b/tests/Migration/Unit/General/OAuth2ProviderTransferTest.php deleted file mode 100644 index af309518..00000000 --- a/tests/Migration/Unit/General/OAuth2ProviderTransferTest.php +++ /dev/null @@ -1,98 +0,0 @@ -source = new MockSource(); - $this->destination = new MockDestination(); - $this->transfer = new Transfer($this->source, $this->destination); - } - - public function testProvidersTransferUnderAuthGroupWithFieldsIntact(): void - { - $this->source->pushMockResource(Google::fromArray([ - 'id' => 'project-google', - 'enabled' => true, - 'clientId' => 'g-client', - 'prompt' => ['consent', 'select_account'], - ])); - $this->source->pushMockResource(Apple::fromArray([ - 'id' => 'project-apple', - 'enabled' => false, - 'serviceId' => 'svc', - 'keyId' => 'KEY', - 'teamId' => 'TEAM', - ])); - $this->source->pushMockResource(Microsoft::fromArray([ - 'id' => 'project-microsoft', - 'enabled' => true, - 'clientId' => 'm-client', - 'tenant' => 'contoso', - ])); - $this->source->pushMockResource(Github::fromArray([ - 'id' => 'project-github', - 'enabled' => true, - 'clientId' => 'gh-client', - ])); - - $this->transfer->run([Resource::TYPE_OAUTH2_PROVIDER], function () { - }); - - // All four land under the single shared type in the auth group. - $ids = $this->destination->getResourceTypeData(Transfer::GROUP_AUTH, Resource::TYPE_OAUTH2_PROVIDER); - $this->assertCount(4, $ids); - - /** @var Google $google */ - $google = $this->destination->getResourceById(Transfer::GROUP_AUTH, Resource::TYPE_OAUTH2_PROVIDER, 'project-google'); - $this->assertInstanceOf(Google::class, $google); - $this->assertSame('g-client', $google->getClientId()); - $this->assertSame(['consent', 'select_account'], $google->getPrompt()); - $this->assertTrue($google->getEnabled()); - - /** @var Apple $apple */ - $apple = $this->destination->getResourceById(Transfer::GROUP_AUTH, Resource::TYPE_OAUTH2_PROVIDER, 'project-apple'); - $this->assertInstanceOf(Apple::class, $apple); - $this->assertSame('svc', $apple->getServiceId()); - $this->assertSame('KEY', $apple->getKeyId()); - $this->assertSame('TEAM', $apple->getTeamId()); - $this->assertFalse($apple->getEnabled()); - - /** @var Microsoft $microsoft */ - $microsoft = $this->destination->getResourceById(Transfer::GROUP_AUTH, Resource::TYPE_OAUTH2_PROVIDER, 'project-microsoft'); - $this->assertInstanceOf(Microsoft::class, $microsoft); - $this->assertSame('m-client', $microsoft->getClientId()); - $this->assertSame('contoso', $microsoft->getTenant()); - - /** @var Github $github */ - $github = $this->destination->getResourceById(Transfer::GROUP_AUTH, Resource::TYPE_OAUTH2_PROVIDER, 'project-github'); - $this->assertInstanceOf(Github::class, $github); - $this->assertSame('gh-client', $github->getClientId()); - $this->assertSame(Transfer::GROUP_AUTH, $github->getGroup()); - } -}