mirror of
https://github.com/appwrite/appwrite.git
synced 2026-05-26 13:51:13 +00:00
Merge remote-tracking branch 'origin/1.9.x' into chore/remove-utopia-logger-sync
# Conflicts: # composer.lock
This commit is contained in:
+1
-1
@@ -12,7 +12,7 @@ RUN composer install --ignore-platform-reqs --optimize-autoloader \
|
||||
--no-plugins --no-scripts --prefer-dist \
|
||||
`if [ "$TESTING" != "true" ]; then echo "--no-dev"; fi`
|
||||
|
||||
FROM appwrite/base:1.2.1 AS base
|
||||
FROM appwrite/base:1.4.1 AS base
|
||||
|
||||
LABEL maintainer="team@appwrite.io"
|
||||
|
||||
|
||||
@@ -215,6 +215,16 @@ return [
|
||||
'description' => 'Access to create function executions',
|
||||
'category' => 'Functions',
|
||||
],
|
||||
'execution.read' => [
|
||||
'description' => 'Access to read function executions. This scope is deprecated for consistency purposes, and replaced by `executions.read`.',
|
||||
'category' => 'Functions',
|
||||
'deprecated' => true,
|
||||
],
|
||||
'execution.write' => [
|
||||
'description' => 'Access to create function executions. This scope is deprecated for consistency purposes, and replaced by `executions.write`.',
|
||||
'category' => 'Functions',
|
||||
'deprecated' => true,
|
||||
],
|
||||
|
||||
// Sites
|
||||
'sites.read' => [
|
||||
|
||||
@@ -79,12 +79,13 @@ return [
|
||||
...getRuntimes($templateRuntimes['DENO'], 'deno cache src/main.ts', 'src/main.ts', 'deno/starter', $allowList),
|
||||
...getRuntimes($templateRuntimes['BUN'], 'bun install', 'src/main.ts', 'bun/starter', $allowList),
|
||||
...getRuntimes($templateRuntimes['RUBY'], 'bundle install', 'lib/main.rb', 'ruby/starter', $allowList),
|
||||
...getRuntimes($templateRuntimes['RUST'], '', 'main.rs', 'rust/starter', $allowList),
|
||||
],
|
||||
'instructions' => 'For documentation and instructions check out <a target="_blank" rel="noopener noreferrer" class="link" href="https://github.com/appwrite/templates/tree/main/node/starter">file</a>.',
|
||||
'instructions' => 'For documentation and instructions check out the <a target="_blank" rel="noopener noreferrer" class="link" href="https://github.com/appwrite/templates">templates repository</a>.',
|
||||
'vcsProvider' => 'github',
|
||||
'providerRepositoryId' => 'templates',
|
||||
'providerOwner' => 'appwrite',
|
||||
'providerVersion' => '0.2.*',
|
||||
'providerVersion' => '0.3.*',
|
||||
'variables' => [],
|
||||
'scopes' => ['users.read']
|
||||
],
|
||||
|
||||
+1
-1
@@ -49,7 +49,7 @@
|
||||
"ext-openssl": "*",
|
||||
"ext-zlib": "*",
|
||||
"ext-sockets": "*",
|
||||
"appwrite/php-runtimes": "0.19.*",
|
||||
"appwrite/php-runtimes": "0.20.*",
|
||||
"appwrite/php-clamav": "2.0.*",
|
||||
"utopia-php/abuse": "1.2.*",
|
||||
"utopia-php/agents": "1.2.*",
|
||||
|
||||
Generated
+7
-7
@@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "788e59ef0f1f12947b5c3700f905c635",
|
||||
"content-hash": "34190862fe85c76d5325fd6b5dea8f90",
|
||||
"packages": [
|
||||
{
|
||||
"name": "adhocore/jwt",
|
||||
@@ -161,16 +161,16 @@
|
||||
},
|
||||
{
|
||||
"name": "appwrite/php-runtimes",
|
||||
"version": "0.19.5",
|
||||
"version": "0.20.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/appwrite/runtimes.git",
|
||||
"reference": "aa2f7760cd0493c0880209b92df812c9386b3546"
|
||||
"reference": "7d9b7f4eef5c0a142a60907b06de2219d025c5c3"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/appwrite/runtimes/zipball/aa2f7760cd0493c0880209b92df812c9386b3546",
|
||||
"reference": "aa2f7760cd0493c0880209b92df812c9386b3546",
|
||||
"url": "https://api.github.com/repos/appwrite/runtimes/zipball/7d9b7f4eef5c0a142a60907b06de2219d025c5c3",
|
||||
"reference": "7d9b7f4eef5c0a142a60907b06de2219d025c5c3",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -210,9 +210,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/appwrite/runtimes/issues",
|
||||
"source": "https://github.com/appwrite/runtimes/tree/0.19.5"
|
||||
"source": "https://github.com/appwrite/runtimes/tree/0.20.0"
|
||||
},
|
||||
"time": "2026-04-01T01:39:23+00:00"
|
||||
"time": "2026-05-01T07:47:07+00:00"
|
||||
},
|
||||
{
|
||||
"name": "brick/math",
|
||||
|
||||
@@ -37,6 +37,13 @@ class Authentik extends OAuth2
|
||||
return 'authentik';
|
||||
}
|
||||
|
||||
public function verifyCredentials(): void
|
||||
{
|
||||
if (empty($this->getAuthentikDomain())) {
|
||||
throw new \Exception('Authentik endpoint is required.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
|
||||
@@ -37,6 +37,13 @@ class FusionAuth extends OAuth2
|
||||
return 'fusionauth';
|
||||
}
|
||||
|
||||
public function verifyCredentials(): void
|
||||
{
|
||||
if (empty($this->getFusionAuthDomain())) {
|
||||
throw new \Exception('FusionAuth endpoint is required.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
|
||||
@@ -37,6 +37,17 @@ class Keycloak extends OAuth2
|
||||
return 'keycloak';
|
||||
}
|
||||
|
||||
public function verifyCredentials(): void
|
||||
{
|
||||
if (empty($this->getKeycloakDomain())) {
|
||||
throw new \Exception('Keycloak endpoint is required.');
|
||||
}
|
||||
|
||||
if (empty($this->getKeycloakRealm())) {
|
||||
throw new \Exception('Keycloak realm name is required.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
|
||||
@@ -36,6 +36,13 @@ class Microsoft extends OAuth2
|
||||
return 'microsoft';
|
||||
}
|
||||
|
||||
public function verifyCredentials(): void
|
||||
{
|
||||
if (empty($this->getTenantID())) {
|
||||
throw new \Exception('Microsoft tenant is required.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
@@ -201,7 +208,7 @@ class Microsoft extends OAuth2
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the Tenant Id from the JSON stored in appSecret. Defaults to 'common' as a fallback
|
||||
* Extracts the Tenant Id from the JSON stored in appSecret.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
@@ -209,6 +216,6 @@ class Microsoft extends OAuth2
|
||||
{
|
||||
$secret = $this->getAppSecret();
|
||||
|
||||
return $secret['tenantID'] ?? 'common';
|
||||
return $secret['tenantID'] ?? '';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,7 +105,7 @@ class Update extends Base
|
||||
))
|
||||
->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('endpoint', '', new Text(256, 1), 'Domain of Authentik instance. For example: example.authentik.com', optional: false)
|
||||
->param('endpoint', null, new Nullable(new Text(256, 0)), 'Domain of Authentik instance. For example: example.authentik.com', 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')
|
||||
@@ -138,7 +138,7 @@ class Update extends Base
|
||||
public function handle(
|
||||
?string $clientId,
|
||||
?string $clientSecret,
|
||||
string $endpoint,
|
||||
?string $endpoint,
|
||||
?bool $enabled,
|
||||
Response $response,
|
||||
Database $dbForPlatform,
|
||||
@@ -151,7 +151,7 @@ class Update extends Base
|
||||
|
||||
// The secret is stored as JSON `{"clientSecret": "...", "authentikDomain": "..."}`
|
||||
// to match the shape Authentik's OAuth2 adapter expects (getAuthentikDomain()).
|
||||
// The `endpoint` param is required on every call, so it's always written.
|
||||
// The `endpoint` param is optional; if omitted, the existing stored endpoint is preserved.
|
||||
// `clientSecret` is optional; if omitted, the existing stored secret is preserved.
|
||||
$storedRaw = $project->getAttribute('oAuthProviders', [])[$providerId . 'Secret'] ?? '';
|
||||
$existing = [];
|
||||
@@ -160,7 +160,7 @@ class Update extends Base
|
||||
}
|
||||
$encodedSecret = \json_encode([
|
||||
'clientSecret' => $clientSecret ?? ($existing['clientSecret'] ?? ''),
|
||||
'authentikDomain' => $endpoint,
|
||||
'authentikDomain' => $endpoint ?? ($existing['authentikDomain'] ?? ''),
|
||||
]);
|
||||
|
||||
$project = $this->persistCredentials($project, $dbForPlatform, $authorization, $clientId, $encodedSecret, $enabled);
|
||||
|
||||
@@ -105,7 +105,7 @@ class Update extends Base
|
||||
))
|
||||
->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('endpoint', '', new Text(256, 1), 'Domain of FusionAuth instance. For example: example.fusionauth.io', optional: false)
|
||||
->param('endpoint', null, new Nullable(new Text(256, 0)), 'Domain of FusionAuth instance. For example: example.fusionauth.io', 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')
|
||||
@@ -138,7 +138,7 @@ class Update extends Base
|
||||
public function handle(
|
||||
?string $clientId,
|
||||
?string $clientSecret,
|
||||
string $endpoint,
|
||||
?string $endpoint,
|
||||
?bool $enabled,
|
||||
Response $response,
|
||||
Database $dbForPlatform,
|
||||
@@ -151,7 +151,7 @@ class Update extends Base
|
||||
|
||||
// The secret is stored as JSON `{"clientSecret": "...", "fusionAuthDomain": "..."}`
|
||||
// to match the shape FusionAuth's OAuth2 adapter expects (getFusionAuthDomain()).
|
||||
// The `endpoint` param is required on every call, so it's always written.
|
||||
// The `endpoint` param is optional; if omitted, the existing stored endpoint is preserved.
|
||||
// `clientSecret` is optional; if omitted, the existing stored secret is preserved.
|
||||
$storedRaw = $project->getAttribute('oAuthProviders', [])[$providerId . 'Secret'] ?? '';
|
||||
$existing = [];
|
||||
@@ -160,7 +160,7 @@ class Update extends Base
|
||||
}
|
||||
$encodedSecret = \json_encode([
|
||||
'clientSecret' => $clientSecret ?? ($existing['clientSecret'] ?? ''),
|
||||
'fusionAuthDomain' => $endpoint,
|
||||
'fusionAuthDomain' => $endpoint ?? ($existing['fusionAuthDomain'] ?? ''),
|
||||
]);
|
||||
|
||||
$project = $this->persistCredentials($project, $dbForPlatform, $authorization, $clientId, $encodedSecret, $enabled);
|
||||
|
||||
@@ -35,7 +35,7 @@ class Update extends Base
|
||||
|
||||
public static function getClientIdName(): string
|
||||
{
|
||||
return 'OAuth 2 app Client ID, or App ID';
|
||||
return 'OAuth2 app Client ID, or App ID';
|
||||
}
|
||||
|
||||
public static function getClientIdExample(): string
|
||||
|
||||
@@ -111,8 +111,8 @@ class Update extends Base
|
||||
))
|
||||
->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('endpoint', '', new Text(256, 1), 'Domain of Keycloak instance. For example: keycloak.example.com', optional: false)
|
||||
->param('realmName', '', new Text(256, 1), 'Keycloak realm name. For example: appwrite-realm', optional: false)
|
||||
->param('endpoint', null, new Nullable(new Text(256, 0)), 'Domain of Keycloak instance. For example: keycloak.example.com', optional: true)
|
||||
->param('realmName', null, new Nullable(new Text(256, 0)), 'Keycloak realm name. For example: appwrite-realm', 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')
|
||||
@@ -147,8 +147,8 @@ class Update extends Base
|
||||
public function handle(
|
||||
?string $clientId,
|
||||
?string $clientSecret,
|
||||
string $endpoint,
|
||||
string $realmName,
|
||||
?string $endpoint,
|
||||
?string $realmName,
|
||||
?bool $enabled,
|
||||
Response $response,
|
||||
Database $dbForPlatform,
|
||||
@@ -161,7 +161,7 @@ class Update extends Base
|
||||
|
||||
// The secret is stored as JSON `{"clientSecret": "...", "keycloakDomain": "...", "keycloakRealm": "..."}`
|
||||
// to match the shape Keycloak's OAuth2 adapter expects (getKeycloakDomain(), getKeycloakRealm()).
|
||||
// The `endpoint` and `realmName` params are required on every call, so they're always written.
|
||||
// The `endpoint` and `realmName` params are optional; if omitted, existing stored values are preserved.
|
||||
// `clientSecret` is optional; if omitted, the existing stored secret is preserved.
|
||||
$storedRaw = $project->getAttribute('oAuthProviders', [])[$providerId . 'Secret'] ?? '';
|
||||
$existing = [];
|
||||
@@ -170,8 +170,8 @@ class Update extends Base
|
||||
}
|
||||
$encodedSecret = \json_encode([
|
||||
'clientSecret' => $clientSecret ?? ($existing['clientSecret'] ?? ''),
|
||||
'keycloakDomain' => $endpoint,
|
||||
'keycloakRealm' => $realmName,
|
||||
'keycloakDomain' => $endpoint ?? ($existing['keycloakDomain'] ?? ''),
|
||||
'keycloakRealm' => $realmName ?? ($existing['keycloakRealm'] ?? ''),
|
||||
]);
|
||||
|
||||
$project = $this->persistCredentials($project, $dbForPlatform, $authorization, $clientId, $encodedSecret, $enabled);
|
||||
|
||||
@@ -115,7 +115,7 @@ class Update extends Base
|
||||
))
|
||||
->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('tenant', '', new Text(256, 1), 'Microsoft Entra ID tenant identifier. Use \'common\', \'organizations\', \'consumers\' or a specific tenant ID. For example: common', optional: false)
|
||||
->param('tenant', null, new Nullable(new Text(256, 0)), 'Microsoft Entra ID tenant identifier. Use \'common\', \'organizations\', \'consumers\' or a specific tenant ID. For example: common', 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')
|
||||
@@ -148,7 +148,7 @@ class Update extends Base
|
||||
public function handle(
|
||||
?string $applicationId,
|
||||
?string $applicationSecret,
|
||||
string $tenant,
|
||||
?string $tenant,
|
||||
?bool $enabled,
|
||||
Response $response,
|
||||
Database $dbForPlatform,
|
||||
@@ -161,7 +161,7 @@ class Update extends Base
|
||||
|
||||
// The secret is stored as JSON `{"clientSecret": "...", "tenantID": "..."}`
|
||||
// to match the shape Microsoft's OAuth2 adapter expects (getTenantID()).
|
||||
// The `tenant` param is required on every call, so it's always written.
|
||||
// The `tenant` param is optional; if omitted, the existing stored tenant is preserved.
|
||||
// `applicationSecret` is optional; if omitted, the existing stored secret is preserved.
|
||||
$storedRaw = $project->getAttribute('oAuthProviders', [])[$providerId . 'Secret'] ?? '';
|
||||
$existing = [];
|
||||
@@ -170,7 +170,7 @@ class Update extends Base
|
||||
}
|
||||
$encodedSecret = \json_encode([
|
||||
'clientSecret' => $applicationSecret ?? ($existing['clientSecret'] ?? ''),
|
||||
'tenantID' => $tenant,
|
||||
'tenantID' => $tenant ?? ($existing['tenantID'] ?? ''),
|
||||
]);
|
||||
|
||||
$project = $this->persistCredentials($project, $dbForPlatform, $authorization, $applicationId, $encodedSecret, $enabled);
|
||||
|
||||
@@ -101,7 +101,7 @@ class ConsoleConsoleClientTest extends Scope
|
||||
$this->assertCount(2, $github['parameters']);
|
||||
$clientId = $github['parameters'][0];
|
||||
$this->assertEquals('clientId', $clientId['$id']);
|
||||
$this->assertEquals('OAuth 2 app Client ID, or App ID', $clientId['name']);
|
||||
$this->assertEquals('OAuth2 app Client ID, or App ID', $clientId['name']);
|
||||
$this->assertEquals('e4d87900000000540733', $clientId['example']);
|
||||
$this->assertEquals('Example of wrong value: 370006', $clientId['hint']);
|
||||
$clientSecret = $github['parameters'][1];
|
||||
|
||||
@@ -872,30 +872,36 @@ trait OAuth2Base
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Update Authentik (clientId + clientSecret + REQUIRED endpoint)
|
||||
// Update Authentik (clientId + clientSecret + optional endpoint)
|
||||
// =========================================================================
|
||||
|
||||
public function testUpdateOAuth2AuthentikRequiresEndpoint(): void
|
||||
public function testUpdateOAuth2AuthentikAllowsOmittedEndpointWhenDisabled(): void
|
||||
{
|
||||
// The `endpoint` param is required (Text(min=1)); omitting → 400.
|
||||
$response = $this->updateOAuth2('authentik', [
|
||||
'clientId' => 'whatever',
|
||||
'clientSecret' => 'whatever',
|
||||
'enabled' => false,
|
||||
]);
|
||||
|
||||
$this->assertSame(400, $response['headers']['status-code']);
|
||||
$this->assertSame('general_argument_invalid', $response['body']['type']);
|
||||
$this->assertSame(200, $response['headers']['status-code']);
|
||||
$this->assertSame('authentik', $response['body']['$id']);
|
||||
|
||||
// Cleanup
|
||||
$this->updateOAuth2('authentik', [
|
||||
'clientId' => '',
|
||||
'clientSecret' => '',
|
||||
'endpoint' => '',
|
||||
'enabled' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
public function testUpdateOAuth2AuthentikEmptyEndpointRejected(): void
|
||||
public function testUpdateOAuth2AuthentikEmptyEndpointRejectedWhenEnabling(): void
|
||||
{
|
||||
// The `endpoint` validator is Text(min=1). Sending `''` must be
|
||||
// rejected the same way as omitting — the validator should treat the
|
||||
// empty-string degenerate case as a missing required field.
|
||||
$response = $this->updateOAuth2('authentik', [
|
||||
'clientId' => 'whatever',
|
||||
'clientSecret' => 'whatever',
|
||||
'endpoint' => '',
|
||||
'enabled' => true,
|
||||
]);
|
||||
|
||||
$this->assertSame(400, $response['headers']['status-code']);
|
||||
@@ -920,15 +926,14 @@ trait OAuth2Base
|
||||
$this->updateOAuth2('authentik', [
|
||||
'clientId' => '',
|
||||
'clientSecret' => '',
|
||||
'endpoint' => 'cleanup.authentik.com',
|
||||
'endpoint' => '',
|
||||
'enabled' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
public function testUpdateOAuth2AuthentikPartialPreservesSecret(): void
|
||||
{
|
||||
// Authentik's `endpoint` is required on every call, so we always
|
||||
// re-send it. The `clientSecret` lives in the JSON blob and must
|
||||
// The `clientSecret` and `endpoint` live in the JSON blob and must
|
||||
// survive when omitted on a subsequent call that only changes clientId.
|
||||
$this->updateOAuth2('authentik', [
|
||||
'clientId' => 'authentik-merge-client',
|
||||
@@ -939,27 +944,24 @@ trait OAuth2Base
|
||||
|
||||
$response = $this->updateOAuth2('authentik', [
|
||||
'clientId' => 'authentik-rotated-client',
|
||||
'endpoint' => 'merge.authentik.com',
|
||||
]);
|
||||
$this->assertSame(200, $response['headers']['status-code']);
|
||||
$this->assertSame('authentik-rotated-client', $response['body']['clientId']);
|
||||
$this->assertSame('merge.authentik.com', $response['body']['endpoint']);
|
||||
|
||||
// Confirm clientSecret survived the omitted-field merge by enabling
|
||||
// — Authentik has no verifyCredentials() hook, so non-empty stored
|
||||
// secret is enough. `endpoint` must be re-sent (required on enable too).
|
||||
// without re-sending endpoint.
|
||||
$enable = $this->updateOAuth2('authentik', [
|
||||
'endpoint' => 'merge.authentik.com',
|
||||
'enabled' => true,
|
||||
]);
|
||||
$this->assertSame(200, $enable['headers']['status-code']);
|
||||
$this->assertTrue($enable['body']['enabled']);
|
||||
|
||||
// Cleanup — endpoint is required, use a placeholder.
|
||||
// Cleanup
|
||||
$this->updateOAuth2('authentik', [
|
||||
'clientId' => '',
|
||||
'clientSecret' => '',
|
||||
'endpoint' => 'cleanup.authentik.com',
|
||||
'endpoint' => '',
|
||||
'enabled' => false,
|
||||
]);
|
||||
}
|
||||
@@ -984,40 +986,46 @@ trait OAuth2Base
|
||||
$this->assertSame('enable.authentik.com', $get['body']['endpoint']);
|
||||
$this->assertSame('', $get['body']['clientSecret']);
|
||||
|
||||
// Cleanup — endpoint is required (Text(min=1)) so use a placeholder.
|
||||
// Cleanup
|
||||
$this->updateOAuth2('authentik', [
|
||||
'clientId' => '',
|
||||
'clientSecret' => '',
|
||||
'endpoint' => 'cleanup.authentik.com',
|
||||
'endpoint' => '',
|
||||
'enabled' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Update FusionAuth (clientId + clientSecret + REQUIRED endpoint)
|
||||
// Update FusionAuth (clientId + clientSecret + optional endpoint)
|
||||
// =========================================================================
|
||||
|
||||
public function testUpdateOAuth2FusionAuthRequiresEndpoint(): void
|
||||
public function testUpdateOAuth2FusionAuthAllowsOmittedEndpointWhenDisabled(): void
|
||||
{
|
||||
// The `endpoint` param is required (Text(min=1)); omitting → 400.
|
||||
$response = $this->updateOAuth2('fusionauth', [
|
||||
'clientId' => 'whatever',
|
||||
'clientSecret' => 'whatever',
|
||||
'enabled' => false,
|
||||
]);
|
||||
|
||||
$this->assertSame(400, $response['headers']['status-code']);
|
||||
$this->assertSame('general_argument_invalid', $response['body']['type']);
|
||||
$this->assertSame(200, $response['headers']['status-code']);
|
||||
$this->assertSame('fusionauth', $response['body']['$id']);
|
||||
|
||||
// Cleanup
|
||||
$this->updateOAuth2('fusionauth', [
|
||||
'clientId' => '',
|
||||
'clientSecret' => '',
|
||||
'endpoint' => '',
|
||||
'enabled' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
public function testUpdateOAuth2FusionAuthEmptyEndpointRejected(): void
|
||||
public function testUpdateOAuth2FusionAuthEmptyEndpointRejectedWhenEnabling(): void
|
||||
{
|
||||
// The `endpoint` validator is Text(min=1). Sending `''` must be
|
||||
// rejected the same way as omitting — the validator should treat the
|
||||
// empty-string degenerate case as a missing required field.
|
||||
$response = $this->updateOAuth2('fusionauth', [
|
||||
'clientId' => 'whatever',
|
||||
'clientSecret' => 'whatever',
|
||||
'endpoint' => '',
|
||||
'enabled' => true,
|
||||
]);
|
||||
|
||||
$this->assertSame(400, $response['headers']['status-code']);
|
||||
@@ -1042,15 +1050,14 @@ trait OAuth2Base
|
||||
$this->updateOAuth2('fusionauth', [
|
||||
'clientId' => '',
|
||||
'clientSecret' => '',
|
||||
'endpoint' => 'cleanup.fusionauth.io',
|
||||
'endpoint' => '',
|
||||
'enabled' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
public function testUpdateOAuth2FusionAuthPartialPreservesSecret(): void
|
||||
{
|
||||
// FusionAuth's `endpoint` is required on every call, so we always
|
||||
// re-send it. The `clientSecret` lives in the JSON blob and must
|
||||
// The `clientSecret` and `endpoint` live in the JSON blob and must
|
||||
// survive when omitted on a subsequent call that only changes clientId.
|
||||
$this->updateOAuth2('fusionauth', [
|
||||
'clientId' => 'fusionauth-merge-client',
|
||||
@@ -1061,27 +1068,24 @@ trait OAuth2Base
|
||||
|
||||
$response = $this->updateOAuth2('fusionauth', [
|
||||
'clientId' => 'fusionauth-rotated-client',
|
||||
'endpoint' => 'merge.fusionauth.io',
|
||||
]);
|
||||
$this->assertSame(200, $response['headers']['status-code']);
|
||||
$this->assertSame('fusionauth-rotated-client', $response['body']['clientId']);
|
||||
$this->assertSame('merge.fusionauth.io', $response['body']['endpoint']);
|
||||
|
||||
// Confirm clientSecret survived the omitted-field merge by enabling
|
||||
// — FusionAuth has no verifyCredentials() hook, so non-empty stored
|
||||
// secret is enough. `endpoint` must be re-sent (required on enable too).
|
||||
// without re-sending endpoint.
|
||||
$enable = $this->updateOAuth2('fusionauth', [
|
||||
'endpoint' => 'merge.fusionauth.io',
|
||||
'enabled' => true,
|
||||
]);
|
||||
$this->assertSame(200, $enable['headers']['status-code']);
|
||||
$this->assertTrue($enable['body']['enabled']);
|
||||
|
||||
// Cleanup — endpoint is required, use a placeholder.
|
||||
// Cleanup
|
||||
$this->updateOAuth2('fusionauth', [
|
||||
'clientId' => '',
|
||||
'clientSecret' => '',
|
||||
'endpoint' => 'cleanup.fusionauth.io',
|
||||
'endpoint' => '',
|
||||
'enabled' => false,
|
||||
]);
|
||||
}
|
||||
@@ -1106,70 +1110,85 @@ trait OAuth2Base
|
||||
$this->assertSame('enable.fusionauth.io', $get['body']['endpoint']);
|
||||
$this->assertSame('', $get['body']['clientSecret']);
|
||||
|
||||
// Cleanup — endpoint is required (Text(min=1)) so use a placeholder.
|
||||
// Cleanup
|
||||
$this->updateOAuth2('fusionauth', [
|
||||
'clientId' => '',
|
||||
'clientSecret' => '',
|
||||
'endpoint' => 'cleanup.fusionauth.io',
|
||||
'endpoint' => '',
|
||||
'enabled' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Update Keycloak (clientId + clientSecret + REQUIRED endpoint + REQUIRED realmName)
|
||||
// Update Keycloak (clientId + clientSecret + optional endpoint + optional realmName)
|
||||
// =========================================================================
|
||||
|
||||
public function testUpdateOAuth2KeycloakRequiresEndpoint(): void
|
||||
public function testUpdateOAuth2KeycloakAllowsOmittedEndpointWhenDisabled(): void
|
||||
{
|
||||
// The `endpoint` param is required (Text(min=1)); omitting → 400.
|
||||
$response = $this->updateOAuth2('keycloak', [
|
||||
'clientId' => 'whatever',
|
||||
'clientSecret' => 'whatever',
|
||||
'realmName' => 'appwrite-realm',
|
||||
'enabled' => false,
|
||||
]);
|
||||
|
||||
$this->assertSame(400, $response['headers']['status-code']);
|
||||
$this->assertSame('general_argument_invalid', $response['body']['type']);
|
||||
$this->assertSame(200, $response['headers']['status-code']);
|
||||
$this->assertSame('keycloak', $response['body']['$id']);
|
||||
|
||||
// Cleanup
|
||||
$this->updateOAuth2('keycloak', [
|
||||
'clientId' => '',
|
||||
'clientSecret' => '',
|
||||
'endpoint' => '',
|
||||
'realmName' => '',
|
||||
'enabled' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
public function testUpdateOAuth2KeycloakEmptyEndpointRejected(): void
|
||||
public function testUpdateOAuth2KeycloakEmptyEndpointRejectedWhenEnabling(): void
|
||||
{
|
||||
// The `endpoint` validator is Text(min=1). Sending `''` must be
|
||||
// rejected the same way as omitting — the validator should treat the
|
||||
// empty-string degenerate case as a missing required field.
|
||||
$response = $this->updateOAuth2('keycloak', [
|
||||
'clientId' => 'whatever',
|
||||
'clientSecret' => 'whatever',
|
||||
'endpoint' => '',
|
||||
'realmName' => 'appwrite-realm',
|
||||
'enabled' => true,
|
||||
]);
|
||||
|
||||
$this->assertSame(400, $response['headers']['status-code']);
|
||||
$this->assertSame('general_argument_invalid', $response['body']['type']);
|
||||
}
|
||||
|
||||
public function testUpdateOAuth2KeycloakRequiresRealmName(): void
|
||||
public function testUpdateOAuth2KeycloakAllowsOmittedRealmNameWhenDisabled(): void
|
||||
{
|
||||
// The `realmName` param is required (Text(min=1)); omitting → 400.
|
||||
$response = $this->updateOAuth2('keycloak', [
|
||||
'clientId' => 'whatever',
|
||||
'clientSecret' => 'whatever',
|
||||
'endpoint' => 'keycloak.example.com',
|
||||
'enabled' => false,
|
||||
]);
|
||||
|
||||
$this->assertSame(400, $response['headers']['status-code']);
|
||||
$this->assertSame('general_argument_invalid', $response['body']['type']);
|
||||
$this->assertSame(200, $response['headers']['status-code']);
|
||||
$this->assertSame('keycloak', $response['body']['$id']);
|
||||
|
||||
// Cleanup
|
||||
$this->updateOAuth2('keycloak', [
|
||||
'clientId' => '',
|
||||
'clientSecret' => '',
|
||||
'endpoint' => '',
|
||||
'realmName' => '',
|
||||
'enabled' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
public function testUpdateOAuth2KeycloakEmptyRealmNameRejected(): void
|
||||
public function testUpdateOAuth2KeycloakEmptyRealmNameRejectedWhenEnabling(): void
|
||||
{
|
||||
// The `realmName` validator is Text(min=1). Sending `''` must be
|
||||
// rejected the same way as omitting.
|
||||
$response = $this->updateOAuth2('keycloak', [
|
||||
'clientId' => 'whatever',
|
||||
'clientSecret' => 'whatever',
|
||||
'endpoint' => 'keycloak.example.com',
|
||||
'realmName' => '',
|
||||
'enabled' => true,
|
||||
]);
|
||||
|
||||
$this->assertSame(400, $response['headers']['status-code']);
|
||||
@@ -1196,16 +1215,15 @@ trait OAuth2Base
|
||||
$this->updateOAuth2('keycloak', [
|
||||
'clientId' => '',
|
||||
'clientSecret' => '',
|
||||
'endpoint' => 'cleanup.keycloak.com',
|
||||
'realmName' => 'cleanup-realm',
|
||||
'endpoint' => '',
|
||||
'realmName' => '',
|
||||
'enabled' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
public function testUpdateOAuth2KeycloakPartialPreservesSecret(): void
|
||||
{
|
||||
// Keycloak's `endpoint` and `realmName` are required on every call,
|
||||
// so we always re-send them. The `clientSecret` lives in the JSON
|
||||
// The `clientSecret`, `endpoint`, and `realmName` live in the JSON
|
||||
// blob and must survive when omitted on a subsequent call that only
|
||||
// changes clientId.
|
||||
$this->updateOAuth2('keycloak', [
|
||||
@@ -1218,8 +1236,6 @@ trait OAuth2Base
|
||||
|
||||
$response = $this->updateOAuth2('keycloak', [
|
||||
'clientId' => 'keycloak-rotated-client',
|
||||
'endpoint' => 'merge.keycloak.com',
|
||||
'realmName' => 'merge-realm',
|
||||
]);
|
||||
$this->assertSame(200, $response['headers']['status-code']);
|
||||
$this->assertSame('keycloak-rotated-client', $response['body']['clientId']);
|
||||
@@ -1227,23 +1243,19 @@ trait OAuth2Base
|
||||
$this->assertSame('merge-realm', $response['body']['realmName']);
|
||||
|
||||
// Confirm clientSecret survived the omitted-field merge by enabling
|
||||
// — Keycloak has no verifyCredentials() hook, so non-empty stored
|
||||
// secret is enough. `endpoint`/`realmName` must be re-sent (required
|
||||
// on enable too).
|
||||
// without re-sending endpoint or realmName.
|
||||
$enable = $this->updateOAuth2('keycloak', [
|
||||
'endpoint' => 'merge.keycloak.com',
|
||||
'realmName' => 'merge-realm',
|
||||
'enabled' => true,
|
||||
]);
|
||||
$this->assertSame(200, $enable['headers']['status-code']);
|
||||
$this->assertTrue($enable['body']['enabled']);
|
||||
|
||||
// Cleanup — endpoint and realmName are required, use placeholders.
|
||||
// Cleanup
|
||||
$this->updateOAuth2('keycloak', [
|
||||
'clientId' => '',
|
||||
'clientSecret' => '',
|
||||
'endpoint' => 'cleanup.keycloak.com',
|
||||
'realmName' => 'cleanup-realm',
|
||||
'endpoint' => '',
|
||||
'realmName' => '',
|
||||
'enabled' => false,
|
||||
]);
|
||||
}
|
||||
@@ -1270,40 +1282,47 @@ trait OAuth2Base
|
||||
$this->assertSame('enable-realm', $get['body']['realmName']);
|
||||
$this->assertSame('', $get['body']['clientSecret']);
|
||||
|
||||
// Cleanup — endpoint and realmName are required (Text(min=1)) so use placeholders.
|
||||
// Cleanup
|
||||
$this->updateOAuth2('keycloak', [
|
||||
'clientId' => '',
|
||||
'clientSecret' => '',
|
||||
'endpoint' => 'cleanup.keycloak.com',
|
||||
'realmName' => 'cleanup-realm',
|
||||
'endpoint' => '',
|
||||
'realmName' => '',
|
||||
'enabled' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Update Microsoft (applicationId + applicationSecret + REQUIRED tenant)
|
||||
// Update Microsoft (applicationId + applicationSecret + optional tenant)
|
||||
// =========================================================================
|
||||
|
||||
public function testUpdateOAuth2MicrosoftRequiresTenant(): void
|
||||
public function testUpdateOAuth2MicrosoftAllowsOmittedTenantWhenDisabled(): void
|
||||
{
|
||||
$response = $this->updateOAuth2('microsoft', [
|
||||
'applicationId' => 'whatever',
|
||||
'applicationSecret' => 'whatever',
|
||||
'enabled' => false,
|
||||
]);
|
||||
|
||||
$this->assertSame(400, $response['headers']['status-code']);
|
||||
$this->assertSame('general_argument_invalid', $response['body']['type']);
|
||||
$this->assertSame(200, $response['headers']['status-code']);
|
||||
$this->assertSame('microsoft', $response['body']['$id']);
|
||||
|
||||
// Cleanup
|
||||
$this->updateOAuth2('microsoft', [
|
||||
'applicationId' => '',
|
||||
'applicationSecret' => '',
|
||||
'tenant' => '',
|
||||
'enabled' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
public function testUpdateOAuth2MicrosoftEmptyTenantRejected(): void
|
||||
public function testUpdateOAuth2MicrosoftEmptyTenantRejectedWhenEnabling(): void
|
||||
{
|
||||
// The `tenant` validator is Text(min=1). Sending `''` must be rejected
|
||||
// the same way as omitting — the validator should treat the empty
|
||||
// string as a missing required field.
|
||||
$response = $this->updateOAuth2('microsoft', [
|
||||
'applicationId' => 'whatever',
|
||||
'applicationSecret' => 'whatever',
|
||||
'tenant' => '',
|
||||
'enabled' => true,
|
||||
]);
|
||||
|
||||
$this->assertSame(400, $response['headers']['status-code']);
|
||||
@@ -1331,7 +1350,7 @@ trait OAuth2Base
|
||||
$this->updateOAuth2('microsoft', [
|
||||
'applicationId' => '',
|
||||
'applicationSecret' => '',
|
||||
'tenant' => 'common',
|
||||
'tenant' => '',
|
||||
'enabled' => false,
|
||||
]);
|
||||
}
|
||||
@@ -1346,23 +1365,21 @@ trait OAuth2Base
|
||||
'enabled' => false,
|
||||
]);
|
||||
|
||||
// Patch with only `tenant` (it's required on every call) and a new
|
||||
// applicationId, leaving applicationSecret omitted. The stored secret
|
||||
// must not be wiped.
|
||||
// Patch with only a new applicationId, leaving applicationSecret and
|
||||
// tenant omitted. The stored JSON values must not be wiped.
|
||||
$response = $this->updateOAuth2('microsoft', [
|
||||
'applicationId' => 'updated-app-id',
|
||||
'tenant' => 'organizations',
|
||||
]);
|
||||
|
||||
$this->assertSame(200, $response['headers']['status-code']);
|
||||
$this->assertSame('updated-app-id', $response['body']['applicationId']);
|
||||
$this->assertSame('organizations', $response['body']['tenant']);
|
||||
$this->assertSame('common', $response['body']['tenant']);
|
||||
|
||||
// Cleanup
|
||||
$this->updateOAuth2('microsoft', [
|
||||
'applicationId' => '',
|
||||
'applicationSecret' => '',
|
||||
'tenant' => 'common',
|
||||
'tenant' => '',
|
||||
'enabled' => false,
|
||||
]);
|
||||
}
|
||||
@@ -1387,11 +1404,11 @@ trait OAuth2Base
|
||||
$this->assertSame('common', $get['body']['tenant']);
|
||||
$this->assertSame('', $get['body']['applicationSecret']);
|
||||
|
||||
// Cleanup — tenant is required (Text(min=1)) so use a placeholder.
|
||||
// Cleanup
|
||||
$this->updateOAuth2('microsoft', [
|
||||
'applicationId' => '',
|
||||
'applicationSecret' => '',
|
||||
'tenant' => 'common',
|
||||
'tenant' => '',
|
||||
'enabled' => false,
|
||||
]);
|
||||
}
|
||||
@@ -2401,8 +2418,9 @@ trait OAuth2Base
|
||||
//
|
||||
// Ensures each provider's Update endpoint is wired up correctly: routing,
|
||||
// provider class, response model and `$id`. Custom-shaped providers
|
||||
// (apple, auth0, authentik, gitlab, microsoft, oidc, okta, dropbox) and
|
||||
// sandboxes (paypalSandbox, tradeshiftSandbox) have dedicated tests above.
|
||||
// (apple, auth0, authentik, fusionauth, gitlab, keycloak, microsoft, oidc,
|
||||
// okta, dropbox) and sandboxes (paypalSandbox, tradeshiftSandbox) have
|
||||
// dedicated tests above.
|
||||
// Github is excluded because its `verifyCredentials()` hook is exercised
|
||||
// separately.
|
||||
// =========================================================================
|
||||
|
||||
Reference in New Issue
Block a user