mirror of
https://github.com/appwrite/appwrite.git
synced 2026-05-26 13:51:13 +00:00
Merge branch '1.9.x' into migrate-away-from-blacksmith-based-runners
This commit is contained in:
@@ -55,7 +55,7 @@ class Google extends OAuth2
|
||||
'state' => \json_encode($this->state),
|
||||
'response_type' => 'code',
|
||||
'access_type' => 'offline',
|
||||
'prompt' => 'consent'
|
||||
'prompt' => $this->getPrompt()
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -190,6 +190,23 @@ class Google extends OAuth2
|
||||
return $secret['clientSecret'] ?? $this->appSecret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the prompt values from the JSON stored in appSecret
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function getPrompt(): string
|
||||
{
|
||||
$secret = $this->getAppSecret();
|
||||
$prompt = $secret['prompt'] ?? [];
|
||||
|
||||
if (empty($prompt)) {
|
||||
$prompt = ['consent'];
|
||||
}
|
||||
|
||||
return \implode(' ', $prompt);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode the JSON stored in appSecret.
|
||||
* Falls back to treating the raw string as the client secret for backwards compatibility.
|
||||
|
||||
@@ -3,8 +3,22 @@
|
||||
namespace Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Google;
|
||||
|
||||
use Appwrite\Auth\OAuth2\Google;
|
||||
use Appwrite\Event\Event as QueueEvent;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Platform\Action;
|
||||
use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Base;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Validator\ArrayList;
|
||||
use Utopia\Validator\Boolean;
|
||||
use Utopia\Validator\Nullable;
|
||||
use Utopia\Validator\Text;
|
||||
use Utopia\Validator\WhiteList;
|
||||
|
||||
class Update extends Base
|
||||
{
|
||||
@@ -52,4 +66,118 @@ class Update extends Base
|
||||
{
|
||||
return 'GOCSPX-2k8gsR0000000000000000VNahJj';
|
||||
}
|
||||
|
||||
public static function getParameters(): array
|
||||
{
|
||||
return \array_merge(parent::getParameters(), [
|
||||
[
|
||||
'$id' => 'prompt',
|
||||
'name' => 'Prompt',
|
||||
'example' => '["consent"]',
|
||||
'hint' => '',
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$providerId = static::getProviderId();
|
||||
$providerLabel = static::getProviderLabel();
|
||||
|
||||
$this
|
||||
->setHttpMethod(Action::HTTP_REQUEST_METHOD_PATCH)
|
||||
->setHttpPath('/v1/project/oauth2/' . $providerId)
|
||||
->desc('Update project OAuth2 ' . $providerLabel)
|
||||
->groups(['api', 'project'])
|
||||
->label('scope', 'oauth2.write')
|
||||
->label('event', 'oauth2.[providerId].update')
|
||||
->label('audits.event', 'project.oauth2.[providerId].update')
|
||||
->label('audits.resource', 'project.oauth2/{response.$id}')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'project',
|
||||
group: 'oauth2',
|
||||
name: static::getProviderSDKMethod(),
|
||||
description: 'Update the project OAuth2 ' . $providerLabel . ' configuration.',
|
||||
auth: [AuthType::ADMIN, AuthType::KEY],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_OK,
|
||||
model: static::getResponseModel(),
|
||||
)
|
||||
],
|
||||
))
|
||||
->param(static::getClientIdParamName(), null, new Nullable(new Text(256, 0)), static::getClientIdDescription(), optional: true)
|
||||
->param(static::getClientSecretParamName(), null, new Nullable(new Text(512, 0)), static::getClientSecretDescription(), optional: true)
|
||||
->param('prompt', null, new Nullable(new ArrayList(new WhiteList(['none', 'consent', 'select_account'], true), 3)), 'Array of Google OAuth2 prompt values. If "none" is included, it must be the only element. "none" means: don\'t display any authentication or consent screens. Must not be specified with other values. "consent" means: prompt the user for consent. "select_account" means: prompt the user to select an account.', optional: true)
|
||||
->param('enabled', null, new Nullable(new Boolean()), 'OAuth2 sign-in method status. Set to true to enable new session creation. Setting to true will trigger end-to-end credentials validation, and will throw if the credentials are invalid.', true)
|
||||
->inject('response')
|
||||
->inject('dbForPlatform')
|
||||
->inject('project')
|
||||
->inject('authorization')
|
||||
->inject('queueForEvents')
|
||||
->callback($this->handle(...));
|
||||
}
|
||||
|
||||
public function buildReadResponse(Document $project): Document
|
||||
{
|
||||
$providerId = static::getProviderId();
|
||||
$oAuthProviders = $project->getAttribute('oAuthProviders', []);
|
||||
$decoded = $this->decodeStoredSecret($project);
|
||||
|
||||
return new Document([
|
||||
'$id' => $providerId,
|
||||
'enabled' => $oAuthProviders[$providerId . 'Enabled'] ?? false,
|
||||
static::getClientIdParamName() => $oAuthProviders[$providerId . 'Appid'] ?? '',
|
||||
static::getClientSecretParamName() => '',
|
||||
'prompt' => $decoded['prompt'] ?? ['consent'],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom callback used instead of the parent's `action()` because Google
|
||||
* takes an additional optional `prompt` parameter. The method is named
|
||||
* differently to avoid an LSP-incompatible override of Base::action().
|
||||
*/
|
||||
public function handle(
|
||||
?string $clientId,
|
||||
?string $clientSecret,
|
||||
?array $prompt,
|
||||
?bool $enabled,
|
||||
Response $response,
|
||||
Database $dbForPlatform,
|
||||
Document $project,
|
||||
Authorization $authorization,
|
||||
QueueEvent $queueForEvents
|
||||
): void {
|
||||
$providerId = static::getProviderId();
|
||||
$queueForEvents->setParam('providerId', $providerId);
|
||||
|
||||
if ($prompt !== null) {
|
||||
if (empty($prompt)) {
|
||||
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Prompt array cannot be empty.');
|
||||
}
|
||||
|
||||
if (\in_array('none', $prompt) && \count($prompt) > 1) {
|
||||
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'When "none" is used as a prompt value, it must be the only element in the array.');
|
||||
}
|
||||
}
|
||||
|
||||
$storedRaw = $project->getAttribute('oAuthProviders', [])[$providerId . 'Secret'] ?? '';
|
||||
$existing = $this->decodeStoredSecret($project);
|
||||
|
||||
// Backwards compatibility: secrets stored before the prompt feature
|
||||
// were saved as plain strings. Treat the raw value as clientSecret.
|
||||
if (!empty($storedRaw) && empty($existing)) {
|
||||
$existing = ['clientSecret' => $storedRaw];
|
||||
}
|
||||
|
||||
$encodedSecret = \json_encode([
|
||||
'clientSecret' => $clientSecret ?? ($existing['clientSecret'] ?? ''),
|
||||
'prompt' => $prompt ?? ($existing['prompt'] ?? ['consent']),
|
||||
]);
|
||||
|
||||
$project = $this->persistCredentials($project, $dbForPlatform, $authorization, $clientId, $encodedSecret, $enabled);
|
||||
|
||||
$response->dynamic($this->buildReadResponse($project), static::getResponseModel());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,20 @@ class OAuth2Google extends OAuth2Base
|
||||
return 'GOCSPX-2k8gsR0000000000000000VNahJj';
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
|
||||
$this->addRule('prompt', [
|
||||
'type' => self::TYPE_ENUM,
|
||||
'description' => 'Google OAuth2 prompt values.',
|
||||
'default' => ['consent'],
|
||||
'example' => ['consent'],
|
||||
'array' => true,
|
||||
'enum' => ['none', 'consent', 'select_account'],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Name
|
||||
*
|
||||
|
||||
@@ -2564,6 +2564,159 @@ trait OAuth2Base
|
||||
]);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Update Google (clientId + clientSecret + optional prompt)
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Default prompt MUST run before any other Google test that sets a custom
|
||||
* prompt value. The global resetProjectOAuth2() only clears Amazon state,
|
||||
* so Google state leaks across tests in the same class. Running this first
|
||||
* guarantees the stored JSON blob has no pre-existing "prompt" key.
|
||||
*/
|
||||
public function testUpdateOAuth2GoogleDefaultPrompt(): void
|
||||
{
|
||||
// When prompt is omitted and nothing is stored, the default is ['consent'].
|
||||
$response = $this->updateOAuth2('google', [
|
||||
'clientId' => 'google-default-client',
|
||||
'clientSecret' => 'google-default-secret',
|
||||
'enabled' => false,
|
||||
]);
|
||||
|
||||
$this->assertSame(200, $response['headers']['status-code']);
|
||||
$this->assertSame(['consent'], $response['body']['prompt']);
|
||||
|
||||
// Cleanup
|
||||
$this->updateOAuth2('google', [
|
||||
'clientId' => '',
|
||||
'clientSecret' => '',
|
||||
'enabled' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
public function testUpdateOAuth2Google(): void
|
||||
{
|
||||
$response = $this->updateOAuth2('google', [
|
||||
'clientId' => '120000000095-92ifjb00000000000000000000g7ijfb.apps.googleusercontent.com',
|
||||
'clientSecret' => 'GOCSPX-2k8gsR0000000000000000VNahJj',
|
||||
'prompt' => ['select_account'],
|
||||
'enabled' => false,
|
||||
]);
|
||||
|
||||
$this->assertSame(200, $response['headers']['status-code']);
|
||||
$this->assertSame('google', $response['body']['$id']);
|
||||
$this->assertSame('120000000095-92ifjb00000000000000000000g7ijfb.apps.googleusercontent.com', $response['body']['clientId']);
|
||||
$this->assertSame(['select_account'], $response['body']['prompt']);
|
||||
|
||||
// Cleanup
|
||||
$this->updateOAuth2('google', [
|
||||
'clientId' => '',
|
||||
'clientSecret' => '',
|
||||
'enabled' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
public function testUpdateOAuth2GooglePartialPreservesPrompt(): void
|
||||
{
|
||||
// Seed clientSecret + prompt.
|
||||
$this->updateOAuth2('google', [
|
||||
'clientId' => 'google-seed-client',
|
||||
'clientSecret' => 'google-seed-secret',
|
||||
'prompt' => ['consent', 'select_account'],
|
||||
'enabled' => false,
|
||||
]);
|
||||
|
||||
// Update only clientId.
|
||||
$response = $this->updateOAuth2('google', [
|
||||
'clientId' => 'google-rotated-client',
|
||||
]);
|
||||
|
||||
$this->assertSame(200, $response['headers']['status-code']);
|
||||
$this->assertSame('google-rotated-client', $response['body']['clientId']);
|
||||
$this->assertSame(['consent', 'select_account'], $response['body']['prompt']);
|
||||
|
||||
// Cleanup
|
||||
$this->updateOAuth2('google', [
|
||||
'clientId' => '',
|
||||
'clientSecret' => '',
|
||||
'enabled' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
public function testUpdateOAuth2GooglePromptNoneAloneRejected(): void
|
||||
{
|
||||
$response = $this->updateOAuth2('google', [
|
||||
'clientId' => 'whatever',
|
||||
'clientSecret' => 'whatever',
|
||||
'prompt' => ['none', 'consent'],
|
||||
'enabled' => false,
|
||||
]);
|
||||
|
||||
$this->assertSame(400, $response['headers']['status-code']);
|
||||
$this->assertSame('general_argument_invalid', $response['body']['type']);
|
||||
}
|
||||
|
||||
public function testUpdateOAuth2GooglePromptEmptyArrayRejected(): void
|
||||
{
|
||||
$response = $this->updateOAuth2('google', [
|
||||
'clientId' => 'whatever',
|
||||
'clientSecret' => 'whatever',
|
||||
'prompt' => [],
|
||||
'enabled' => false,
|
||||
]);
|
||||
|
||||
$this->assertSame(400, $response['headers']['status-code']);
|
||||
$this->assertSame('general_argument_invalid', $response['body']['type']);
|
||||
}
|
||||
|
||||
public function testUpdateOAuth2GooglePromptNoneAloneAccepted(): void
|
||||
{
|
||||
$response = $this->updateOAuth2('google', [
|
||||
'clientId' => '120000000095-92ifjb00000000000000000000g7ijfb.apps.googleusercontent.com',
|
||||
'clientSecret' => 'GOCSPX-2k8gsR0000000000000000VNahJj',
|
||||
'prompt' => ['none'],
|
||||
'enabled' => false,
|
||||
]);
|
||||
|
||||
$this->assertSame(200, $response['headers']['status-code']);
|
||||
$this->assertSame(['none'], $response['body']['prompt']);
|
||||
|
||||
// Cleanup
|
||||
$this->updateOAuth2('google', [
|
||||
'clientId' => '',
|
||||
'clientSecret' => '',
|
||||
'enabled' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
public function testUpdateOAuth2GoogleEnableAndReadBack(): void
|
||||
{
|
||||
$update = $this->updateOAuth2('google', [
|
||||
'clientId' => 'google-enable-client',
|
||||
'clientSecret' => 'google-enable-secret',
|
||||
'prompt' => ['select_account'],
|
||||
'enabled' => true,
|
||||
]);
|
||||
|
||||
$this->assertSame(200, $update['headers']['status-code']);
|
||||
$this->assertTrue($update['body']['enabled']);
|
||||
|
||||
// GET must hide clientSecret while keeping clientId and prompt.
|
||||
$get = $this->getOAuth2Provider('google');
|
||||
$this->assertSame(200, $get['headers']['status-code']);
|
||||
$this->assertTrue($get['body']['enabled']);
|
||||
$this->assertSame('google-enable-client', $get['body']['clientId']);
|
||||
$this->assertSame(['select_account'], $get['body']['prompt']);
|
||||
$this->assertSame('', $get['body']['clientSecret']);
|
||||
|
||||
// Cleanup
|
||||
$this->updateOAuth2('google', [
|
||||
'clientId' => '',
|
||||
'clientSecret' => '',
|
||||
'enabled' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Smoke test: every plain (clientId + clientSecret) provider
|
||||
//
|
||||
|
||||
Reference in New Issue
Block a user