mirror of
https://github.com/appwrite/appwrite.git
synced 2026-05-26 13:51:13 +00:00
Compare commits
26
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a369d49b2c | ||
|
|
22691bee66 | ||
|
|
51f036491a | ||
|
|
4df0934023 | ||
|
|
5e974713b7 | ||
|
|
e2071bd5dd | ||
|
|
0621a32aa6 | ||
|
|
e1cbbe3943 | ||
|
|
226e36db95 | ||
|
|
faedf49488 | ||
|
|
a87263a571 | ||
|
|
dafa97879c | ||
|
|
a263afeff1 | ||
|
|
29915ddd3b | ||
|
|
c0f5fa90cb | ||
|
|
e666dc9504 | ||
|
|
3dc69ba62a | ||
|
|
7bf5f2d360 | ||
|
|
6df5556473 | ||
|
|
96e85c0bab | ||
|
|
40ab50ec9d | ||
|
|
9b762dde40 | ||
|
|
615aff0714 | ||
|
|
525b929e54 | ||
|
|
074ffad826 | ||
|
|
36c87d109a |
@@ -1139,6 +1139,11 @@ return [
|
||||
'description' => 'Key with the requested ID could not be found.',
|
||||
'code' => 404,
|
||||
],
|
||||
Exception::KEY_ALREADY_EXISTS => [
|
||||
'name' => Exception::KEY_ALREADY_EXISTS,
|
||||
'description' => 'Key with the same ID already exists. Try again with a different ID.',
|
||||
'code' => 409,
|
||||
],
|
||||
Exception::PLATFORM_NOT_FOUND => [
|
||||
'name' => Exception::PLATFORM_NOT_FOUND,
|
||||
'description' => 'Platform with the requested ID could not be found.',
|
||||
|
||||
@@ -1469,13 +1469,14 @@ Http::get('/v1/account/sessions/oauth2/:provider/redirect')
|
||||
->inject('devKey')
|
||||
->inject('user')
|
||||
->inject('dbForProject')
|
||||
->inject('dbForPlatform')
|
||||
->inject('geodb')
|
||||
->inject('queueForEvents')
|
||||
->inject('store')
|
||||
->inject('proofForPassword')
|
||||
->inject('proofForToken')
|
||||
->inject('authorization')
|
||||
->action(function (string $provider, string $code, string $state, string $error, string $error_description, Request $request, Response $response, Document $project, Validator $redirectValidator, Document $devKey, User $user, Database $dbForProject, Reader $geodb, Event $queueForEvents, Store $store, ProofsPassword $proofForPassword, ProofsToken $proofForToken, Authorization $authorization) use ($oauthDefaultSuccess) {
|
||||
->action(function (string $provider, string $code, string $state, string $error, string $error_description, Request $request, Response $response, Document $project, Validator $redirectValidator, Document $devKey, User $user, Database $dbForProject, Database $dbForPlatform, Reader $geodb, Event $queueForEvents, Store $store, ProofsPassword $proofForPassword, ProofsToken $proofForToken, Authorization $authorization) use ($oauthDefaultSuccess) {
|
||||
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') === 'disabled' ? 'http' : 'https';
|
||||
$port = $request->getPort();
|
||||
$callbackBase = $protocol . '://' . $request->getHostname();
|
||||
@@ -1512,6 +1513,25 @@ Http::get('/v1/account/sessions/oauth2/:provider/redirect')
|
||||
$state = $defaultState;
|
||||
}
|
||||
|
||||
// Allow redirect to rule URL if related to project
|
||||
//Check if $redirectValidator is instance of Redirect class
|
||||
if ($redirectValidator instanceof Redirect) {
|
||||
$rules = $authorization->skip(fn () => $dbForPlatform->find('rules', [
|
||||
Query::equal('domain', [
|
||||
parse_url($state['success'], PHP_URL_HOST),
|
||||
parse_url($state['failure'], PHP_URL_HOST)
|
||||
]),
|
||||
Query::equal('projectInternalId', [$project->getSequence()]),
|
||||
Query::limit(2)
|
||||
]));
|
||||
|
||||
foreach ($rules as $rule) {
|
||||
$allowedHostnames = $redirectValidator->getAllowedHostnames();
|
||||
$allowedHostnames[] = $rule->getAttribute('domain', '');
|
||||
$redirectValidator->setAllowedHostnames($allowedHostnames);
|
||||
}
|
||||
}
|
||||
|
||||
if ($devKey->isEmpty() && !$redirectValidator->isValid($state['success'])) {
|
||||
throw new Exception(Exception::PROJECT_INVALID_SUCCESS_URL);
|
||||
}
|
||||
|
||||
@@ -14,16 +14,21 @@ use Appwrite\SDK\Deprecated;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Template\Template;
|
||||
use Appwrite\Utopia\Database\Validator\CustomId;
|
||||
use Appwrite\Utopia\Database\Validator\Queries\Keys;
|
||||
use Appwrite\Utopia\Response;
|
||||
use PHPMailer\PHPMailer\PHPMailer;
|
||||
use Utopia\Config\Config;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Exception\Duplicate;
|
||||
use Utopia\Database\Exception\Query as QueryException;
|
||||
use Utopia\Database\Helpers\ID;
|
||||
use Utopia\Database\Helpers\Permission;
|
||||
use Utopia\Database\Helpers\Role;
|
||||
use Utopia\Database\Query;
|
||||
use Utopia\Database\Validator\Datetime as DatetimeValidator;
|
||||
use Utopia\Database\Validator\Query\Cursor;
|
||||
use Utopia\Database\Validator\UID;
|
||||
use Utopia\Domains\Validator\PublicDomain;
|
||||
use Utopia\Http;
|
||||
@@ -1094,12 +1099,15 @@ Http::post('/v1/projects/:projectId/keys')
|
||||
]
|
||||
))
|
||||
->param('projectId', '', new UID(), 'Project unique ID.')
|
||||
// TODO: When migrating to Platform API, mark keyId required for consistency
|
||||
->param('keyId', 'unique()', new CustomId(), 'Key ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.', true)
|
||||
->param('name', null, new Text(128), 'Key name. Max length: 128 chars.')
|
||||
->param('scopes', null, new Nullable(new ArrayList(new WhiteList(array_keys(Config::getParam('projectScopes')), true), APP_LIMIT_ARRAY_PARAMS_SIZE)), 'Key scopes list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' scopes are allowed.')
|
||||
->param('expire', null, new Nullable(new DatetimeValidator()), 'Expiration time in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. Use null for unlimited expiration.', true)
|
||||
->inject('response')
|
||||
->inject('dbForPlatform')
|
||||
->action(function (string $projectId, string $name, array $scopes, ?string $expire, Response $response, Database $dbForPlatform) {
|
||||
->action(function (string $projectId, string $keyId, string $name, array $scopes, ?string $expire, Response $response, Database $dbForPlatform) {
|
||||
$keyId = $keyId == 'unique()' ? ID::unique() : $keyId;
|
||||
|
||||
$project = $dbForPlatform->getDocument('projects', $projectId);
|
||||
|
||||
@@ -1108,7 +1116,7 @@ Http::post('/v1/projects/:projectId/keys')
|
||||
}
|
||||
|
||||
$key = new Document([
|
||||
'$id' => ID::unique(),
|
||||
'$id' => $keyId,
|
||||
'$permissions' => [
|
||||
Permission::read(Role::any()),
|
||||
Permission::update(Role::any()),
|
||||
@@ -1125,7 +1133,11 @@ Http::post('/v1/projects/:projectId/keys')
|
||||
'secret' => API_KEY_STANDARD . '_' . \bin2hex(\random_bytes(128)),
|
||||
]);
|
||||
|
||||
$key = $dbForPlatform->createDocument('keys', $key);
|
||||
try {
|
||||
$key = $dbForPlatform->createDocument('keys', $key);
|
||||
} catch (Duplicate) {
|
||||
throw new Exception(Exception::KEY_ALREADY_EXISTS);
|
||||
}
|
||||
|
||||
$dbForPlatform->purgeCachedDocument('projects', $project->getId());
|
||||
|
||||
@@ -1152,10 +1164,11 @@ Http::get('/v1/projects/:projectId/keys')
|
||||
]
|
||||
))
|
||||
->param('projectId', '', new UID(), 'Project unique ID.')
|
||||
->param('queries', [], new Keys(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Keys::ALLOWED_ATTRIBUTES), true)
|
||||
->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true)
|
||||
->inject('response')
|
||||
->inject('dbForPlatform')
|
||||
->action(function (string $projectId, bool $includeTotal, Response $response, Database $dbForPlatform) {
|
||||
->action(function (string $projectId, array $queries, bool $includeTotal, Response $response, Database $dbForPlatform) {
|
||||
|
||||
$project = $dbForPlatform->getDocument('projects', $projectId);
|
||||
|
||||
@@ -1163,15 +1176,46 @@ Http::get('/v1/projects/:projectId/keys')
|
||||
throw new Exception(Exception::PROJECT_NOT_FOUND);
|
||||
}
|
||||
|
||||
$keys = $dbForPlatform->find('keys', [
|
||||
Query::equal('resourceType', ['projects']),
|
||||
Query::equal('resourceInternalId', [$project->getSequence()]),
|
||||
Query::limit(5000),
|
||||
]);
|
||||
try {
|
||||
$queries = Query::parseQueries($queries);
|
||||
} catch (QueryException $e) {
|
||||
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
|
||||
}
|
||||
|
||||
// Backwards compatibility
|
||||
if (\count(Query::getByType($queries, [Query::TYPE_LIMIT])) === 0) {
|
||||
$queries[] = Query::limit(5000);
|
||||
}
|
||||
|
||||
$queries[] = Query::equal('resourceType', ['projects']);
|
||||
$queries[] = Query::equal('resourceInternalId', [$project->getSequence()]);
|
||||
|
||||
$cursor = Query::getCursorQueries($queries, false);
|
||||
$cursor = \reset($cursor);
|
||||
|
||||
if ($cursor !== false) {
|
||||
$validator = new Cursor();
|
||||
if (!$validator->isValid($cursor)) {
|
||||
throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription());
|
||||
}
|
||||
|
||||
$keyId = $cursor->getValue();
|
||||
$cursorDocument = $dbForPlatform->getDocument('keys', $keyId);
|
||||
|
||||
if ($cursorDocument->isEmpty()) {
|
||||
throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Key '{$keyId}' for the 'cursor' value not found.");
|
||||
}
|
||||
|
||||
$cursor->setValue($cursorDocument);
|
||||
}
|
||||
|
||||
$filterQueries = Query::groupByType($queries)['filters'];
|
||||
|
||||
$keys = $dbForPlatform->find('keys', $queries);
|
||||
|
||||
$response->dynamic(new Document([
|
||||
'keys' => $keys,
|
||||
'total' => $includeTotal ? count($keys) : 0,
|
||||
'total' => $includeTotal ? $dbForPlatform->count('keys', $filterQueries, APP_LIMIT_COUNT) : 0,
|
||||
]), Response::MODEL_KEY_LIST);
|
||||
});
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@ use Appwrite\Platform\Modules\Compute\Specification;
|
||||
const APP_NAME = 'Appwrite';
|
||||
const APP_DOMAIN = 'appwrite.io';
|
||||
|
||||
const APP_VIEWS_DIR = __DIR__ . '/../views';
|
||||
|
||||
// Email
|
||||
const APP_EMAIL_TEAM = 'team@localhost.test'; // Default email address
|
||||
const APP_EMAIL_SECURITY = ''; // Default security email address
|
||||
|
||||
@@ -318,6 +318,7 @@ class Exception extends \Exception
|
||||
|
||||
/** Keys */
|
||||
public const string KEY_NOT_FOUND = 'key_not_found';
|
||||
public const string KEY_ALREADY_EXISTS = 'key_already_exists';
|
||||
|
||||
/** Variables */
|
||||
public const string VARIABLE_NOT_FOUND = 'variable_not_found';
|
||||
|
||||
@@ -22,6 +22,27 @@ class Origin extends Validator
|
||||
{
|
||||
}
|
||||
|
||||
public function setAllowedHostnames(array $allowedHostnames): self
|
||||
{
|
||||
$this->allowedHostnames = $allowedHostnames;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setAllowedSchemes(array $allowedSchemes): self
|
||||
{
|
||||
$this->allowedSchemes = $allowedSchemes;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getAllowedHostnames(): array
|
||||
{
|
||||
return $this->allowedHostnames;
|
||||
}
|
||||
|
||||
public function getAllowedSchemes(): array
|
||||
{
|
||||
return $this->allowedSchemes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if Origin is valid.
|
||||
|
||||
@@ -31,7 +31,7 @@ class Get extends Action
|
||||
->desc('Create GitHub app installation')
|
||||
->groups(['api', 'vcs'])
|
||||
->label('scope', 'vcs.read')
|
||||
->label('error', __DIR__ . '/../../views/general/error.phtml')
|
||||
->label('error', APP_VIEWS_DIR . '/general/error.phtml')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'vcs',
|
||||
group: 'installations',
|
||||
|
||||
@@ -35,7 +35,7 @@ class Get extends Action
|
||||
->desc('Get installation and authorization from GitHub app')
|
||||
->groups(['api', 'vcs'])
|
||||
->label('scope', 'public')
|
||||
->label('error', __DIR__ . '/../../views/general/error.phtml')
|
||||
->label('error', APP_VIEWS_DIR . '/general/error.phtml')
|
||||
->param('installation_id', '', new Text(256, 0), 'GitHub installation ID', true)
|
||||
->param('setup_action', '', new Text(256, 0), 'GitHub setup action type', true)
|
||||
->param('state', '', new Text(2048), 'GitHub state. Contains info sent when starting authorization flow.', true)
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Utopia\Database\Validator\Queries;
|
||||
|
||||
class Keys extends Base
|
||||
{
|
||||
public const ALLOWED_ATTRIBUTES = [
|
||||
'expire',
|
||||
'accessedAt',
|
||||
'name',
|
||||
'scopes',
|
||||
];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct('keys', self::ALLOWED_ATTRIBUTES);
|
||||
}
|
||||
}
|
||||
@@ -60,6 +60,7 @@ trait ProjectCustom
|
||||
'cookie' => 'a_session_console=' . $this->getRoot()['session'],
|
||||
'x-appwrite-project' => 'console',
|
||||
], [
|
||||
'keyId' => ID::unique(),
|
||||
'name' => 'Demo Project Key',
|
||||
'scopes' => [
|
||||
'users.read',
|
||||
@@ -194,6 +195,7 @@ trait ProjectCustom
|
||||
'cookie' => 'a_session_console=' . $this->getRoot()['session'],
|
||||
'x-appwrite-project' => 'console',
|
||||
], [
|
||||
'keyId' => ID::unique(),
|
||||
'name' => 'Demo Project Key',
|
||||
'scopes' => $scopes,
|
||||
]);
|
||||
|
||||
@@ -1168,7 +1168,7 @@ class ProjectsConsoleClientTest extends Scope
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'duration' => 60, // Set session duration to 1 minute
|
||||
'duration' => 10, // Set session duration to 10 seconds
|
||||
]);
|
||||
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
@@ -1177,7 +1177,7 @@ class ProjectsConsoleClientTest extends Scope
|
||||
$this->assertArrayHasKey('platforms', $response['body']);
|
||||
$this->assertArrayHasKey('webhooks', $response['body']);
|
||||
$this->assertArrayHasKey('keys', $response['body']);
|
||||
$this->assertEquals(60, $response['body']['authDuration']);
|
||||
$this->assertEquals(10, $response['body']['authDuration']);
|
||||
|
||||
$projectId = $response['body']['$id'];
|
||||
|
||||
@@ -1218,44 +1218,30 @@ class ProjectsConsoleClientTest extends Scope
|
||||
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
|
||||
// Check session doesn't expire too soon.
|
||||
sleep(30);
|
||||
// Eventually session expires, within 15 seconds (10+variance)
|
||||
$this->assertEventually(function () use ($projectId, $sessionCookie) {
|
||||
// Get User
|
||||
$response = $this->client->call(Client::METHOD_GET, '/account', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $projectId,
|
||||
'Cookie' => $sessionCookie,
|
||||
]));
|
||||
|
||||
// Get User
|
||||
$response = $this->client->call(Client::METHOD_GET, '/account', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $projectId,
|
||||
'Cookie' => $sessionCookie,
|
||||
]));
|
||||
$this->assertEquals(401, $response['headers']['status-code']);
|
||||
}, timeoutMs: 15 * 1000);
|
||||
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
|
||||
// Wait just over a minute
|
||||
sleep(35);
|
||||
|
||||
// Get User
|
||||
$response = $this->client->call(Client::METHOD_GET, '/account', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $projectId,
|
||||
'Cookie' => $sessionCookie,
|
||||
]));
|
||||
|
||||
$this->assertEquals(401, $response['headers']['status-code']);
|
||||
|
||||
// Set session duration to 15s
|
||||
// Set session duration to 10min
|
||||
$response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/auth/duration', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'duration' => 15, // seconds
|
||||
'duration' => 600, // seconds
|
||||
]);
|
||||
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
$this->assertEquals(15, $response['body']['authDuration']);
|
||||
|
||||
// Wait 20 seconds, ensure non-valid session
|
||||
\sleep(20);
|
||||
$this->assertEquals(600, $response['body']['authDuration']);
|
||||
|
||||
// Ensure session is still expired (new duration only affects new sessions)
|
||||
$response = $this->client->call(Client::METHOD_GET, '/account', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $projectId,
|
||||
@@ -2774,6 +2760,7 @@ class ProjectsConsoleClientTest extends Scope
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'cookie' => 'a_session_console=' . $this->getRoot()['session'],
|
||||
]), [
|
||||
'keyId' => ID::unique(),
|
||||
'name' => 'Key Test',
|
||||
'scopes' => ['functions.read', 'teams.write'],
|
||||
]);
|
||||
@@ -3123,6 +3110,7 @@ class ProjectsConsoleClientTest extends Scope
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'keyId' => ID::unique(),
|
||||
'name' => 'Key Test',
|
||||
'scopes' => ['teams.read', 'teams.write'],
|
||||
]);
|
||||
@@ -3138,6 +3126,66 @@ class ProjectsConsoleClientTest extends Scope
|
||||
$this->assertArrayHasKey('accessedAt', $response['body']);
|
||||
$this->assertEmpty($response['body']['accessedAt']);
|
||||
|
||||
/**
|
||||
* Test for SUCCESS without key ID
|
||||
*/
|
||||
$response = $this->client->call(Client::METHOD_POST, '/projects/' . $id . '/keys', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'name' => 'Key Custom',
|
||||
'scopes' => ['teams.read', 'teams.write'],
|
||||
]);
|
||||
|
||||
$this->assertEquals(201, $response['headers']['status-code']);
|
||||
$this->assertNotEmpty($response['body']['$id']);
|
||||
|
||||
/**
|
||||
* Test for SUCCESS with custom ID
|
||||
*/
|
||||
$customKeyId = \uniqid() . 'custom-id';
|
||||
$response = $this->client->call(Client::METHOD_POST, '/projects/' . $id . '/keys', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'keyId' => $customKeyId,
|
||||
'name' => 'Key Custom',
|
||||
'scopes' => ['teams.read', 'teams.write'],
|
||||
]);
|
||||
|
||||
$this->assertEquals(201, $response['headers']['status-code']);
|
||||
$this->assertSame($customKeyId, $response['body']['$id']);
|
||||
|
||||
/**
|
||||
* Test for FAILURE with custom ID
|
||||
*/
|
||||
$response = $this->client->call(Client::METHOD_POST, '/projects/' . $id . '/keys', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'keyId' => $customKeyId,
|
||||
'name' => 'Key Custom',
|
||||
'scopes' => ['teams.read', 'teams.write'],
|
||||
]);
|
||||
|
||||
$this->assertEquals(409, $response['headers']['status-code']);
|
||||
|
||||
/**
|
||||
* Test for SUCCESS with magic string ID
|
||||
*/
|
||||
$response = $this->client->call(Client::METHOD_POST, '/projects/' . $id . '/keys', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'keyId' => 'unique()',
|
||||
'name' => 'Key Custom',
|
||||
'scopes' => ['teams.read', 'teams.write'],
|
||||
]);
|
||||
|
||||
$this->assertEquals(201, $response['headers']['status-code']);
|
||||
$this->assertNotEmpty($response['body']['$id']);
|
||||
$this->assertNotSame('unique()', $response['body']['$id']);
|
||||
|
||||
$data = array_merge($data, [
|
||||
'keyId' => $response['body']['$id'],
|
||||
'secret' => $response['body']['secret']
|
||||
@@ -3150,6 +3198,7 @@ class ProjectsConsoleClientTest extends Scope
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'keyId' => ID::unique(),
|
||||
'name' => 'Key Test',
|
||||
'scopes' => ['unknown'],
|
||||
]);
|
||||
@@ -3167,19 +3216,258 @@ class ProjectsConsoleClientTest extends Scope
|
||||
{
|
||||
$id = $data['projectId'] ?? '';
|
||||
|
||||
/** Create a second key with an expiry for query testing */
|
||||
$expireDate = DateTime::addSeconds(new \DateTime(), 3600);
|
||||
$response = $this->client->call(Client::METHOD_POST, '/projects/' . $id . '/keys', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'name' => 'Key Test 2',
|
||||
'scopes' => ['users.read'],
|
||||
'expire' => $expireDate,
|
||||
]);
|
||||
|
||||
$this->assertEquals(201, $response['headers']['status-code']);
|
||||
$key2Id = $response['body']['$id'];
|
||||
|
||||
/** List all keys (no queries) */
|
||||
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id . '/keys', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), []);
|
||||
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
$this->assertEquals(5, $response['body']['total']);
|
||||
$this->assertCount(5, $response['body']['keys']);
|
||||
|
||||
/** List keys with limit */
|
||||
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id . '/keys', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'queries' => [
|
||||
Query::limit(1)->toString(),
|
||||
]
|
||||
]);
|
||||
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
$this->assertCount(1, $response['body']['keys']);
|
||||
$this->assertEquals(5, $response['body']['total']);
|
||||
|
||||
/** List keys with offset */
|
||||
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id . '/keys', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'queries' => [
|
||||
Query::offset(1)->toString(),
|
||||
]
|
||||
]);
|
||||
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
$this->assertCount(4, $response['body']['keys']);
|
||||
$this->assertEquals(5, $response['body']['total']);
|
||||
|
||||
/** List keys with cursor after */
|
||||
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id . '/keys', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'queries' => [
|
||||
Query::cursorAfter(new Document(['$id' => $data['keyId']]))->toString(),
|
||||
]
|
||||
]);
|
||||
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
$this->assertCount(1, $response['body']['keys']);
|
||||
$this->assertEquals(5, $response['body']['total']);
|
||||
$this->assertEquals($key2Id, $response['body']['keys'][0]['$id']);
|
||||
|
||||
/** List keys filtering by expire (lessThan now — should match none) */
|
||||
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id . '/keys', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'queries' => [
|
||||
Query::lessThan('expire', (new \DateTime())->format('Y-m-d H:i:s'))->toString(),
|
||||
]
|
||||
]);
|
||||
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
$this->assertEquals(0, $response['body']['total']);
|
||||
|
||||
/** List keys filtering by expire (greaterThan now — should match the key with expiry) */
|
||||
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id . '/keys', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'queries' => [
|
||||
Query::greaterThan('expire', (new \DateTime())->format('Y-m-d H:i:s'))->toString(),
|
||||
]
|
||||
]);
|
||||
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
$this->assertEquals(1, $response['body']['total']);
|
||||
$this->assertCount(1, $response['body']['keys']);
|
||||
|
||||
/** List keys filtering by name (equal — exact match) */
|
||||
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id . '/keys', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'queries' => [
|
||||
Query::equal('name', ['Key Test'])->toString(),
|
||||
]
|
||||
]);
|
||||
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
$this->assertEquals(1, $response['body']['total']);
|
||||
$this->assertCount(1, $response['body']['keys']);
|
||||
$this->assertEquals('Key Test', $response['body']['keys'][0]['name']);
|
||||
|
||||
/** List keys filtering by name (equal — multiple values) */
|
||||
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id . '/keys', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'queries' => [
|
||||
Query::equal('name', ['Key Test', 'Key Test 2'])->toString(),
|
||||
]
|
||||
]);
|
||||
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
$this->assertEquals(2, $response['body']['total']);
|
||||
$this->assertCount(2, $response['body']['keys']);
|
||||
|
||||
/** List keys filtering by name (equal — no match) */
|
||||
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id . '/keys', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'queries' => [
|
||||
Query::equal('name', ['Non Existent Key'])->toString(),
|
||||
]
|
||||
]);
|
||||
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
$this->assertEquals(0, $response['body']['total']);
|
||||
$this->assertCount(0, $response['body']['keys']);
|
||||
|
||||
/** List keys filtering by scopes (contains — match key with teams.read) */
|
||||
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id . '/keys', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'queries' => [
|
||||
Query::contains('scopes', ['teams.read'])->toString(),
|
||||
]
|
||||
]);
|
||||
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
$this->assertEquals(4, $response['body']['total']);
|
||||
$this->assertCount(4, $response['body']['keys']);
|
||||
|
||||
/** List keys filtering by scopes (contains — match key with users.read) */
|
||||
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id . '/keys', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'queries' => [
|
||||
Query::contains('scopes', ['users.read'])->toString(),
|
||||
]
|
||||
]);
|
||||
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
$this->assertEquals(1, $response['body']['total']);
|
||||
$this->assertCount(1, $response['body']['keys']);
|
||||
|
||||
/** List keys filtering by scopes (contains — no match) */
|
||||
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id . '/keys', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'queries' => [
|
||||
Query::contains('scopes', ['databases.read'])->toString(),
|
||||
]
|
||||
]);
|
||||
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
$this->assertEquals(0, $response['body']['total']);
|
||||
$this->assertCount(0, $response['body']['keys']);
|
||||
|
||||
/** List keys filtering by name and scopes combined */
|
||||
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id . '/keys', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'queries' => [
|
||||
Query::equal('name', ['Key Test'])->toString(),
|
||||
Query::contains('scopes', ['teams.read'])->toString(),
|
||||
]
|
||||
]);
|
||||
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
$this->assertEquals(1, $response['body']['total']);
|
||||
$this->assertCount(1, $response['body']['keys']);
|
||||
$this->assertEquals('Key Test', $response['body']['keys'][0]['name']);
|
||||
|
||||
/** List keys with orderDesc */
|
||||
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id . '/keys', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'queries' => [
|
||||
Query::orderDesc('$createdAt')->toString(),
|
||||
]
|
||||
]);
|
||||
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
$this->assertCount(5, $response['body']['keys']);
|
||||
$this->assertGreaterThan($response['body']['keys'][1]['$createdAt'], $response['body']['keys'][0]['$createdAt']);
|
||||
|
||||
/** List keys with total disabled */
|
||||
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id . '/keys', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'total' => false,
|
||||
'queries' => [
|
||||
Query::limit(1)->toString()
|
||||
]
|
||||
]);
|
||||
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
$this->assertCount(1, $response['body']['keys']);
|
||||
$this->assertEquals(0, $response['body']['total']);
|
||||
|
||||
/**
|
||||
* Test for FAILURE
|
||||
*/
|
||||
|
||||
/** Test invalid query attribute */
|
||||
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id . '/keys', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'queries' => [
|
||||
Query::equal('secret', ['test'])->toString(),
|
||||
]
|
||||
]);
|
||||
|
||||
$this->assertEquals(400, $response['headers']['status-code']);
|
||||
|
||||
/** Test invalid cursor */
|
||||
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id . '/keys', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'queries' => [
|
||||
Query::cursorAfter(new Document(['$id' => 'invalid']))->toString(),
|
||||
]
|
||||
]);
|
||||
|
||||
$this->assertEquals(400, $response['headers']['status-code']);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
@@ -3200,7 +3488,7 @@ class ProjectsConsoleClientTest extends Scope
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
$this->assertNotEmpty($response['body']['$id']);
|
||||
$this->assertEquals($keyId, $response['body']['$id']);
|
||||
$this->assertEquals('Key Test', $response['body']['name']);
|
||||
$this->assertEquals('Key Custom', $response['body']['name']);
|
||||
$this->assertContains('teams.read', $response['body']['scopes']);
|
||||
$this->assertContains('teams.write', $response['body']['scopes']);
|
||||
$this->assertCount(2, $response['body']['scopes']);
|
||||
@@ -3240,6 +3528,7 @@ class ProjectsConsoleClientTest extends Scope
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'keyId' => ID::unique(),
|
||||
'name' => 'Key Test',
|
||||
'scopes' => ['users.write'],
|
||||
'expire' => DateTime::addSeconds(new \DateTime(), 3600),
|
||||
@@ -3260,6 +3549,7 @@ class ProjectsConsoleClientTest extends Scope
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'keyId' => ID::unique(),
|
||||
'name' => 'Key Test',
|
||||
'scopes' => ['health.read'],
|
||||
'expire' => null,
|
||||
@@ -3282,6 +3572,7 @@ class ProjectsConsoleClientTest extends Scope
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'keyId' => ID::unique(),
|
||||
'name' => 'Key Test',
|
||||
'scopes' => ['health.read'],
|
||||
'expire' => DateTime::addSeconds(new \DateTime(), -3600),
|
||||
@@ -3323,6 +3614,7 @@ class ProjectsConsoleClientTest extends Scope
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'keyId' => ID::unique(),
|
||||
'name' => 'Key Test',
|
||||
'scopes' => ['teams.read'],
|
||||
'expire' => DateTime::addSeconds(new \DateTime(), 3600),
|
||||
@@ -3355,6 +3647,7 @@ class ProjectsConsoleClientTest extends Scope
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'keyId' => ID::unique(),
|
||||
'name' => 'Key Test',
|
||||
'scopes' => ['health.read'],
|
||||
'expire' => DateTime::addSeconds(new \DateTime(), 3600),
|
||||
@@ -4364,6 +4657,7 @@ class ProjectsConsoleClientTest extends Scope
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'keyId' => ID::unique(),
|
||||
'name' => 'Key Test',
|
||||
'scopes' => ['users.read', 'users.write'],
|
||||
]);
|
||||
@@ -4384,6 +4678,7 @@ class ProjectsConsoleClientTest extends Scope
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'keyId' => ID::unique(),
|
||||
'name' => 'Key Test',
|
||||
'scopes' => ['users.read', 'users.write'],
|
||||
]);
|
||||
@@ -5191,6 +5486,24 @@ class ProjectsConsoleClientTest extends Scope
|
||||
], followRedirects: false);
|
||||
$this->assertEquals(400, $response['headers']['status-code']);
|
||||
|
||||
// Also ensure final step blocks unknown redirect URL
|
||||
$response = $this->client->call(Client::METHOD_GET, '/account/sessions/oauth2/' . $provider . '/redirect', [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $projectId,
|
||||
'origin' => '',
|
||||
'referer' => 'https://mockserver.com',
|
||||
], [
|
||||
'code' => 'any-code',
|
||||
'state' => \json_encode([
|
||||
'success' => 'https://domain-without-rule.com',
|
||||
'failure' => 'https://domain-without-rule.com'
|
||||
]),
|
||||
'error' => '',
|
||||
'error_description' => '',
|
||||
], followRedirects: false);
|
||||
$this->assertEquals(400, $response['headers']['status-code']);
|
||||
$this->assertStringContainsString('project_invalid_success_url', $response['body']);
|
||||
|
||||
// Ensure rule's domain can be redirect URL
|
||||
$response = $this->client->call(Client::METHOD_GET, '/account/sessions/oauth2/' . $provider, [
|
||||
'content-type' => 'application/json',
|
||||
@@ -5203,6 +5516,24 @@ class ProjectsConsoleClientTest extends Scope
|
||||
], followRedirects: false);
|
||||
$this->assertEquals(301, $response['headers']['status-code']);
|
||||
|
||||
// Also ensure final step allows redirect URL
|
||||
$response = $this->client->call(Client::METHOD_GET, '/account/sessions/oauth2/' . $provider . '/redirect', [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $projectId,
|
||||
'origin' => '',
|
||||
'referer' => 'https://mockserver.com',
|
||||
], [
|
||||
'code' => 'any-code',
|
||||
'state' => \json_encode([
|
||||
'success' => 'https://' . $domain,
|
||||
'failure' => 'https://' . $domain
|
||||
]),
|
||||
'error' => '',
|
||||
'error_deescription' => '',
|
||||
], followRedirects: false);
|
||||
$this->assertEquals(301, $response['headers']['status-code']);
|
||||
$this->assertStringContainsString('https://' . $domain, $response['headers']['location']);
|
||||
|
||||
// Ensure unknown domain cannot be redirect URL
|
||||
$response = $this->client->call(Client::METHOD_POST, '/account/sessions/magic-url', [
|
||||
'content-type' => 'application/json',
|
||||
|
||||
@@ -74,4 +74,60 @@ class OriginTest extends TestCase
|
||||
$this->assertEquals(false, $validator->isValid('random-scheme://localhost'));
|
||||
$this->assertEquals('Invalid Scheme. The scheme used (random-scheme) in the Origin (random-scheme://localhost) is not supported. If you are using a custom scheme, please change it to `appwrite-callback-<PROJECT_ID>`', $validator->getDescription());
|
||||
}
|
||||
|
||||
public function testGetAllowedHostnames(): void
|
||||
{
|
||||
$validator = new Origin(
|
||||
allowedHostnames: ['appwrite.io', 'localhost'],
|
||||
allowedSchemes: ['exp']
|
||||
);
|
||||
|
||||
$this->assertEquals(['appwrite.io', 'localhost'], $validator->getAllowedHostnames());
|
||||
}
|
||||
|
||||
public function testGetAllowedSchemes(): void
|
||||
{
|
||||
$validator = new Origin(
|
||||
allowedHostnames: ['appwrite.io'],
|
||||
allowedSchemes: ['exp', 'appwrite-callback-123']
|
||||
);
|
||||
|
||||
$this->assertEquals(['exp', 'appwrite-callback-123'], $validator->getAllowedSchemes());
|
||||
}
|
||||
|
||||
public function testSetAllowedHostnames(): void
|
||||
{
|
||||
$validator = new Origin(
|
||||
allowedHostnames: ['appwrite.io'],
|
||||
allowedSchemes: ['exp']
|
||||
);
|
||||
|
||||
$this->assertEquals(true, $validator->isValid('https://appwrite.io'));
|
||||
$this->assertEquals(false, $validator->isValid('https://example.com'));
|
||||
|
||||
$result = $validator->setAllowedHostnames(['example.com']);
|
||||
|
||||
$this->assertSame($validator, $result);
|
||||
$this->assertEquals(['example.com'], $validator->getAllowedHostnames());
|
||||
$this->assertEquals(true, $validator->isValid('https://example.com'));
|
||||
$this->assertEquals(false, $validator->isValid('https://appwrite.io'));
|
||||
}
|
||||
|
||||
public function testSetAllowedSchemes(): void
|
||||
{
|
||||
$validator = new Origin(
|
||||
allowedHostnames: ['appwrite.io'],
|
||||
allowedSchemes: ['exp']
|
||||
);
|
||||
|
||||
$this->assertEquals(true, $validator->isValid('exp://'));
|
||||
$this->assertEquals(false, $validator->isValid('appwrite-callback-456://'));
|
||||
|
||||
$result = $validator->setAllowedSchemes(['appwrite-callback-456']);
|
||||
|
||||
$this->assertSame($validator, $result);
|
||||
$this->assertEquals(['appwrite-callback-456'], $validator->getAllowedSchemes());
|
||||
$this->assertEquals(true, $validator->isValid('appwrite-callback-456://'));
|
||||
$this->assertEquals(false, $validator->isValid('exp://'));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user