Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 121 additions & 0 deletions src/Migration/Destinations/Appwrite.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +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\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;
Expand Down Expand Up @@ -260,6 +266,7 @@ public static function getSupportedResources(): array
Resource::TYPE_MEMBERSHIP,
Resource::TYPE_AUTH_METHODS,
Resource::TYPE_POLICIES,
Resource::TYPE_OAUTH2_PROVIDER,

// Database
Resource::TYPE_DATABASE,
Expand Down Expand Up @@ -2196,6 +2203,10 @@ public function importAuthResource(Resource $resource): Resource
/** @var Policies $resource */
$this->createPolicies($resource);
break;
case Resource::TYPE_OAUTH2_PROVIDER:
/** @var OAuth2Provider $resource */
$this->createOAuth2Provider($resource);
break;
}

$resource->setStatus(Resource::STATUS_SUCCESS);
Expand Down Expand Up @@ -3551,6 +3562,116 @@ protected function createAuthMethods(AuthMethods $resource): bool
return true;
}

/**
* 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, 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.
*
* 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
{
$key = $resource::getProviderKey();
$project = $this->dbForPlatform->getDocument('projects', $this->projectId);
$oAuthProviders = $project->getAttribute('oAuthProviders', []);

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();
}
// 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
// the client secret on the destination.
$oAuthProviders[$key . 'Secret'] = $this->mergeJsonSecret(
$oAuthProviders[$key . 'Secret'] ?? '',
['endpoint' => $resource->getEndpoint()],
);
} elseif ($resource instanceof OAuth2Microsoft && $resource->getTenant() !== '') {
$oAuthProviders[$key . 'Secret'] = $this->mergeJsonSecret(
$oAuthProviders[$key . 'Secret'] ?? '',
['tenant' => $resource->getTenant()],
);
} elseif ($resource instanceof OAuth2Google && !empty($resource->getPrompt())) {
$oAuthProviders[$key . 'Secret'] = $this->mergeJsonSecret(
$oAuthProviders[$key . 'Secret'] ?? '',
['prompt' => $resource->getPrompt()],
);
}
}

$oAuthProviders[$key . 'Enabled'] = $resource->getEnabled();

$this->dbForPlatform->getAuthorization()->skip(fn () => $this->dbForPlatform->updateDocument(
'projects',
$this->projectId,
new UtopiaDocument(['oAuthProviders' => $oAuthProviders]),
));

$this->dbForPlatform->purgeCachedDocument('projects', $this->projectId);

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
{
$fields = [];
if ($keyId !== '') {
$fields['keyID'] = $keyId;
}
if ($teamId !== '') {
$fields['teamID'] = $teamId;
}

return $this->mergeJsonSecret($existing, $fields);
}

/**
* 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<string, mixed> $fields
*/
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;
}

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
Expand Down
9 changes: 9 additions & 0 deletions src/Migration/Resource.php
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,14 @@ abstract class Resource implements \JsonSerializable

public const TYPE_POLICIES = 'policies';

// 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';

// Integrations
Expand Down Expand Up @@ -131,6 +139,7 @@ abstract class Resource implements \JsonSerializable
self::TYPE_MEMBERSHIP,
self::TYPE_AUTH_METHODS,
self::TYPE_POLICIES,
self::TYPE_OAUTH2_PROVIDER,
self::TYPE_PLATFORM,
self::TYPE_API_KEY,
self::TYPE_WEBHOOK,
Expand Down
11 changes: 11 additions & 0 deletions src/Migration/Resources/Auth/OAuth2/Amazon.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

namespace Utopia\Migration\Resources\Auth\OAuth2;

class Amazon extends StandardProvider
{
public static function getProviderKey(): string
{
return 'amazon';
}
}
75 changes: 75 additions & 0 deletions src/Migration/Resources/Auth/OAuth2/Apple.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<?php

namespace Utopia\Migration\Resources\Auth\OAuth2;

/**
* Apple OAuth2 provider. Bespoke shape — the credential is split across four
* fields. `serviceId`/`keyId`/`teamId` are readable on the source and
* migrated; `p8File` is write-only and must be re-entered on the destination.
*/
class Apple extends OAuth2Provider
{
public function __construct(
string $id,
bool $enabled,
private readonly string $serviceId = '',
private readonly string $keyId = '',
private readonly string $teamId = '',
string $createdAt = '',
string $updatedAt = '',
) {
parent::__construct($id, $enabled, $createdAt, $updatedAt);
}

/**
* @param array<string, mixed> $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<string, mixed>
*/
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 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;
}
}
11 changes: 11 additions & 0 deletions src/Migration/Resources/Auth/OAuth2/Auth0.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

namespace Utopia\Migration\Resources\Auth\OAuth2;

class Auth0 extends WithEndpointProvider
{
public static function getProviderKey(): string
{
return 'auth0';
}
}
11 changes: 11 additions & 0 deletions src/Migration/Resources/Auth/OAuth2/Authentik.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

namespace Utopia\Migration\Resources\Auth\OAuth2;

class Authentik extends WithEndpointProvider
{
public static function getProviderKey(): string
{
return 'authentik';
}
}
11 changes: 11 additions & 0 deletions src/Migration/Resources/Auth/OAuth2/Autodesk.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

namespace Utopia\Migration\Resources\Auth\OAuth2;

class Autodesk extends StandardProvider
{
public static function getProviderKey(): string
{
return 'autodesk';
}
}
11 changes: 11 additions & 0 deletions src/Migration/Resources/Auth/OAuth2/Bitbucket.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

namespace Utopia\Migration\Resources\Auth\OAuth2;

class Bitbucket extends StandardProvider
{
public static function getProviderKey(): string
{
return 'bitbucket';
}
}
11 changes: 11 additions & 0 deletions src/Migration/Resources/Auth/OAuth2/Bitly.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

namespace Utopia\Migration\Resources\Auth\OAuth2;

class Bitly extends StandardProvider
{
public static function getProviderKey(): string
{
return 'bitly';
}
}
11 changes: 11 additions & 0 deletions src/Migration/Resources/Auth/OAuth2/Box.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

namespace Utopia\Migration\Resources\Auth\OAuth2;

class Box extends StandardProvider
{
public static function getProviderKey(): string
{
return 'box';
}
}
11 changes: 11 additions & 0 deletions src/Migration/Resources/Auth/OAuth2/Dailymotion.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

namespace Utopia\Migration\Resources\Auth\OAuth2;

class Dailymotion extends StandardProvider
{
public static function getProviderKey(): string
{
return 'dailymotion';
}
}
11 changes: 11 additions & 0 deletions src/Migration/Resources/Auth/OAuth2/Discord.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

namespace Utopia\Migration\Resources\Auth\OAuth2;

class Discord extends StandardProvider
{
public static function getProviderKey(): string
{
return 'discord';
}
}
11 changes: 11 additions & 0 deletions src/Migration/Resources/Auth/OAuth2/Disqus.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

namespace Utopia\Migration\Resources\Auth\OAuth2;

class Disqus extends StandardProvider
{
public static function getProviderKey(): string
{
return 'disqus';
}
}
11 changes: 11 additions & 0 deletions src/Migration/Resources/Auth/OAuth2/Dropbox.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

namespace Utopia\Migration\Resources\Auth\OAuth2;

class Dropbox extends StandardProvider
{
public static function getProviderKey(): string
{
return 'dropbox';
}
}
11 changes: 11 additions & 0 deletions src/Migration/Resources/Auth/OAuth2/Etsy.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

namespace Utopia\Migration\Resources\Auth\OAuth2;

class Etsy extends StandardProvider
{
public static function getProviderKey(): string
{
return 'etsy';
}
}
Loading
Loading