mirror of
https://github.com/appwrite/appwrite.git
synced 2026-05-26 13:51:13 +00:00
Merge branch '1.8.x' into fix/cli-static-setresource
This commit is contained in:
@@ -81,6 +81,7 @@ RUN chmod +x /usr/local/bin/doctor && \
|
||||
chmod +x /usr/local/bin/worker-certificates && \
|
||||
chmod +x /usr/local/bin/worker-databases && \
|
||||
chmod +x /usr/local/bin/worker-deletes && \
|
||||
chmod +x /usr/local/bin/worker-executions && \
|
||||
chmod +x /usr/local/bin/worker-functions && \
|
||||
chmod +x /usr/local/bin/worker-mails && \
|
||||
chmod +x /usr/local/bin/worker-messaging && \
|
||||
|
||||
@@ -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,29 @@ 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) {
|
||||
$domains = \array_filter([
|
||||
parse_url($state['success'], PHP_URL_HOST) ?? '',
|
||||
parse_url($state['failure'], PHP_URL_HOST) ?? ''
|
||||
], fn ($domain) => \is_string($domain) && $domain !== '');
|
||||
|
||||
if (!empty($domains)) {
|
||||
$rules = $authorization->skip(fn () => $dbForPlatform->find('rules', [
|
||||
Query::equal('domain', \array_values(\array_unique($domains))),
|
||||
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\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);
|
||||
});
|
||||
|
||||
|
||||
+16
-19
@@ -8,7 +8,7 @@ use Appwrite\Auth\Key;
|
||||
use Appwrite\Event\Certificate;
|
||||
use Appwrite\Event\Delete as DeleteEvent;
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Event\Func;
|
||||
use Appwrite\Event\Execution;
|
||||
use Appwrite\Event\StatsUsage;
|
||||
use Appwrite\Extend\Exception as AppwriteException;
|
||||
use Appwrite\Network\Cors;
|
||||
@@ -60,7 +60,7 @@ Config::setParam('domainVerification', false);
|
||||
Config::setParam('cookieDomain', 'localhost');
|
||||
Config::setParam('cookieSamesite', Response::COOKIE_SAMESITE_NONE);
|
||||
|
||||
function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, Authorization $authorization, ?Key $apiKey, DeleteEvent $queueForDeletes, int $executionsRetentionCount)
|
||||
function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Event $queueForEvents, StatsUsage $queueForStatsUsage, Execution $queueForExecutions, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, Authorization $authorization, ?Key $apiKey, DeleteEvent $queueForDeletes, int $executionsRetentionCount)
|
||||
{
|
||||
$host = $request->getHostname() ?? '';
|
||||
if (!empty($previewHostname)) {
|
||||
@@ -696,14 +696,11 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
|
||||
throw $th;
|
||||
}
|
||||
} finally {
|
||||
if ($type === 'function') {
|
||||
$queueForFunctions
|
||||
->setType(Func::TYPE_ASYNC_WRITE)
|
||||
if ($type === 'function' || $type === 'site') {
|
||||
$queueForExecutions
|
||||
->setExecution($execution)
|
||||
->setProject($project)
|
||||
->trigger();
|
||||
} elseif ($type === 'site') { // TODO: Move it to logs worker later
|
||||
$dbForProject->createDocument('executions', $execution);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -877,7 +874,7 @@ Http::init()
|
||||
->inject('geodb')
|
||||
->inject('queueForStatsUsage')
|
||||
->inject('queueForEvents')
|
||||
->inject('queueForFunctions')
|
||||
->inject('queueForExecutions')
|
||||
->inject('executor')
|
||||
->inject('platform')
|
||||
->inject('isResourceBlocked')
|
||||
@@ -888,7 +885,7 @@ Http::init()
|
||||
->inject('authorization')
|
||||
->inject('queueForDeletes')
|
||||
->inject('executionsRetentionCount')
|
||||
->action(function (Http $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Document $project, Database $dbForPlatform, callable $getProjectDB, Locale $locale, array $localeCodes, Reader $geodb, StatsUsage $queueForStatsUsage, Event $queueForEvents, Func $queueForFunctions, Executor $executor, array $platform, callable $isResourceBlocked, string $previewHostname, Document $devKey, ?Key $apiKey, Cors $cors, Authorization $authorization, DeleteEvent $queueForDeletes, int $executionsRetentionCount) {
|
||||
->action(function (Http $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Document $project, Database $dbForPlatform, callable $getProjectDB, Locale $locale, array $localeCodes, Reader $geodb, StatsUsage $queueForStatsUsage, Event $queueForEvents, Execution $queueForExecutions, Executor $executor, array $platform, callable $isResourceBlocked, string $previewHostname, Document $devKey, ?Key $apiKey, Cors $cors, Authorization $authorization, DeleteEvent $queueForDeletes, int $executionsRetentionCount) {
|
||||
/*
|
||||
* Appwrite Router
|
||||
*/
|
||||
@@ -896,7 +893,7 @@ Http::init()
|
||||
$platformHostnames = $platform['hostnames'] ?? [];
|
||||
// Only run Router when external domain
|
||||
if (!\in_array($hostname, $platformHostnames) || !empty($previewHostname)) {
|
||||
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $queueForDeletes, $executionsRetentionCount)) {
|
||||
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $queueForStatsUsage, $queueForExecutions, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $queueForDeletes, $executionsRetentionCount)) {
|
||||
$utopia->getRoute()?->label('router', true);
|
||||
}
|
||||
}
|
||||
@@ -1175,7 +1172,7 @@ Http::options()
|
||||
->inject('getProjectDB')
|
||||
->inject('queueForEvents')
|
||||
->inject('queueForStatsUsage')
|
||||
->inject('queueForFunctions')
|
||||
->inject('queueForExecutions')
|
||||
->inject('executor')
|
||||
->inject('geodb')
|
||||
->inject('isResourceBlocked')
|
||||
@@ -1188,14 +1185,14 @@ Http::options()
|
||||
->inject('authorization')
|
||||
->inject('queueForDeletes')
|
||||
->inject('executionsRetentionCount')
|
||||
->action(function (Http $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, Document $project, Document $devKey, ?Key $apiKey, Cors $cors, Authorization $authorization, DeleteEvent $queueForDeletes, int $executionsRetentionCount) {
|
||||
->action(function (Http $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, StatsUsage $queueForStatsUsage, Execution $queueForExecutions, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, Document $project, Document $devKey, ?Key $apiKey, Cors $cors, Authorization $authorization, DeleteEvent $queueForDeletes, int $executionsRetentionCount) {
|
||||
/*
|
||||
* Appwrite Router
|
||||
*/
|
||||
$platformHostnames = $platform['hostnames'] ?? [];
|
||||
// Only run Router when external domain
|
||||
if (!in_array($request->getHostname(), $platformHostnames) || !empty($previewHostname)) {
|
||||
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $queueForDeletes, $executionsRetentionCount)) {
|
||||
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $queueForStatsUsage, $queueForExecutions, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $queueForDeletes, $executionsRetentionCount)) {
|
||||
$utopia->getRoute()?->label('router', true);
|
||||
}
|
||||
}
|
||||
@@ -1571,7 +1568,7 @@ Http::get('/robots.txt')
|
||||
->inject('getProjectDB')
|
||||
->inject('queueForEvents')
|
||||
->inject('queueForStatsUsage')
|
||||
->inject('queueForFunctions')
|
||||
->inject('queueForExecutions')
|
||||
->inject('executor')
|
||||
->inject('geodb')
|
||||
->inject('isResourceBlocked')
|
||||
@@ -1581,13 +1578,13 @@ Http::get('/robots.txt')
|
||||
->inject('authorization')
|
||||
->inject('queueForDeletes')
|
||||
->inject('executionsRetentionCount')
|
||||
->action(function (Http $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, ?Key $apiKey, Authorization $authorization, DeleteEvent $queueForDeletes, int $executionsRetentionCount) {
|
||||
->action(function (Http $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, StatsUsage $queueForStatsUsage, Execution $queueForExecutions, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, ?Key $apiKey, Authorization $authorization, DeleteEvent $queueForDeletes, int $executionsRetentionCount) {
|
||||
$platformHostnames = $platform['hostnames'] ?? [];
|
||||
if (in_array($request->getHostname(), $platformHostnames) || !empty($previewHostname)) {
|
||||
$template = new View(__DIR__ . '/../views/general/robots.phtml');
|
||||
$response->text($template->render(false));
|
||||
} else {
|
||||
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $queueForDeletes, $executionsRetentionCount)) {
|
||||
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $queueForStatsUsage, $queueForExecutions, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $queueForDeletes, $executionsRetentionCount)) {
|
||||
$utopia->getRoute()?->label('router', true);
|
||||
}
|
||||
}
|
||||
@@ -1606,7 +1603,7 @@ Http::get('/humans.txt')
|
||||
->inject('getProjectDB')
|
||||
->inject('queueForEvents')
|
||||
->inject('queueForStatsUsage')
|
||||
->inject('queueForFunctions')
|
||||
->inject('queueForExecutions')
|
||||
->inject('executor')
|
||||
->inject('geodb')
|
||||
->inject('isResourceBlocked')
|
||||
@@ -1616,13 +1613,13 @@ Http::get('/humans.txt')
|
||||
->inject('authorization')
|
||||
->inject('queueForDeletes')
|
||||
->inject('executionsRetentionCount')
|
||||
->action(function (Http $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, ?Key $apiKey, Authorization $authorization, DeleteEvent $queueForDeletes, int $executionsRetentionCount) {
|
||||
->action(function (Http $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, StatsUsage $queueForStatsUsage, Execution $queueForExecutions, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, ?Key $apiKey, Authorization $authorization, DeleteEvent $queueForDeletes, int $executionsRetentionCount) {
|
||||
$platformHostnames = $platform['hostnames'] ?? [];
|
||||
if (in_array($request->getHostname(), $platformHostnames) || !empty($previewHostname)) {
|
||||
$template = new View(__DIR__ . '/../views/general/humans.phtml');
|
||||
$response->text($template->render(false));
|
||||
} else {
|
||||
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $queueForDeletes, $executionsRetentionCount)) {
|
||||
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $queueForStatsUsage, $queueForExecutions, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $queueForDeletes, $executionsRetentionCount)) {
|
||||
$utopia->getRoute()?->label('router', true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -10,6 +10,7 @@ use Appwrite\Event\Certificate;
|
||||
use Appwrite\Event\Database as EventDatabase;
|
||||
use Appwrite\Event\Delete;
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Event\Execution;
|
||||
use Appwrite\Event\Func;
|
||||
use Appwrite\Event\Mail;
|
||||
use Appwrite\Event\Messaging;
|
||||
@@ -159,6 +160,9 @@ Http::setResource('queueForAudits', function (Publisher $publisher) {
|
||||
Http::setResource('queueForFunctions', function (Publisher $publisher) {
|
||||
return new Func($publisher);
|
||||
}, ['publisher']);
|
||||
Http::setResource('queueForExecutions', function (Publisher $publisher) {
|
||||
return new Execution($publisher);
|
||||
}, ['publisher']);
|
||||
Http::setResource('eventProcessor', function () {
|
||||
return new EventProcessor();
|
||||
}, []);
|
||||
|
||||
@@ -9,6 +9,7 @@ use Appwrite\Event\Certificate;
|
||||
use Appwrite\Event\Database as EventDatabase;
|
||||
use Appwrite\Event\Delete;
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Event\Execution;
|
||||
use Appwrite\Event\Func;
|
||||
use Appwrite\Event\Mail;
|
||||
use Appwrite\Event\Messaging;
|
||||
@@ -358,6 +359,10 @@ Server::setResource('queueForFunctions', function (Publisher $publisher) {
|
||||
return new Func($publisher);
|
||||
}, ['publisher']);
|
||||
|
||||
Server::setResource('queueForExecutions', function (Publisher $publisher) {
|
||||
return new Execution($publisher);
|
||||
}, ['publisher']);
|
||||
|
||||
Server::setResource('queueForRealtime', function () {
|
||||
return new Realtime();
|
||||
}, []);
|
||||
|
||||
Executable
+3
@@ -0,0 +1,3 @@
|
||||
#!/bin/sh
|
||||
|
||||
exec php /usr/src/code/app/worker.php executions "$@"
|
||||
@@ -641,6 +641,37 @@ services:
|
||||
- _APP_LOGGING_CONFIG
|
||||
- _APP_DATABASE_SHARED_TABLES
|
||||
|
||||
appwrite-worker-executions:
|
||||
entrypoint: worker-executions
|
||||
<<: *x-logging
|
||||
container_name: appwrite-worker-executions
|
||||
image: appwrite-dev
|
||||
networks:
|
||||
- appwrite
|
||||
volumes:
|
||||
- ./app:/usr/src/code/app
|
||||
- ./src:/usr/src/code/src
|
||||
depends_on:
|
||||
redis:
|
||||
condition: service_started
|
||||
mariadb:
|
||||
condition: service_started
|
||||
environment:
|
||||
- _APP_ENV
|
||||
- _APP_WORKER_PER_CORE
|
||||
- _APP_POOL_ADAPTER
|
||||
- _APP_REDIS_HOST
|
||||
- _APP_REDIS_PORT
|
||||
- _APP_REDIS_USER
|
||||
- _APP_REDIS_PASS
|
||||
- _APP_DB_HOST
|
||||
- _APP_DB_PORT
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
- _APP_LOGGING_CONFIG
|
||||
- _APP_DATABASE_SHARED_TABLES
|
||||
|
||||
appwrite-worker-functions:
|
||||
entrypoint: worker-functions
|
||||
<<: *x-logging
|
||||
|
||||
@@ -46,6 +46,9 @@ class Event
|
||||
public const MESSAGING_QUEUE_NAME = 'v1-messaging';
|
||||
public const MESSAGING_CLASS_NAME = 'MessagingV1';
|
||||
|
||||
public const EXECUTIONS_QUEUE_NAME = 'v1-executions';
|
||||
public const EXECUTIONS_CLASS_NAME = 'ExecutionsV1';
|
||||
|
||||
public const MIGRATIONS_QUEUE_NAME = 'v1-migrations';
|
||||
public const MIGRATIONS_CLASS_NAME = 'MigrationsV1';
|
||||
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Event;
|
||||
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Queue\Publisher;
|
||||
|
||||
class Execution extends Event
|
||||
{
|
||||
protected ?Document $execution = null;
|
||||
|
||||
public function __construct(protected Publisher $publisher)
|
||||
{
|
||||
parent::__construct($publisher);
|
||||
|
||||
$this
|
||||
->setQueue(Event::EXECUTIONS_QUEUE_NAME)
|
||||
->setClass(Event::EXECUTIONS_CLASS_NAME);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets execution document for the execution event.
|
||||
*
|
||||
* @param Document $execution
|
||||
* @return self
|
||||
*/
|
||||
public function setExecution(Document $execution): self
|
||||
{
|
||||
$this->execution = $execution;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns set execution document for the execution event.
|
||||
*
|
||||
* @return null|Document
|
||||
*/
|
||||
public function getExecution(): ?Document
|
||||
{
|
||||
return $this->execution;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare payload for the execution event.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function preparePayload(): array
|
||||
{
|
||||
return [
|
||||
'project' => $this->project,
|
||||
'execution' => $this->execution,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -9,8 +9,6 @@ use Utopia\System\System;
|
||||
|
||||
class Func extends Event
|
||||
{
|
||||
public const TYPE_ASYNC_WRITE = 'async_write';
|
||||
|
||||
protected string $jwt = '';
|
||||
protected string $type = '';
|
||||
protected string $body = '';
|
||||
|
||||
@@ -86,4 +86,11 @@ class StatsUsage extends Event
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
public function reset(): Event
|
||||
{
|
||||
$this->metrics = [];
|
||||
parent::reset();
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -5,6 +5,7 @@ namespace Appwrite\Platform\Services;
|
||||
use Appwrite\Platform\Workers\Audits;
|
||||
use Appwrite\Platform\Workers\Certificates;
|
||||
use Appwrite\Platform\Workers\Deletes;
|
||||
use Appwrite\Platform\Workers\Executions;
|
||||
use Appwrite\Platform\Workers\Functions;
|
||||
use Appwrite\Platform\Workers\Mails;
|
||||
use Appwrite\Platform\Workers\Messaging;
|
||||
@@ -23,6 +24,7 @@ class Workers extends Service
|
||||
->addAction(Audits::getName(), new Audits())
|
||||
->addAction(Certificates::getName(), new Certificates())
|
||||
->addAction(Deletes::getName(), new Deletes())
|
||||
->addAction(Executions::getName(), new Executions())
|
||||
->addAction(Functions::getName(), new Functions())
|
||||
->addAction(Mails::getName(), new Mails())
|
||||
->addAction(Messaging::getName(), new Messaging())
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Workers;
|
||||
|
||||
use Exception;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Platform\Action;
|
||||
use Utopia\Queue\Message;
|
||||
use Utopia\System\System;
|
||||
|
||||
class Executions extends Action
|
||||
{
|
||||
public static function getName(): string
|
||||
{
|
||||
return 'executions';
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->desc('Executions worker')
|
||||
->groups(['executions'])
|
||||
->inject('message')
|
||||
->inject('dbForProject')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(
|
||||
Message $message,
|
||||
Database $dbForProject,
|
||||
): void {
|
||||
$payload = $message->getPayload() ?? [];
|
||||
|
||||
if (empty($payload)) {
|
||||
throw new Exception('Missing payload');
|
||||
}
|
||||
|
||||
$execution = new Document($payload['execution'] ?? []);
|
||||
|
||||
if ($execution->isEmpty()) {
|
||||
throw new Exception('Missing execution');
|
||||
}
|
||||
|
||||
if (System::getEnv('_APP_REGION') !== 'nyc') { // TODO: Remove region check
|
||||
$dbForProject->upsertDocument('executions', $execution);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ namespace Appwrite\Platform\Workers;
|
||||
|
||||
use Ahc\Jwt\JWT;
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Event\Execution as ExecutionEvent;
|
||||
use Appwrite\Event\Func;
|
||||
use Appwrite\Event\Realtime;
|
||||
use Appwrite\Event\StatsUsage;
|
||||
@@ -16,8 +17,6 @@ use Utopia\Config\Config;
|
||||
use Utopia\Console;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Exception\Conflict;
|
||||
use Utopia\Database\Exception\Structure;
|
||||
use Utopia\Database\Helpers\ID;
|
||||
use Utopia\Database\Helpers\Permission;
|
||||
use Utopia\Database\Helpers\Role;
|
||||
@@ -50,6 +49,7 @@ class Functions extends Action
|
||||
->inject('queueForRealtime')
|
||||
->inject('queueForEvents')
|
||||
->inject('queueForStatsUsage')
|
||||
->inject('queueForExecutions')
|
||||
->inject('log')
|
||||
->inject('executor')
|
||||
->inject('isResourceBlocked')
|
||||
@@ -65,6 +65,7 @@ class Functions extends Action
|
||||
Realtime $queueForRealtime,
|
||||
Event $queueForEvents,
|
||||
StatsUsage $queueForStatsUsage,
|
||||
ExecutionEvent $queueForExecutions,
|
||||
Log $log,
|
||||
Executor $executor,
|
||||
callable $isResourceBlocked
|
||||
@@ -77,15 +78,6 @@ class Functions extends Action
|
||||
|
||||
$type = $payload['type'] ?? '';
|
||||
|
||||
// Short-term solution to offhand write operation from API container
|
||||
if ($type === Func::TYPE_ASYNC_WRITE) {
|
||||
$execution = new Document($payload['execution'] ?? []);
|
||||
if (System::getEnv('_APP_REGION') !== 'nyc') { // TODO: Remove region check
|
||||
$dbForProject->createDocument('executions', $execution);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
$events = $payload['events'] ?? [];
|
||||
$data = $payload['body'] ?? '';
|
||||
$eventData = $payload['payload'] ?? '';
|
||||
@@ -166,6 +158,7 @@ class Functions extends Action
|
||||
queueForRealtime: $queueForRealtime,
|
||||
queueForStatsUsage: $queueForStatsUsage,
|
||||
queueForEvents: $queueForEvents,
|
||||
queueForExecutions: $queueForExecutions,
|
||||
project: $project,
|
||||
function: $function,
|
||||
executor: $executor,
|
||||
@@ -210,6 +203,7 @@ class Functions extends Action
|
||||
queueForRealtime: $queueForRealtime,
|
||||
queueForStatsUsage: $queueForStatsUsage,
|
||||
queueForEvents: $queueForEvents,
|
||||
queueForExecutions: $queueForExecutions,
|
||||
project: $project,
|
||||
function: $function,
|
||||
executor: $executor,
|
||||
@@ -236,6 +230,7 @@ class Functions extends Action
|
||||
queueForRealtime: $queueForRealtime,
|
||||
queueForStatsUsage: $queueForStatsUsage,
|
||||
queueForEvents: $queueForEvents,
|
||||
queueForExecutions: $queueForExecutions,
|
||||
project: $project,
|
||||
function: $function,
|
||||
executor: $executor,
|
||||
@@ -268,7 +263,8 @@ class Functions extends Action
|
||||
*/
|
||||
private function fail(
|
||||
string $message,
|
||||
Database $dbForProject,
|
||||
Document $project,
|
||||
ExecutionEvent $queueForExecutions,
|
||||
Document $function,
|
||||
string $trigger,
|
||||
string $path,
|
||||
@@ -311,13 +307,10 @@ class Functions extends Action
|
||||
'duration' => 0.0,
|
||||
]);
|
||||
|
||||
if (System::getEnv('_APP_REGION') !== 'nyc') { // TODO: Remove region check
|
||||
$execution = $dbForProject->createDocument('executions', $execution);
|
||||
|
||||
if ($execution->isEmpty()) {
|
||||
throw new Exception('Failed to create execution');
|
||||
}
|
||||
}
|
||||
$queueForExecutions
|
||||
->setExecution($execution)
|
||||
->setProject($project)
|
||||
->trigger();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -341,9 +334,6 @@ class Functions extends Action
|
||||
* @param string|null $eventData
|
||||
* @param string|null $executionId
|
||||
* @return void
|
||||
* @throws Structure
|
||||
* @throws \Utopia\Database\Exception
|
||||
* @throws Conflict
|
||||
*/
|
||||
private function execute(
|
||||
Log $log,
|
||||
@@ -353,6 +343,7 @@ class Functions extends Action
|
||||
Realtime $queueForRealtime,
|
||||
StatsUsage $queueForStatsUsage,
|
||||
Event $queueForEvents,
|
||||
ExecutionEvent $queueForExecutions,
|
||||
Document $project,
|
||||
Document $function,
|
||||
Executor $executor,
|
||||
@@ -380,19 +371,19 @@ class Functions extends Action
|
||||
|
||||
if ($deployment->getAttribute('resourceId') !== $functionId) {
|
||||
$errorMessage = 'The execution could not be completed because a corresponding deployment was not found. A function deployment needs to be created before it can be executed. Please create a deployment for your function and try again.';
|
||||
$this->fail($errorMessage, $dbForProject, $function, $trigger, $path, $method, $user, $jwt, $event);
|
||||
$this->fail($errorMessage, $project, $queueForExecutions, $function, $trigger, $path, $method, $user, $jwt, $event);
|
||||
return;
|
||||
}
|
||||
|
||||
if ($deployment->isEmpty()) {
|
||||
$errorMessage = 'The execution could not be completed because a corresponding deployment was not found. A function deployment needs to be created before it can be executed. Please create a deployment for your function and try again.';
|
||||
$this->fail($errorMessage, $dbForProject, $function, $trigger, $path, $method, $user, $jwt, $event);
|
||||
$this->fail($errorMessage, $project, $queueForExecutions, $function, $trigger, $path, $method, $user, $jwt, $event);
|
||||
return;
|
||||
}
|
||||
|
||||
if ($deployment->getAttribute('status') !== 'ready') {
|
||||
$errorMessage = 'The execution could not be completed because the build is not ready. Please wait for the build to complete and try again.';
|
||||
$this->fail($errorMessage, $dbForProject, $function, $trigger, $path, $method, $user, $jwt, $event);
|
||||
$this->fail($errorMessage, $project, $queueForExecutions, $function, $trigger, $path, $method, $user, $jwt, $event);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -423,60 +414,38 @@ class Functions extends Action
|
||||
$headers['x-appwrite-continent-code'] = '';
|
||||
$headers['x-appwrite-continent-eu'] = 'false';
|
||||
|
||||
/** Create execution or update execution status */
|
||||
$execution = $dbForProject->getDocument('executions', $executionId ?? '');
|
||||
if ($execution->isEmpty()) {
|
||||
/** Create or update execution to processing status */
|
||||
if (empty($executionId)) {
|
||||
$executionId = ID::unique();
|
||||
$headers['x-appwrite-execution-id'] = $executionId;
|
||||
$headersFiltered = [];
|
||||
foreach ($headers as $key => $value) {
|
||||
if (\in_array(\strtolower($key), FUNCTION_ALLOWLIST_HEADERS_REQUEST)) {
|
||||
$headersFiltered[] = [ 'name' => $key, 'value' => $value ];
|
||||
}
|
||||
}
|
||||
}
|
||||
$headers['x-appwrite-execution-id'] = $executionId;
|
||||
|
||||
$execution = new Document([
|
||||
'$id' => $executionId,
|
||||
'$permissions' => $user->isEmpty() ? [] : [Permission::read(Role::user($user->getId()))],
|
||||
'resourceInternalId' => $function->getSequence(),
|
||||
'resourceId' => $function->getId(),
|
||||
'resourceType' => 'functions',
|
||||
'deploymentInternalId' => $deployment->getSequence(),
|
||||
'deploymentId' => $deployment->getId(),
|
||||
'trigger' => $trigger,
|
||||
'status' => 'processing',
|
||||
'responseStatusCode' => 0,
|
||||
'responseHeaders' => [],
|
||||
'requestPath' => $path,
|
||||
'requestMethod' => $method,
|
||||
'requestHeaders' => $headersFiltered,
|
||||
'errors' => '',
|
||||
'logs' => '',
|
||||
'duration' => 0.0,
|
||||
]);
|
||||
|
||||
if (System::getEnv('_APP_REGION') !== 'nyc') { // TODO: Remove region check
|
||||
$execution = $dbForProject->createDocument('executions', $execution);
|
||||
|
||||
// TODO: @Meldiron Trigger executions.create event here
|
||||
|
||||
if ($execution->isEmpty()) {
|
||||
throw new Exception('Failed to create or read execution');
|
||||
}
|
||||
$headersFiltered = [];
|
||||
foreach ($headers as $key => $value) {
|
||||
if (\in_array(\strtolower($key), FUNCTION_ALLOWLIST_HEADERS_REQUEST)) {
|
||||
$headersFiltered[] = [ 'name' => $key, 'value' => $value ];
|
||||
}
|
||||
}
|
||||
|
||||
if ($execution->getAttribute('status') !== 'processing') {
|
||||
$execution->setAttribute('status', 'processing');
|
||||
|
||||
if (System::getEnv('_APP_REGION') !== 'nyc') { // TODO: Remove region check
|
||||
try {
|
||||
$execution = $dbForProject->updateDocument('executions', $executionId, $execution);
|
||||
} catch (\Throwable $e) {
|
||||
$log->addExtra('updateError', $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
$execution = new Document([
|
||||
'$id' => $executionId,
|
||||
'$permissions' => $user->isEmpty() ? [] : [Permission::read(Role::user($user->getId()))],
|
||||
'resourceInternalId' => $function->getSequence(),
|
||||
'resourceId' => $function->getId(),
|
||||
'resourceType' => 'functions',
|
||||
'deploymentInternalId' => $deployment->getSequence(),
|
||||
'deploymentId' => $deployment->getId(),
|
||||
'trigger' => $trigger,
|
||||
'status' => 'processing',
|
||||
'responseStatusCode' => 0,
|
||||
'responseHeaders' => [],
|
||||
'requestPath' => $path,
|
||||
'requestMethod' => $method,
|
||||
'requestHeaders' => $headersFiltered,
|
||||
'errors' => '',
|
||||
'logs' => '',
|
||||
'duration' => 0.0,
|
||||
]);
|
||||
|
||||
$durationStart = \microtime(true);
|
||||
|
||||
@@ -618,14 +587,11 @@ class Functions extends Action
|
||||
$error = $th->getMessage();
|
||||
$errorCode = $th->getCode();
|
||||
} finally {
|
||||
/** Update execution status */
|
||||
if (System::getEnv('_APP_REGION') !== 'nyc') { // TODO: Remove region check
|
||||
try {
|
||||
$execution = $dbForProject->updateDocument('executions', $executionId, $execution);
|
||||
} catch (\Throwable $e) {
|
||||
$log->addExtra('updateError', $e->getMessage());
|
||||
}
|
||||
}
|
||||
/** Persist final execution status */
|
||||
$queueForExecutions
|
||||
->setExecution($execution)
|
||||
->setProject($project)
|
||||
->trigger();
|
||||
|
||||
/** Trigger usage queue */
|
||||
$queueForStatsUsage
|
||||
|
||||
@@ -5,6 +5,7 @@ namespace Appwrite\Platform\Workers;
|
||||
use Ahc\Jwt\JWT;
|
||||
use Appwrite\Event\Mail;
|
||||
use Appwrite\Event\Realtime;
|
||||
use Appwrite\Event\StatsUsage;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Template\Template;
|
||||
use Utopia\Config\Config;
|
||||
@@ -25,6 +26,10 @@ use Utopia\Migration\Destination;
|
||||
use Utopia\Migration\Destinations\Appwrite as DestinationAppwrite;
|
||||
use Utopia\Migration\Destinations\CSV as DestinationCSV;
|
||||
use Utopia\Migration\Exception as MigrationException;
|
||||
use Utopia\Migration\Resource;
|
||||
use Utopia\Migration\Resources\Database\Database as ResourceDatabase;
|
||||
use Utopia\Migration\Resources\Database\Row as ResourceRow;
|
||||
use Utopia\Migration\Resources\Database\Table as ResourceTable;
|
||||
use Utopia\Migration\Source;
|
||||
use Utopia\Migration\Sources\Appwrite as SourceAppwrite;
|
||||
use Utopia\Migration\Sources\CSV;
|
||||
@@ -52,6 +57,7 @@ class Migrations extends Action
|
||||
*/
|
||||
protected array $sourceReport = [];
|
||||
|
||||
private string $source;
|
||||
/**
|
||||
* @var callable|null
|
||||
*/
|
||||
@@ -78,6 +84,7 @@ class Migrations extends Action
|
||||
->inject('deviceForMigrations')
|
||||
->inject('deviceForFiles')
|
||||
->inject('queueForMails')
|
||||
->inject('queueForStatsUsage')
|
||||
->inject('plan')
|
||||
->inject('authorization')
|
||||
->callback($this->action(...));
|
||||
@@ -96,6 +103,7 @@ class Migrations extends Action
|
||||
Device $deviceForMigrations,
|
||||
Device $deviceForFiles,
|
||||
Mail $queueForMails,
|
||||
StatsUsage $queueForStatsUsage,
|
||||
array $plan,
|
||||
Authorization $authorization,
|
||||
): void {
|
||||
@@ -139,6 +147,7 @@ class Migrations extends Action
|
||||
$migration,
|
||||
$queueForRealtime,
|
||||
$queueForMails,
|
||||
$queueForStatsUsage,
|
||||
$platform,
|
||||
$authorization
|
||||
);
|
||||
@@ -324,6 +333,7 @@ class Migrations extends Action
|
||||
Document $migration,
|
||||
Realtime $queueForRealtime,
|
||||
Mail $queueForMails,
|
||||
StatsUsage $queueForStatsUsage,
|
||||
array $platform,
|
||||
Authorization $authorization,
|
||||
): void {
|
||||
@@ -368,6 +378,7 @@ class Migrations extends Action
|
||||
$destination
|
||||
);
|
||||
|
||||
$aggregatedResources = [];
|
||||
/** Start Transfer */
|
||||
if (empty($source->getErrors())) {
|
||||
$migration->setAttribute('stage', 'migrating');
|
||||
@@ -375,9 +386,40 @@ class Migrations extends Action
|
||||
|
||||
$transfer->run(
|
||||
$migration->getAttribute('resources'),
|
||||
function () use ($migration, $transfer, $project, $queueForRealtime) {
|
||||
function ($resources) use ($migration, $transfer, $project, $queueForRealtime, &$aggregatedResources) {
|
||||
$migration->setAttribute('resourceData', json_encode($transfer->getCache()));
|
||||
$migration->setAttribute('statusCounters', json_encode($transfer->getStatusCounters()));
|
||||
|
||||
if (!empty($resources)) {
|
||||
/**
|
||||
* @var Resource $resource
|
||||
*/
|
||||
$resource = $resources[0];
|
||||
$count = count($resources);
|
||||
$databaseId = null;
|
||||
$tableId = null;
|
||||
switch ($resource->getName()) {
|
||||
case ResourceTable::getName():
|
||||
/** @var ResourceTable $resource */
|
||||
$databaseId = $resource->getDatabase()->getSequence();
|
||||
break;
|
||||
case ResourceRow::getName():
|
||||
/** @var ResourceRow $resource */
|
||||
$table = $resource->getTable();
|
||||
$databaseId = $table->getDatabase()->getSequence();
|
||||
$tableId = $table->getSequence();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
$aggregatedResources[] = [
|
||||
'name' => $resource->getName(),
|
||||
'count' => $count,
|
||||
'databaseId' => $databaseId,
|
||||
'tableId' => $tableId
|
||||
];
|
||||
|
||||
}
|
||||
$this->updateMigrationDocument($migration, $project, $queueForRealtime);
|
||||
},
|
||||
$migration->getAttribute('resourceId'),
|
||||
@@ -443,6 +485,16 @@ class Migrations extends Action
|
||||
}
|
||||
|
||||
if ($migration->getAttribute('status', '') === 'completed') {
|
||||
foreach ($aggregatedResources as $resource) {
|
||||
$this->processMigrationResourceStats(
|
||||
$resource,
|
||||
$queueForStatsUsage,
|
||||
$project,
|
||||
$migration->getAttribute('source'),
|
||||
$authorization,
|
||||
$migration->getAttribute('resourceId')
|
||||
);
|
||||
}
|
||||
$destination?->success();
|
||||
$source?->success();
|
||||
|
||||
@@ -737,4 +789,58 @@ class Migrations extends Action
|
||||
|
||||
return $errors;
|
||||
}
|
||||
|
||||
private function processMigrationResourceStats(array $resources, StatsUsage $queueForStatsUsage, Document $projectDocument, string $source, Authorization $authorization, ?string $resourceId)
|
||||
{
|
||||
$resourceName = $resources['name'];
|
||||
$count = $resources['count'];
|
||||
$databaseInternalId = $resources['databaseId'];
|
||||
$tableInternalId = $resources['tableId'];
|
||||
|
||||
if ($source === CSV::getName()) {
|
||||
[$databaseId, $tableId] = explode(':', $resourceId);
|
||||
$database = $authorization->skip(fn () => $this->dbForProject->getDocument('databases', $databaseId));
|
||||
$table = $authorization->skip(fn () => $this->dbForProject->getDocument('database_' . $database->getSequence(), $tableId));
|
||||
$databaseInternalId = (int) $database->getSequence();
|
||||
$tableInternalId = (int) $table->getSequence();
|
||||
}
|
||||
|
||||
switch ($resourceName) {
|
||||
case ResourceDatabase::getName():
|
||||
$queueForStatsUsage->addMetric(METRIC_DATABASES, $count);
|
||||
break;
|
||||
|
||||
case ResourceTable::getName():
|
||||
$queueForStatsUsage
|
||||
->addMetric(METRIC_COLLECTIONS, $count)
|
||||
->addMetric(
|
||||
str_replace('{databaseInternalId}', $databaseInternalId, METRIC_DATABASE_ID_COLLECTIONS),
|
||||
$count
|
||||
);
|
||||
break;
|
||||
|
||||
case ResourceRow::getName():
|
||||
$queueForStatsUsage
|
||||
->addMetric(
|
||||
str_replace(
|
||||
['{databaseInternalId}','{collectionInternalId}'],
|
||||
[$databaseInternalId, $tableInternalId],
|
||||
METRIC_DATABASE_ID_COLLECTION_ID_DOCUMENTS
|
||||
),
|
||||
$count
|
||||
)
|
||||
->addMetric(
|
||||
str_replace('{databaseInternalId}', $databaseInternalId, METRIC_DATABASE_ID_DOCUMENTS),
|
||||
$count
|
||||
)
|
||||
->addMetric(METRIC_DOCUMENTS, $count);
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
$queueForStatsUsage->setProject($projectDocument)->trigger();
|
||||
$queueForStatsUsage->reset();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
]);
|
||||
|
||||
@@ -1751,7 +1751,10 @@ class FunctionsCustomServerTest extends Scope
|
||||
$this->assertEventually(function () use ($functionId, $userId) {
|
||||
$executions = $this->listExecutions($functionId);
|
||||
|
||||
$lastExecution = $executions['body']['executions'][0];
|
||||
$this->assertEquals(200, $executions['headers']['status-code']);
|
||||
$executionsList = $executions['body']['executions'] ?? [];
|
||||
$this->assertNotEmpty($executionsList);
|
||||
$lastExecution = $executionsList[0];
|
||||
|
||||
$this->assertEquals('completed', $lastExecution['status']);
|
||||
$this->assertEquals(204, $lastExecution['responseStatusCode']);
|
||||
|
||||
@@ -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',
|
||||
@@ -5230,6 +5561,55 @@ class ProjectsConsoleClientTest extends Scope
|
||||
$this->assertEquals(201, $response['headers']['status-code']);
|
||||
}
|
||||
|
||||
public function testOAuthRedirectWithCustomSchemeState(): void
|
||||
{
|
||||
// Prepare project
|
||||
$projectId = $this->setupProject([
|
||||
'projectId' => ID::unique(),
|
||||
'name' => 'testOAuthRedirectWithCustomSchemeState',
|
||||
'region' => System::getEnv('_APP_REGION', 'default')
|
||||
]);
|
||||
|
||||
$provider = 'mock';
|
||||
$appId = '1';
|
||||
$secret = '123456';
|
||||
|
||||
// Prepare OAuth provider
|
||||
$response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $projectId . '/oauth2', array_merge([
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'provider' => $provider,
|
||||
'appId' => $appId,
|
||||
'secret' => $secret,
|
||||
'enabled' => true,
|
||||
]);
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
|
||||
$scheme = 'appwrite-callback-' . $projectId;
|
||||
$state = \json_encode([
|
||||
'success' => $scheme . ':///',
|
||||
'failure' => $scheme . ':///'
|
||||
]);
|
||||
|
||||
$response = $this->client->call(Client::METHOD_GET, '/account/sessions/oauth2/' . $provider . '/redirect', [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $projectId,
|
||||
'origin' => '',
|
||||
'referer' => '',
|
||||
], [
|
||||
'code' => 'any-code',
|
||||
'state' => $state,
|
||||
'error' => 'access_denied',
|
||||
'error_description' => 'test',
|
||||
], followRedirects: false);
|
||||
|
||||
$this->assertEquals(301, $response['headers']['status-code']);
|
||||
$this->assertStringStartsWith($scheme . '://', $response['headers']['location']);
|
||||
$this->assertStringContainsString('error=', $response['headers']['location']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @group abuseEnabled
|
||||
*/
|
||||
|
||||
@@ -212,6 +212,27 @@ services:
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
|
||||
appwrite-worker-executions:
|
||||
entrypoint: worker-executions
|
||||
container_name: appwrite-worker-executions
|
||||
build:
|
||||
context: .
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- appwrite
|
||||
depends_on:
|
||||
- redis
|
||||
- mariadb
|
||||
environment:
|
||||
- _APP_ENV
|
||||
- _APP_REDIS_HOST
|
||||
- _APP_REDIS_PORT
|
||||
- _APP_DB_HOST
|
||||
- _APP_DB_PORT
|
||||
- _APP_DB_SCHEMA
|
||||
- _APP_DB_USER
|
||||
- _APP_DB_PASS
|
||||
|
||||
appwrite-worker-functions:
|
||||
entrypoint: worker-functions
|
||||
container_name: appwrite-worker-functions
|
||||
|
||||
@@ -23,7 +23,7 @@ class ComposeTest extends TestCase
|
||||
|
||||
public function testServices(): void
|
||||
{
|
||||
$this->assertCount(15, $this->object->getServices());
|
||||
$this->assertCount(16, $this->object->getServices());
|
||||
$this->assertEquals('appwrite', $this->object->getService('appwrite')->getContainerName());
|
||||
$this->assertEquals('', $this->object->getService('appwrite')->getImageVersion());
|
||||
$this->assertEquals('3.6', $this->object->getService('traefik')->getImageVersion());
|
||||
|
||||
@@ -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