mirror of
https://github.com/appwrite/appwrite
synced 2026-05-23 08:58:35 +00:00
Merge branch '1.8.x' into fix-10612-validate-relationship-document-id
This commit is contained in:
commit
23c1f8fd5a
27 changed files with 736 additions and 129 deletions
|
|
@ -26,8 +26,8 @@ unset($common['files']);
|
|||
$collections = [
|
||||
'buckets' => $buckets,
|
||||
'databases' => $databases,
|
||||
'projects' => array_merge($projects, $common),
|
||||
'console' => array_merge($platform, $common),
|
||||
'projects' => array_merge_recursive($projects, $common),
|
||||
'console' => array_merge_recursive($platform, $common),
|
||||
'logs' => $logs,
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -633,29 +633,7 @@ $platformCollections = [
|
|||
'name' => 'keys',
|
||||
'attributes' => [
|
||||
[
|
||||
'$id' => ID::custom('projectInternalId'),
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => Database::LENGTH_KEY,
|
||||
'signed' => true,
|
||||
'required' => true,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('projectId'),
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => Database::LENGTH_KEY,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => 0,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => 'resourceType',
|
||||
'$id' => ID::custom('resourceType'),
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => Database::LENGTH_KEY,
|
||||
|
|
@ -666,7 +644,7 @@ $platformCollections = [
|
|||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => 'resourceId',
|
||||
'$id' => ID::custom('resourceId'),
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => Database::LENGTH_KEY,
|
||||
|
|
@ -677,7 +655,7 @@ $platformCollections = [
|
|||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => 'resourceInternalId',
|
||||
'$id' => ID::custom('resourceInternalId'),
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => Database::LENGTH_KEY,
|
||||
|
|
@ -756,21 +734,14 @@ $platformCollections = [
|
|||
],
|
||||
'indexes' => [
|
||||
[
|
||||
'$id' => ID::custom('_key_project'),
|
||||
'type' => Database::INDEX_KEY,
|
||||
'attributes' => ['projectInternalId'],
|
||||
'lengths' => [Database::LENGTH_KEY],
|
||||
'orders' => [Database::ORDER_ASC],
|
||||
],
|
||||
[
|
||||
'$id' => '_key_resource',
|
||||
'$id' => ID::custom('_key_resource'),
|
||||
'type' => Database::INDEX_KEY,
|
||||
'attributes' => ['resourceType', 'resourceInternalId'],
|
||||
'lengths' => [Database::LENGTH_KEY],
|
||||
'orders' => [Database::ORDER_ASC],
|
||||
'lengths' => [],
|
||||
'orders' => [],
|
||||
],
|
||||
[
|
||||
'$id' => '_key_accessedAt',
|
||||
'$id' => ID::custom('_key_accessedAt'),
|
||||
'type' => Database::INDEX_KEY,
|
||||
'attributes' => ['accessedAt'],
|
||||
'lengths' => [],
|
||||
|
|
|
|||
|
|
@ -357,6 +357,11 @@ return [
|
|||
'description' => 'API key and session used in the same request. Use either `setSession` or `setKey`. Learn about which authentication method to use in the SSR docs: https://appwrite.io/docs/products/auth/server-side-rendering',
|
||||
'code' => 403,
|
||||
],
|
||||
Exception::USER_JWT_AND_COOKIE_SET => [
|
||||
'name' => Exception::USER_JWT_AND_COOKIE_SET,
|
||||
'description' => 'JWT and cookie used in the same request. Use either `setJWT` or `setCookie`. Learn about which authentication method to use in the SSR docs: https://appwrite.io/docs/products/auth/server-side-rendering',
|
||||
'code' => 403,
|
||||
],
|
||||
Exception::API_KEY_EXPIRED => [
|
||||
'name' => Exception::API_KEY_EXPIRED,
|
||||
'description' => 'The dynamic API key has expired. Please don\'t use dynamic API keys for more than duration of the execution.',
|
||||
|
|
@ -1074,6 +1079,11 @@ return [
|
|||
'description' => 'The project key has expired. Please generate a new key using the Appwrite console.',
|
||||
'code' => 401,
|
||||
],
|
||||
Exception::ACCOUNT_KEY_EXPIRED => [
|
||||
'name' => Exception::ACCOUNT_KEY_EXPIRED,
|
||||
'description' => 'The account API key has expired. Please generate a new key using the Appwrite console.',
|
||||
'code' => 401,
|
||||
],
|
||||
Exception::ROUTER_HOST_NOT_FOUND => [
|
||||
'name' => Exception::ROUTER_HOST_NOT_FOUND,
|
||||
'description' => 'Host is not trusted. This could occur because you have not configured a custom domain. Add a custom domain to your project first and try again.',
|
||||
|
|
@ -1328,4 +1338,19 @@ return [
|
|||
'description' => 'Target has an invalid provider type.',
|
||||
'code' => 400,
|
||||
],
|
||||
Exception::USER_ID_MISSING => [
|
||||
'name' => Exception::USER_ID_MISSING,
|
||||
'description' => 'When using account API key, make sure to pass x-appwrite-user header with your user ID.',
|
||||
'code' => 403,
|
||||
],
|
||||
Exception::ORGANIZATION_ID_MISSING => [
|
||||
'name' => Exception::ORGANIZATION_ID_MISSING,
|
||||
'description' => 'When using organization API key, make sure to pass x-appwrite-organization header with your organization ID.',
|
||||
'code' => 403,
|
||||
],
|
||||
Exception::PROJECT_ID_MISSING => [
|
||||
'name' => Exception::PROJECT_ID_MISSING,
|
||||
'description' => 'When using project API key, make sure to pass x-appwrite-project header with your project ID.',
|
||||
'code' => 403,
|
||||
],
|
||||
];
|
||||
|
|
|
|||
15
app/config/scopes/account.php
Normal file
15
app/config/scopes/account.php
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<?php
|
||||
|
||||
// List of scopes for Account API keys (Tokens)
|
||||
|
||||
return [
|
||||
"account" => [
|
||||
"description" => 'Access to manage account, its organizations, sessions, tokens, and billing.',
|
||||
],
|
||||
"teams.read" => [
|
||||
"description" => 'Access to read account\'s organizations.',
|
||||
],
|
||||
"teams.write" => [
|
||||
"description" => 'Access to create, update and delete account\'s organizations and its memberships.',
|
||||
],
|
||||
];
|
||||
42
app/config/scopes/organization.php
Normal file
42
app/config/scopes/organization.php
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
<?php
|
||||
|
||||
// List of scopes for organization (teams) API keys
|
||||
|
||||
return [
|
||||
"platforms.read" => [
|
||||
"description" => 'Access to read project\'s platforms',
|
||||
],
|
||||
"platforms.write" => [
|
||||
"description" =>
|
||||
'Access to create, update, and delete project\'s platforms',
|
||||
],
|
||||
"projects.read" => [
|
||||
"description" => 'Access to read organization\'s projects',
|
||||
],
|
||||
"projects.write" => [
|
||||
"description" =>
|
||||
"Access to create, update, and delete projects in organization",
|
||||
],
|
||||
"keys.read" => [
|
||||
"description" => 'Access to read project\'s API keys',
|
||||
],
|
||||
"keys.write" => [
|
||||
"description" =>
|
||||
"Access to create, update, and delete project\'s API keys",
|
||||
],
|
||||
"devKeys.read" => [
|
||||
"description" => 'Access to read project\'s development keys',
|
||||
],
|
||||
"devKeys.write" => [
|
||||
"description" =>
|
||||
"Access to create, update, and delete project\'s development keys",
|
||||
],
|
||||
"webhooks.read" => [
|
||||
"description" =>
|
||||
"Access to read project\'s webhooks",
|
||||
],
|
||||
"webhooks.write" => [
|
||||
"description" =>
|
||||
"Access to create, update, and delete project\'s webhooks",
|
||||
],
|
||||
];
|
||||
|
|
@ -1483,7 +1483,7 @@ App::post('/v1/projects/:projectId/keys')
|
|||
))
|
||||
->param('projectId', '', new UID(), 'Project unique ID.')
|
||||
->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('scopes')), true), APP_LIMIT_ARRAY_PARAMS_SIZE)), 'Key scopes list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' scopes are allowed.')
|
||||
->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')
|
||||
|
|
@ -1502,9 +1502,6 @@ App::post('/v1/projects/:projectId/keys')
|
|||
Permission::update(Role::any()),
|
||||
Permission::delete(Role::any()),
|
||||
],
|
||||
// TODO: @hmacr Remove `projectInternalId` and `projectId` column writes before deleting the column.
|
||||
'projectInternalId' => $project->getSequence(),
|
||||
'projectId' => $project->getId(),
|
||||
'resourceInternalId' => $project->getSequence(),
|
||||
'resourceId' => $project->getId(),
|
||||
'resourceType' => 'projects',
|
||||
|
|
@ -1628,7 +1625,7 @@ App::put('/v1/projects/:projectId/keys/:keyId')
|
|||
->param('projectId', '', new UID(), 'Project unique ID.')
|
||||
->param('keyId', '', new UID(), 'Key unique ID.')
|
||||
->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('scopes')), true), APP_LIMIT_ARRAY_PARAMS_SIZE)), 'Key scopes list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' events are allowed.')
|
||||
->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 . ' events 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')
|
||||
|
|
@ -1729,7 +1726,7 @@ App::post('/v1/projects/:projectId/jwts')
|
|||
]
|
||||
))
|
||||
->param('projectId', '', new UID(), 'Project unique ID.')
|
||||
->param('scopes', [], new ArrayList(new WhiteList(array_keys(Config::getParam('scopes')), true), APP_LIMIT_ARRAY_PARAMS_SIZE), 'List of scopes allowed for JWT key. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' scopes are allowed.')
|
||||
->param('scopes', [], new ArrayList(new WhiteList(array_keys(Config::getParam('projectScopes')), true), APP_LIMIT_ARRAY_PARAMS_SIZE), 'List of scopes allowed for JWT key. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' scopes are allowed.')
|
||||
->param('duration', 900, new Range(0, 3600), 'Time in seconds before JWT expires. Default duration is 900 seconds, and maximum is 3600 seconds.', true)
|
||||
->inject('response')
|
||||
->inject('dbForPlatform')
|
||||
|
|
|
|||
|
|
@ -433,15 +433,22 @@ App::delete('/v1/teams/:teamId')
|
|||
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to remove team from DB');
|
||||
}
|
||||
|
||||
// Sync delete
|
||||
$deletes = new Deletes();
|
||||
$deletes->deleteMemberships($getProjectDB, $team, $project);
|
||||
|
||||
// Async delete
|
||||
if ($project->getId() === 'console') {
|
||||
$queueForDeletes
|
||||
->setType(DELETE_TYPE_TEAM_PROJECTS)
|
||||
->setDocument($team);
|
||||
->setDocument($team)
|
||||
->trigger();
|
||||
}
|
||||
|
||||
$queueForDeletes
|
||||
->setType(DELETE_TYPE_DOCUMENT)
|
||||
->setDocument($team);
|
||||
|
||||
$queueForEvents
|
||||
->setParam('teamId', $team->getId())
|
||||
->setPayload($response->output($team, Response::MODEL_TEAM))
|
||||
|
|
|
|||
|
|
@ -1503,7 +1503,7 @@ App::error()
|
|||
$template = $error->getView() ?? (($route) ? $route->getLabel('error', null) : null);
|
||||
|
||||
// TODO: Ideally use group 'api' here, but all wildcard routes seem to have 'api' at the moment
|
||||
if (!\str_starts_with($route->getPath(), '/v1')) {
|
||||
if (empty($route) || !\str_starts_with($route->getPath(), '/v1')) {
|
||||
$template = __DIR__ . '/../views/general/error.phtml';
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -191,7 +191,7 @@ App::post('/v1/mock/api-key-unprefixed')
|
|||
throw new Exception(Exception::PROJECT_NOT_FOUND);
|
||||
}
|
||||
|
||||
$scopes = array_keys(Config::getParam('scopes'));
|
||||
$scopes = array_keys(Config::getParam('projectScopes'));
|
||||
|
||||
$key = new Document([
|
||||
'$id' => ID::unique(),
|
||||
|
|
@ -200,9 +200,6 @@ App::post('/v1/mock/api-key-unprefixed')
|
|||
Permission::update(Role::any()),
|
||||
Permission::delete(Role::any()),
|
||||
],
|
||||
// TODO: @hmacr Remove `projectInternalId` and `projectId` column writes before deleting the column.
|
||||
'projectInternalId' => $project->getSequence(),
|
||||
'projectId' => $project->getId(),
|
||||
'resourceInternalId' => $project->getSequence(),
|
||||
'resourceId' => $project->getId(),
|
||||
'resourceType' => 'projects',
|
||||
|
|
|
|||
|
|
@ -157,10 +157,6 @@ App::init()
|
|||
|
||||
// Step 5: API Key Authentication
|
||||
if (!empty($apiKey)) {
|
||||
// Verify no user session exists simultaneously
|
||||
if (!$user->isEmpty()) {
|
||||
throw new Exception(Exception::USER_API_KEY_AND_SESSION_SET);
|
||||
}
|
||||
// Check if key is expired
|
||||
if ($apiKey->isExpired()) {
|
||||
throw new Exception(Exception::PROJECT_KEY_EXPIRED);
|
||||
|
|
@ -189,23 +185,38 @@ App::init()
|
|||
}
|
||||
|
||||
// For standard keys, update last accessed time
|
||||
if ($apiKey->getType() === API_KEY_STANDARD) {
|
||||
$dbKey = $project->find(
|
||||
key: 'secret',
|
||||
find: $request->getHeader('x-appwrite-key', ''),
|
||||
subject: 'keys'
|
||||
);
|
||||
if (\in_array($apiKey->getType(), [API_KEY_STANDARD, API_KEY_ORGANIZATION, API_KEY_ACCOUNT])) {
|
||||
$dbKey = null;
|
||||
if (!empty($apiKey->getProjectId())) {
|
||||
$dbKey = $project->find(
|
||||
key: 'secret',
|
||||
find: $request->getHeader('x-appwrite-key', ''),
|
||||
subject: 'keys'
|
||||
);
|
||||
} elseif (!empty($apiKey->getUserId())) {
|
||||
$dbKey = $user->find(
|
||||
key: 'secret',
|
||||
find: $request->getHeader('x-appwrite-key', ''),
|
||||
subject: 'keys'
|
||||
);
|
||||
} elseif (!empty($apiKey->getTeamId())) {
|
||||
$dbKey = $team->find(
|
||||
key: 'secret',
|
||||
find: $request->getHeader('x-appwrite-key', ''),
|
||||
subject: 'keys'
|
||||
);
|
||||
}
|
||||
|
||||
if (!$dbKey) {
|
||||
throw new Exception(Exception::USER_UNAUTHORIZED);
|
||||
}
|
||||
|
||||
$updates = new Document();
|
||||
|
||||
$accessedAt = $dbKey->getAttribute('accessedAt', 0);
|
||||
|
||||
if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_KEY_ACCESS)) > $accessedAt) {
|
||||
$dbKey->setAttribute('accessedAt', DateTime::now());
|
||||
$dbForPlatform->updateDocument('keys', $dbKey->getId(), $dbKey);
|
||||
$dbForPlatform->purgeCachedDocument('projects', $project->getId());
|
||||
$updates->setAttribute('accessedAt', DateTime::now());
|
||||
}
|
||||
|
||||
$sdkValidator = new WhiteList($servers, true);
|
||||
|
|
@ -216,12 +227,21 @@ App::init()
|
|||
|
||||
if (!in_array($sdk, $sdks)) {
|
||||
$sdks[] = $sdk;
|
||||
$dbKey->setAttribute('sdks', $sdks);
|
||||
|
||||
/** Update access time as well */
|
||||
$dbKey->setAttribute('accessedAt', Datetime::now());
|
||||
$dbForPlatform->updateDocument('keys', $dbKey->getId(), $dbKey);
|
||||
$dbForPlatform->purgeCachedDocument('projects', $project->getId());
|
||||
$updates->setAttribute('sdks', $sdks);
|
||||
$updates->setAttribute('accessedAt', Datetime::now());
|
||||
}
|
||||
}
|
||||
|
||||
if (!$updates->isEmpty()) {
|
||||
$dbForPlatform->getAuthorization()->skip(fn () => $dbForPlatform->updateDocument('keys', $dbKey->getId(), $updates));
|
||||
|
||||
if (!empty($apiKey->getProjectId())) {
|
||||
$dbForPlatform->getAuthorization()->skip(fn () => $dbForPlatform->purgeCachedDocument('projects', $project->getId()));
|
||||
} elseif (!empty($apiKey->getUserId())) {
|
||||
$dbForPlatform->getAuthorization()->skip(fn () => $dbForPlatform->purgeCachedDocument('users', $user->getId()));
|
||||
} elseif (!empty($apiKey->getTeamId())) {
|
||||
$dbForPlatform->getAuthorization()->skip(fn () => $dbForPlatform->purgeCachedDocument('teams', $team->getId()));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -22,7 +22,9 @@ Config::load('collections', __DIR__ . '/../config/collections.php', $configAdapt
|
|||
Config::load('frameworks', __DIR__ . '/../config/frameworks.php', $configAdapter);
|
||||
Config::load('usage', __DIR__ . '/../config/usage.php', $configAdapter);
|
||||
Config::load('roles', __DIR__ . '/../config/roles.php', $configAdapter); // User roles and scopes
|
||||
Config::load('scopes', __DIR__ . '/../config/scopes.php', $configAdapter); // User roles and scopes
|
||||
Config::load('projectScopes', __DIR__ . '/../config/scopes/project.php', $configAdapter);
|
||||
Config::load('organizationScopes', __DIR__ . '/../config/scopes/organization.php', $configAdapter);
|
||||
Config::load('accountScopes', __DIR__ . '/../config/scopes/account.php', $configAdapter);
|
||||
Config::load('services', __DIR__ . '/../config/services.php', $configAdapter); // List of services
|
||||
Config::load('variables', __DIR__ . '/../config/variables.php', $configAdapter); // List of env variables
|
||||
Config::load('regions', __DIR__ . '/../config/regions.php', $configAdapter); // List of available regions
|
||||
|
|
|
|||
|
|
@ -194,6 +194,7 @@ const DELETE_TYPE_SITES = 'sites';
|
|||
const DELETE_TYPE_FUNCTIONS = 'functions';
|
||||
const DELETE_TYPE_DEPLOYMENTS = 'deployments';
|
||||
const DELETE_TYPE_USERS = 'users';
|
||||
const DELETE_TYPE_TEAMS = 'teams';
|
||||
const DELETE_TYPE_TEAM_PROJECTS = 'teams_projects';
|
||||
const DELETE_TYPE_EXECUTIONS = 'executions';
|
||||
const DELETE_TYPE_EXECUTIONS_LIMIT = 'executionsLimit';
|
||||
|
|
@ -249,6 +250,8 @@ const MESSAGE_TYPE_PUSH = 'push';
|
|||
// API key types
|
||||
const API_KEY_STANDARD = 'standard';
|
||||
const API_KEY_DYNAMIC = 'dynamic';
|
||||
const API_KEY_ORGANIZATION = 'organization';
|
||||
const API_KEY_ACCOUNT = 'account';
|
||||
// Usage metrics
|
||||
const METRIC_TEAMS = 'teams';
|
||||
const METRIC_USERS = 'users';
|
||||
|
|
|
|||
|
|
@ -433,3 +433,34 @@ Database::addFilter(
|
|||
return $value;
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
Database::addFilter(
|
||||
'subQueryOrganizationKeys',
|
||||
function (mixed $value) {
|
||||
return;
|
||||
},
|
||||
function (mixed $value, Document $document, Database $database) {
|
||||
return $database->getAuthorization()->skip(fn () => $database
|
||||
->find('keys', [
|
||||
Query::equal('resourceType', ['teams']),
|
||||
Query::equal('resourceInternalId', [$document->getSequence()]),
|
||||
Query::limit(APP_LIMIT_SUBQUERY),
|
||||
]));
|
||||
}
|
||||
);
|
||||
|
||||
Database::addFilter(
|
||||
'subQueryAccountKeys',
|
||||
function (mixed $value) {
|
||||
return;
|
||||
},
|
||||
function (mixed $value, Document $document, Database $database) {
|
||||
return $database->getAuthorization()->skip(fn () => $database
|
||||
->find('keys', [
|
||||
Query::equal('resourceType', ['users']),
|
||||
Query::equal('resourceInternalId', [$document->getSequence()]),
|
||||
Query::limit(APP_LIMIT_SUBQUERY),
|
||||
]));
|
||||
}
|
||||
);
|
||||
|
|
|
|||
|
|
@ -177,7 +177,7 @@ Response::setModel(new BaseList('Deployments List', Response::MODEL_DEPLOYMENT_L
|
|||
Response::setModel(new BaseList('Executions List', Response::MODEL_EXECUTION_LIST, 'executions', Response::MODEL_EXECUTION));
|
||||
Response::setModel(new BaseList('Projects List', Response::MODEL_PROJECT_LIST, 'projects', Response::MODEL_PROJECT, true, false));
|
||||
Response::setModel(new BaseList('Webhooks List', Response::MODEL_WEBHOOK_LIST, 'webhooks', Response::MODEL_WEBHOOK, true, false));
|
||||
Response::setModel(new BaseList('API Keys List', Response::MODEL_KEY_LIST, 'keys', Response::MODEL_KEY, true, false));
|
||||
Response::setModel(new BaseList('API Keys List', Response::MODEL_KEY_LIST, 'keys', Response::MODEL_KEY, true, true));
|
||||
Response::setModel(new BaseList('Dev Keys List', Response::MODEL_DEV_KEY_LIST, 'devKeys', Response::MODEL_DEV_KEY, true, false));
|
||||
Response::setModel(new BaseList('Auth Providers List', Response::MODEL_AUTH_PROVIDER_LIST, 'platforms', Response::MODEL_AUTH_PROVIDER, true, false));
|
||||
Response::setModel(new BaseList('Platforms List', Response::MODEL_PLATFORM_LIST, 'platforms', Response::MODEL_PLATFORM, true, false));
|
||||
|
|
|
|||
|
|
@ -343,6 +343,7 @@ App::setResource('user', function (string $mode, Document $project, Document $co
|
|||
* 5. Regardless of the results from steps 1-4, attempts to fetch the JWT token.
|
||||
* 6. If the JWT user has a valid session ID, updates the user variable with the user from `projectDB`,
|
||||
* overwriting the previous value.
|
||||
* 7. If account API key is passed, use user of the account API key as long as user ID header matches too
|
||||
*/
|
||||
|
||||
$authorization->setDefaultStatus(true);
|
||||
|
|
@ -419,12 +420,17 @@ App::setResource('user', function (string $mode, Document $project, Document $co
|
|||
// }
|
||||
$authJWT = $request->getHeader('x-appwrite-jwt', '');
|
||||
if (!empty($authJWT) && !$project->isEmpty()) { // JWT authentication
|
||||
if (!$user->isEmpty()) {
|
||||
throw new Exception(Exception::USER_JWT_AND_COOKIE_SET);
|
||||
}
|
||||
|
||||
$jwt = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 3600, 0);
|
||||
try {
|
||||
$payload = $jwt->decode($authJWT);
|
||||
} catch (JWTException $error) {
|
||||
throw new Exception(Exception::USER_JWT_INVALID, 'Failed to verify JWT. ' . $error->getMessage());
|
||||
}
|
||||
|
||||
$jwtUserId = $payload['userId'] ?? '';
|
||||
if (!empty($jwtUserId)) {
|
||||
if ($mode === APP_MODE_ADMIN) {
|
||||
|
|
@ -440,6 +446,34 @@ App::setResource('user', function (string $mode, Document $project, Document $co
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Account based on account API key
|
||||
$accountKey = $request->getHeader('x-appwrite-key', '');
|
||||
$accountKeyUserId = $request->getHeader('x-appwrite-user', '');
|
||||
if (!empty($accountKeyUserId) && !empty($accountKey)) {
|
||||
if (!$user->isEmpty()) {
|
||||
throw new Exception(Exception::USER_API_KEY_AND_SESSION_SET);
|
||||
}
|
||||
|
||||
$accountKeyUser = $dbForPlatform->getAuthorization()->skip(fn () => $dbForPlatform->getDocument('users', $accountKeyUserId));
|
||||
if (!$accountKeyUser->isEmpty()) {
|
||||
$key = $accountKeyUser->find(
|
||||
key: 'secret',
|
||||
find: $accountKey,
|
||||
subject: 'keys'
|
||||
);
|
||||
|
||||
if (!empty($key)) {
|
||||
$expire = $key->getAttribute('expire');
|
||||
if (!empty($expire) && $expire < DatabaseDateTime::formatTz(DatabaseDateTime::now())) {
|
||||
throw new Exception(Exception::ACCOUNT_KEY_EXPIRED);
|
||||
}
|
||||
|
||||
$user = $accountKeyUser;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$dbForProject->setMetadata('user', $user->getId());
|
||||
$dbForPlatform->setMetadata('user', $user->getId());
|
||||
|
||||
|
|
@ -1227,7 +1261,7 @@ App::setResource('team', function (Document $project, Database $dbForPlatform, A
|
|||
$teamInternalId = $project->getAttribute('teamInternalId', '');
|
||||
} else {
|
||||
$route = $utopia->match($request);
|
||||
$path = $route->getPath();
|
||||
$path = !empty($route) ? $route->getPath() : $request->getURI();
|
||||
if (str_starts_with($path, '/v1/projects/:projectId')) {
|
||||
$uri = $request->getURI();
|
||||
$pid = explode('/', $uri)[3];
|
||||
|
|
@ -1282,15 +1316,39 @@ App::setResource('previewHostname', function (Request $request, ?Key $apiKey) {
|
|||
return '';
|
||||
}, ['request', 'apiKey']);
|
||||
|
||||
App::setResource('apiKey', function (Request $request, Document $project): ?Key {
|
||||
App::setResource('apiKey', function (Request $request, Document $project, Document $team, Document $user): ?Key {
|
||||
$key = $request->getHeader('x-appwrite-key');
|
||||
|
||||
if (empty($key)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Key::decode($project, $key);
|
||||
}, ['request', 'project']);
|
||||
$key = Key::decode($project, $team, $user, $key);
|
||||
|
||||
$userHeader = $request->getHeader('x-appwrite-user');
|
||||
$organizationHeader = $request->getHeader('x-appwrite-organization');
|
||||
$projectHeader = $request->getHeader('x-appwrite-project');
|
||||
|
||||
if (!empty($key->getProjectId())) {
|
||||
if (empty($projectHeader) || $projectHeader !== $key->getProjectId()) {
|
||||
throw new Exception(Exception::PROJECT_ID_MISSING);
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($key->getUserId())) {
|
||||
if (empty($userHeader) || $userHeader !== $key->getUserId()) {
|
||||
throw new Exception(Exception::USER_ID_MISSING);
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($key->getTeamId())) {
|
||||
if (empty($organizationHeader) || $organizationHeader !== $key->getTeamId()) {
|
||||
throw new Exception(Exception::ORGANIZATION_ID_MISSING);
|
||||
}
|
||||
}
|
||||
|
||||
return $key;
|
||||
}, ['request', 'project', 'team', 'user']);
|
||||
|
||||
App::setResource('executor', fn () => new Executor());
|
||||
|
||||
|
|
|
|||
68
composer.lock
generated
68
composer.lock
generated
|
|
@ -2066,16 +2066,16 @@
|
|||
},
|
||||
{
|
||||
"name": "phpseclib/phpseclib",
|
||||
"version": "3.0.48",
|
||||
"version": "3.0.49",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/phpseclib/phpseclib.git",
|
||||
"reference": "64065a5679c50acb886e82c07aa139b0f757bb89"
|
||||
"reference": "6233a1e12584754e6b5daa69fe1289b47775c1b9"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/64065a5679c50acb886e82c07aa139b0f757bb89",
|
||||
"reference": "64065a5679c50acb886e82c07aa139b0f757bb89",
|
||||
"url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/6233a1e12584754e6b5daa69fe1289b47775c1b9",
|
||||
"reference": "6233a1e12584754e6b5daa69fe1289b47775c1b9",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
|
@ -2156,7 +2156,7 @@
|
|||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/phpseclib/phpseclib/issues",
|
||||
"source": "https://github.com/phpseclib/phpseclib/tree/3.0.48"
|
||||
"source": "https://github.com/phpseclib/phpseclib/tree/3.0.49"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
|
|
@ -2172,7 +2172,7 @@
|
|||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2025-12-15T11:51:42+00:00"
|
||||
"time": "2026-01-27T09:17:28+00:00"
|
||||
},
|
||||
{
|
||||
"name": "psr/container",
|
||||
|
|
@ -2735,16 +2735,16 @@
|
|||
},
|
||||
{
|
||||
"name": "symfony/http-client",
|
||||
"version": "v7.4.4",
|
||||
"version": "v7.4.5",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/http-client.git",
|
||||
"reference": "d63c23357d74715a589454c141c843f0172bec6c"
|
||||
"reference": "84bb634857a893cc146cceb467e31b3f02c5fe9f"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/http-client/zipball/d63c23357d74715a589454c141c843f0172bec6c",
|
||||
"reference": "d63c23357d74715a589454c141c843f0172bec6c",
|
||||
"url": "https://api.github.com/repos/symfony/http-client/zipball/84bb634857a893cc146cceb467e31b3f02c5fe9f",
|
||||
"reference": "84bb634857a893cc146cceb467e31b3f02c5fe9f",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
|
@ -2812,7 +2812,7 @@
|
|||
"http"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/http-client/tree/v7.4.4"
|
||||
"source": "https://github.com/symfony/http-client/tree/v7.4.5"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
|
|
@ -2832,7 +2832,7 @@
|
|||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-01-23T16:34:22+00:00"
|
||||
"time": "2026-01-27T16:16:02+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/http-client-contracts",
|
||||
|
|
@ -5120,28 +5120,28 @@
|
|||
},
|
||||
{
|
||||
"name": "utopia-php/swoole",
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/swoole.git",
|
||||
"reference": "95a937acb393dbf95cccba239d55886e2848ab0b"
|
||||
"reference": "c5ce710dfffc4df09bf3e7aea2d1e55c53e77a95"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/swoole/zipball/95a937acb393dbf95cccba239d55886e2848ab0b",
|
||||
"reference": "95a937acb393dbf95cccba239d55886e2848ab0b",
|
||||
"url": "https://api.github.com/repos/utopia-php/swoole/zipball/c5ce710dfffc4df09bf3e7aea2d1e55c53e77a95",
|
||||
"reference": "c5ce710dfffc4df09bf3e7aea2d1e55c53e77a95",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-swoole": "*",
|
||||
"php": ">=8.0",
|
||||
"ext-swoole": "6.*",
|
||||
"php": ">=8.1",
|
||||
"utopia-php/framework": "0.33.37"
|
||||
},
|
||||
"require-dev": {
|
||||
"laravel/pint": "1.2.*",
|
||||
"phpstan/phpstan": "^1.10",
|
||||
"phpunit/phpunit": "^9.3",
|
||||
"swoole/ide-helper": "5.0.2"
|
||||
"swoole/ide-helper": "6.0.2"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
|
|
@ -5165,9 +5165,9 @@
|
|||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/utopia-php/swoole/issues",
|
||||
"source": "https://github.com/utopia-php/swoole/tree/1.0.0"
|
||||
"source": "https://github.com/utopia-php/swoole/tree/1.0.1"
|
||||
},
|
||||
"time": "2026-01-14T14:00:11+00:00"
|
||||
"time": "2026-01-28T12:43:38+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/system",
|
||||
|
|
@ -8199,16 +8199,16 @@
|
|||
},
|
||||
{
|
||||
"name": "symfony/finder",
|
||||
"version": "v8.0.4",
|
||||
"version": "v8.0.5",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/finder.git",
|
||||
"reference": "42e48eb02e07d5f3771d194d67da117eb824c8c1"
|
||||
"reference": "8bd576e97c67d45941365bf824e18dc8538e6eb0"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/finder/zipball/42e48eb02e07d5f3771d194d67da117eb824c8c1",
|
||||
"reference": "42e48eb02e07d5f3771d194d67da117eb824c8c1",
|
||||
"url": "https://api.github.com/repos/symfony/finder/zipball/8bd576e97c67d45941365bf824e18dc8538e6eb0",
|
||||
"reference": "8bd576e97c67d45941365bf824e18dc8538e6eb0",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
|
@ -8243,7 +8243,7 @@
|
|||
"description": "Finds files and directories via an intuitive fluent interface",
|
||||
"homepage": "https://symfony.com",
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/finder/tree/v8.0.4"
|
||||
"source": "https://github.com/symfony/finder/tree/v8.0.5"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
|
|
@ -8263,7 +8263,7 @@
|
|||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-01-12T12:37:40+00:00"
|
||||
"time": "2026-01-26T15:08:38+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/options-resolver",
|
||||
|
|
@ -8668,16 +8668,16 @@
|
|||
},
|
||||
{
|
||||
"name": "symfony/process",
|
||||
"version": "v8.0.4",
|
||||
"version": "v8.0.5",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/process.git",
|
||||
"reference": "10df72602d88c0a3fa685b822976a052611dd607"
|
||||
"reference": "b5f3aa6762e33fd95efbaa2ec4f4bc9fdd16d674"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/process/zipball/10df72602d88c0a3fa685b822976a052611dd607",
|
||||
"reference": "10df72602d88c0a3fa685b822976a052611dd607",
|
||||
"url": "https://api.github.com/repos/symfony/process/zipball/b5f3aa6762e33fd95efbaa2ec4f4bc9fdd16d674",
|
||||
"reference": "b5f3aa6762e33fd95efbaa2ec4f4bc9fdd16d674",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
|
@ -8709,7 +8709,7 @@
|
|||
"description": "Executes commands in sub-processes",
|
||||
"homepage": "https://symfony.com",
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/process/tree/v8.0.4"
|
||||
"source": "https://github.com/symfony/process/tree/v8.0.5"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
|
|
@ -8729,7 +8729,7 @@
|
|||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-01-23T11:07:10+00:00"
|
||||
"time": "2026-01-26T15:08:38+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/string",
|
||||
|
|
@ -9075,5 +9075,5 @@
|
|||
"platform-overrides": {
|
||||
"php": "8.3"
|
||||
},
|
||||
"plugin-api-version": "2.6.0"
|
||||
"plugin-api-version": "2.9.0"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -98,6 +98,7 @@ services:
|
|||
- ./public:/usr/src/code/public
|
||||
- ./src:/usr/src/code/src
|
||||
- ./dev:/usr/src/code/dev
|
||||
# - ./vendor/utopia-php/framework:/usr/src/code/vendor/utopia-php/framework
|
||||
depends_on:
|
||||
- mariadb
|
||||
- redis
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@ class Key
|
|||
{
|
||||
public function __construct(
|
||||
protected string $projectId,
|
||||
protected string $teamId,
|
||||
protected string $userId,
|
||||
protected string $type,
|
||||
protected string $role,
|
||||
protected array $scopes,
|
||||
|
|
@ -34,6 +36,16 @@ class Key
|
|||
return $this->projectId;
|
||||
}
|
||||
|
||||
public function getUserId(): string
|
||||
{
|
||||
return $this->userId;
|
||||
}
|
||||
|
||||
public function getTeamId(): string
|
||||
{
|
||||
return $this->teamId;
|
||||
}
|
||||
|
||||
public function getType(): string
|
||||
{
|
||||
return $this->type;
|
||||
|
|
@ -95,13 +107,12 @@ class Key
|
|||
* Decode the given secret key into a Key object, containing the project ID, type, role, scopes, and name.
|
||||
* Can be a stored API key or a dynamic key (JWT).
|
||||
*
|
||||
* @param Document $project
|
||||
* @param string $key
|
||||
* @return Key
|
||||
* @throws Exception
|
||||
*/
|
||||
public static function decode(
|
||||
Document $project,
|
||||
Document $team,
|
||||
Document $user,
|
||||
string $key
|
||||
): Key {
|
||||
if (\str_contains($key, '_')) {
|
||||
|
|
@ -118,6 +129,8 @@ class Key
|
|||
|
||||
$guestKey = new Key(
|
||||
$project->getId(),
|
||||
'',
|
||||
'',
|
||||
$type,
|
||||
User::ROLE_GUESTS,
|
||||
$roles[User::ROLE_GUESTS]['scopes'] ?? [],
|
||||
|
|
@ -133,6 +146,7 @@ class Key
|
|||
leeway: 0
|
||||
);
|
||||
|
||||
$payload = [];
|
||||
try {
|
||||
$payload = $jwtObj->decode($secret);
|
||||
} catch (JWTException) {
|
||||
|
|
@ -155,6 +169,8 @@ class Key
|
|||
|
||||
return new Key(
|
||||
$projectId,
|
||||
'',
|
||||
'',
|
||||
$type,
|
||||
$role,
|
||||
$scopes,
|
||||
|
|
@ -188,12 +204,86 @@ class Key
|
|||
|
||||
return new Key(
|
||||
$project->getId(),
|
||||
'',
|
||||
'',
|
||||
$type,
|
||||
$role,
|
||||
$scopes,
|
||||
$name,
|
||||
$expired
|
||||
);
|
||||
case API_KEY_ACCOUNT:
|
||||
$key = $user->find(
|
||||
key: 'secret',
|
||||
find: $key,
|
||||
subject: 'keys'
|
||||
);
|
||||
|
||||
// Invalid key
|
||||
if (!$key) {
|
||||
return $guestKey;
|
||||
}
|
||||
|
||||
$expire = $key->getAttribute('expire');
|
||||
$expired = false;
|
||||
if (!empty($expire) && $expire < DateTime::formatTz(DateTime::now())) {
|
||||
$expired = true;
|
||||
}
|
||||
|
||||
$name = $key->getAttribute('name', 'UNKNOWN');
|
||||
|
||||
$role = User::ROLE_USERS;
|
||||
|
||||
$scopes = $key->getAttribute('scopes', []);
|
||||
|
||||
$key = new Key(
|
||||
'',
|
||||
'',
|
||||
$user->getId(),
|
||||
$type,
|
||||
$role,
|
||||
$scopes,
|
||||
$name,
|
||||
$expired
|
||||
);
|
||||
|
||||
return $key;
|
||||
case API_KEY_ORGANIZATION:
|
||||
$key = $team->find(
|
||||
key: 'secret',
|
||||
find: $key,
|
||||
subject: 'keys'
|
||||
);
|
||||
|
||||
// Invalid key
|
||||
if (!$key) {
|
||||
return $guestKey;
|
||||
}
|
||||
|
||||
$expire = $key->getAttribute('expire');
|
||||
$expired = false;
|
||||
if (!empty($expire) && $expire < DateTime::formatTz(DateTime::now())) {
|
||||
$expired = true;
|
||||
}
|
||||
|
||||
$name = $key->getAttribute('name', 'UNKNOWN');
|
||||
|
||||
$role = User::ROLE_APPS;
|
||||
|
||||
$scopes = $key->getAttribute('scopes', []);
|
||||
|
||||
$key = new Key(
|
||||
'',
|
||||
$team->getId(),
|
||||
'',
|
||||
$type,
|
||||
$role,
|
||||
$scopes,
|
||||
$name,
|
||||
$expired
|
||||
);
|
||||
|
||||
return $key;
|
||||
default:
|
||||
return $guestKey;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -107,7 +107,9 @@ class Exception extends \Exception
|
|||
public const string USER_DELETION_PROHIBITED = 'user_deletion_prohibited';
|
||||
public const string USER_TARGET_NOT_FOUND = 'user_target_not_found';
|
||||
public const string USER_TARGET_ALREADY_EXISTS = 'user_target_already_exists';
|
||||
public const string USER_API_KEY_AND_SESSION_SET = 'user_key_and_session_set';
|
||||
public const string USER_API_KEY_AND_SESSION_SET = 'user_api_key_and_session_set';
|
||||
public const string USER_JWT_AND_COOKIE_SET = 'user_jwt_and_cookie_set';
|
||||
public const string USER_ID_MISSING = 'user_id_missing';
|
||||
|
||||
public const string API_KEY_EXPIRED = 'api_key_expired';
|
||||
|
||||
|
|
@ -119,6 +121,8 @@ class Exception extends \Exception
|
|||
public const string TEAM_INVITE_MISMATCH = 'team_invite_mismatch';
|
||||
public const string TEAM_ALREADY_EXISTS = 'team_already_exists';
|
||||
|
||||
public const string ORGANIZATION_ID_MISSING = 'organization_id_missing';
|
||||
|
||||
/** Console */
|
||||
public const string RESOURCE_ALREADY_EXISTS = 'resource_already_exists';
|
||||
|
||||
|
|
@ -283,6 +287,7 @@ class Exception extends \Exception
|
|||
|
||||
/** Projects */
|
||||
public const string PROJECT_NOT_FOUND = 'project_not_found';
|
||||
public const string PROJECT_ID_MISSING = 'project_id_missing';
|
||||
public const string PROJECT_PROVIDER_DISABLED = 'project_provider_disabled';
|
||||
public const string PROJECT_PROVIDER_UNSUPPORTED = 'project_provider_unsupported';
|
||||
public const string PROJECT_ALREADY_EXISTS = 'project_already_exists';
|
||||
|
|
@ -290,6 +295,7 @@ class Exception extends \Exception
|
|||
public const string PROJECT_INVALID_FAILURE_URL = 'project_invalid_failure_url';
|
||||
public const string PROJECT_RESERVED_PROJECT = 'project_reserved_project';
|
||||
public const string PROJECT_KEY_EXPIRED = 'project_key_expired';
|
||||
public const string ACCOUNT_KEY_EXPIRED = 'account_key_expired';
|
||||
|
||||
public const string PROJECT_SMTP_CONFIG_INVALID = 'project_smtp_config_invalid';
|
||||
|
||||
|
|
|
|||
|
|
@ -87,7 +87,7 @@ class Create extends Base
|
|||
->param('logging', true, new Boolean(), 'When disabled, executions will exclude logs and errors, and will be slightly faster.', true)
|
||||
->param('entrypoint', '', new Text(1028, 0), 'Entrypoint File. This path is relative to the "providerRootDirectory".', true)
|
||||
->param('commands', '', new Text(8192, 0), 'Build Commands.', true)
|
||||
->param('scopes', [], new ArrayList(new WhiteList(array_keys(Config::getParam('scopes')), true), APP_LIMIT_ARRAY_PARAMS_SIZE), 'List of scopes allowed for API key auto-generated for every execution. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' scopes are allowed.', true)
|
||||
->param('scopes', [], new ArrayList(new WhiteList(array_keys(Config::getParam('projectScopes')), true), APP_LIMIT_ARRAY_PARAMS_SIZE), 'List of scopes allowed for API key auto-generated for every execution. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' scopes are allowed.', true)
|
||||
->param('installationId', '', new Text(128, 0), 'Appwrite Installation ID for VCS (Version Control System) deployment.', true)
|
||||
->param('providerRepositoryId', '', new Text(128, 0), 'Repository ID of the repo linked to the function.', true)
|
||||
->param('providerBranch', '', new Text(128, 0), 'Production branch for the repo linked to the function.', true)
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ class Update extends Base
|
|||
->param('logging', true, new Boolean(), 'When disabled, executions will exclude logs and errors, and will be slightly faster.', true)
|
||||
->param('entrypoint', '', new Text(1028, 0), 'Entrypoint File. This path is relative to the "providerRootDirectory".', true)
|
||||
->param('commands', '', new Text(8192, 0), 'Build Commands.', true)
|
||||
->param('scopes', [], new ArrayList(new WhiteList(array_keys(Config::getParam('scopes')), true), APP_LIMIT_ARRAY_PARAMS_SIZE), 'List of scopes allowed for API Key auto-generated for every execution. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' scopes are allowed.', true)
|
||||
->param('scopes', [], new ArrayList(new WhiteList(array_keys(Config::getParam('projectScopes')), true), APP_LIMIT_ARRAY_PARAMS_SIZE), 'List of scopes allowed for API Key auto-generated for every execution. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' scopes are allowed.', true)
|
||||
->param('installationId', '', new Text(128, 0), 'Appwrite Installation ID for VCS (Version Controle System) deployment.', true)
|
||||
->param('providerRepositoryId', null, new Nullable(new Text(128, 0)), 'Repository ID of the repo linked to the function', true)
|
||||
->param('providerBranch', '', new Text(128, 0), 'Production branch for the repo linked to the function', true)
|
||||
|
|
|
|||
|
|
@ -190,7 +190,7 @@ class Screenshot extends Action
|
|||
'cookie' => $cookieConsole
|
||||
], [
|
||||
'name' => 'Screenshot API key',
|
||||
'scopes' => \array_keys(Config::getParam('scopes', []))
|
||||
'scopes' => \array_keys(Config::getParam('projectScopes', []))
|
||||
]);
|
||||
|
||||
if ($response['headers']['status-code'] !== 201) {
|
||||
|
|
|
|||
|
|
@ -125,6 +125,9 @@ class Deletes extends Action
|
|||
case DELETE_TYPE_USERS:
|
||||
$this->deleteUser($getProjectDB, $document, $project);
|
||||
break;
|
||||
case DELETE_TYPE_TEAMS:
|
||||
$this->deleteTeam($getProjectDB, $document, $project);
|
||||
break;
|
||||
case DELETE_TYPE_BUCKETS:
|
||||
$this->deleteBucket($getProjectDB, $deviceForFiles, $document, $project);
|
||||
break;
|
||||
|
|
@ -530,7 +533,7 @@ class Deletes extends Action
|
|||
}
|
||||
|
||||
/**
|
||||
* @var $dbForProject Database
|
||||
* @var Database $dbForProject
|
||||
*/
|
||||
$dbForProject = $getProjectDB($document);
|
||||
|
||||
|
|
@ -661,6 +664,24 @@ class Deletes extends Action
|
|||
}
|
||||
}
|
||||
|
||||
private function deleteTeam(callable $getProjectDB, Document $document, Document $project): void
|
||||
{
|
||||
$teamId = $document->getId();
|
||||
$teamInternalId = $document->getSequence();
|
||||
$dbForProject = $getProjectDB($project);
|
||||
|
||||
if ($project->getId() === 'console') {
|
||||
// Delete Keys
|
||||
$this->deleteByGroup('keys', [
|
||||
Query::equal('resourceInternalId', [$teamInternalId]),
|
||||
Query::equal('resourceType', ['teams']),
|
||||
Query::orderAsc()
|
||||
], $dbForProject);
|
||||
}
|
||||
|
||||
$dbForProject->purgeCachedDocument('teams', $teamId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param callable $getProjectDB
|
||||
* @param Document $document user document
|
||||
|
|
@ -680,6 +701,15 @@ class Deletes extends Action
|
|||
Query::orderAsc()
|
||||
], $dbForProject);
|
||||
|
||||
if ($project->getId() === 'console') {
|
||||
// Delete Keys
|
||||
$this->deleteByGroup('keys', [
|
||||
Query::equal('resourceInternalId', [$userInternalId]),
|
||||
Query::equal('resourceType', ['users']),
|
||||
Query::orderAsc()
|
||||
], $dbForProject);
|
||||
}
|
||||
|
||||
$dbForProject->purgeCachedDocument('users', $userId);
|
||||
|
||||
// Delete Memberships and decrement team membership counts
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ class Key extends Model
|
|||
/**
|
||||
* @var bool
|
||||
*/
|
||||
protected bool $public = false;
|
||||
protected bool $public = true; // Public because reused for more key types
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1765,12 +1765,13 @@ class ProjectsConsoleClientTest extends Scope
|
|||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @depends testUpdateProjectAuthLimit
|
||||
*/
|
||||
public function testUpdateProjectAuthSessionsLimit($data): array
|
||||
public function testUpdateProjectAuthSessionsLimit(): void
|
||||
{
|
||||
$id = $data['projectId'] ?? '';
|
||||
$id = $this->setupProject([
|
||||
'projectId' => ID::unique(),
|
||||
'name' => 'testUpdateProjectAuthSessionsLimit',
|
||||
'region' => System::getEnv('_APP_REGION', 'default')
|
||||
]);
|
||||
|
||||
/**
|
||||
* Test for failure
|
||||
|
|
@ -1881,10 +1882,9 @@ class ProjectsConsoleClientTest extends Scope
|
|||
'limit' => 10,
|
||||
]);
|
||||
|
||||
return $data;
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @depends testUpdateProjectAuthLimit
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ class KeyTest extends TestCase
|
|||
{
|
||||
public function testDecode(): void
|
||||
{
|
||||
// Decode dynamic key
|
||||
$projectId = 'test';
|
||||
$usage = false;
|
||||
$scopes = [
|
||||
|
|
@ -22,34 +23,345 @@ class KeyTest extends TestCase
|
|||
'documents.read',
|
||||
];
|
||||
$roleScopes = Config::getParam('roles', [])[User::ROLE_APPS]['scopes'];
|
||||
$guestRoleScopes = Config::getParam('roles', [])[User::ROLE_GUESTS]['scopes'];
|
||||
|
||||
$key = static::generateKey($projectId, $usage, $scopes);
|
||||
$project = new Document(['$id' => $projectId,]);
|
||||
$decoded = Key::decode($project, $key);
|
||||
$decoded = Key::decode(
|
||||
project: new Document(['$id' => $projectId]),
|
||||
team: new Document(),
|
||||
user: new Document(),
|
||||
key: $key,
|
||||
);
|
||||
|
||||
$this->assertEquals($projectId, $decoded->getProjectId());
|
||||
$this->assertEquals('', $decoded->getTeamId());
|
||||
$this->assertEquals('', $decoded->getUserId());
|
||||
$this->assertEquals(API_KEY_DYNAMIC, $decoded->getType());
|
||||
$this->assertEquals(User::ROLE_APPS, $decoded->getRole());
|
||||
$this->assertEquals(\array_merge($scopes, $roleScopes), $decoded->getScopes());
|
||||
$this->assertEquals('Dynamic Key', $decoded->getName());
|
||||
|
||||
// Decode dynamic key with extras
|
||||
$extra = [
|
||||
'disabledMetrics' => ['metric123'],
|
||||
'hostnameOverride' => true,
|
||||
'bannerDisabled' => true,
|
||||
'projectCheckDisabled' => true,
|
||||
'previewAuthDisabled' => true,
|
||||
'deploymentStatusIgnored' => true,
|
||||
];
|
||||
$key = static::generateKey($projectId, $usage, $scopes, extra: $extra);
|
||||
$decoded = Key::decode(
|
||||
project: new Document(['$id' => $projectId]),
|
||||
team: new Document(),
|
||||
user: new Document(),
|
||||
key: $key,
|
||||
);
|
||||
$this->assertEquals($projectId, $decoded->getProjectId());
|
||||
$this->assertEquals('', $decoded->getTeamId());
|
||||
$this->assertEquals('', $decoded->getUserId());
|
||||
$this->assertEquals(API_KEY_DYNAMIC, $decoded->getType());
|
||||
$this->assertEquals(User::ROLE_APPS, $decoded->getRole());
|
||||
$this->assertEquals(\array_merge($scopes, $roleScopes), $decoded->getScopes());
|
||||
$this->assertEquals('Dynamic Key', $decoded->getName());
|
||||
$this->assertEquals(['metric123'], $decoded->getDisabledMetrics());
|
||||
$this->assertEquals(true, $decoded->getHostnameOverride());
|
||||
$this->assertEquals(true, $decoded->isBannerDisabled());
|
||||
$this->assertEquals(true, $decoded->isProjectCheckDisabled());
|
||||
$this->assertEquals(true, $decoded->isPreviewAuthDisabled());
|
||||
$this->assertEquals(true, $decoded->isDeploymentStatusIgnored());
|
||||
|
||||
// Decode invalid dynamic key
|
||||
$invalidKey = API_KEY_DYNAMIC . '_invalid_jwt_token';
|
||||
$decoded = Key::decode(
|
||||
project: new Document(['$id' => $projectId]),
|
||||
team: new Document(),
|
||||
user: new Document(),
|
||||
key: $invalidKey,
|
||||
);
|
||||
$this->assertEquals($projectId, $decoded->getProjectId());
|
||||
$this->assertEquals('', $decoded->getTeamId());
|
||||
$this->assertEquals('', $decoded->getUserId());
|
||||
$this->assertEquals(API_KEY_DYNAMIC, $decoded->getType());
|
||||
$this->assertEquals(User::ROLE_GUESTS, $decoded->getRole());
|
||||
$this->assertEquals($guestRoleScopes, $decoded->getScopes());
|
||||
$this->assertEquals('UNKNOWN', $decoded->getName());
|
||||
|
||||
// Decode expired dynamic key
|
||||
$expiredKey = static::generateKey($projectId, $usage, $scopes, maxAge: 1, timestamp: time() - 60);
|
||||
\sleep(2);
|
||||
$decoded = Key::decode(
|
||||
project: new Document(['$id' => $projectId]),
|
||||
team: new Document(),
|
||||
user: new Document(),
|
||||
key: $expiredKey,
|
||||
);
|
||||
$this->assertEquals($projectId, $decoded->getProjectId());
|
||||
$this->assertEquals('', $decoded->getTeamId());
|
||||
$this->assertEquals('', $decoded->getUserId());
|
||||
$this->assertEquals(API_KEY_DYNAMIC, $decoded->getType());
|
||||
$this->assertEquals(User::ROLE_GUESTS, $decoded->getRole());
|
||||
$this->assertEquals($guestRoleScopes, $decoded->getScopes());
|
||||
$this->assertEquals('UNKNOWN', $decoded->getName());
|
||||
|
||||
// Decode standard key
|
||||
$scopes = ['custom.write'];
|
||||
$decoded = Key::decode(
|
||||
project: new Document(['$id' => $projectId, 'keys' => [
|
||||
new Document([
|
||||
'secret' => 'standard_abcd1234',
|
||||
'expire' => null,
|
||||
'name' => 'Standard key',
|
||||
'scopes' => $scopes
|
||||
])
|
||||
]]),
|
||||
team: new Document(),
|
||||
user: new Document(),
|
||||
key: 'standard_abcd1234',
|
||||
);
|
||||
$this->assertEquals($projectId, $decoded->getProjectId());
|
||||
$this->assertEquals('', $decoded->getTeamId());
|
||||
$this->assertEquals('', $decoded->getUserId());
|
||||
$this->assertEquals(API_KEY_STANDARD, $decoded->getType());
|
||||
$this->assertEquals(User::ROLE_APPS, $decoded->getRole());
|
||||
$this->assertEquals(\array_merge($scopes, $roleScopes), $decoded->getScopes());
|
||||
$this->assertEquals('Standard key', $decoded->getName());
|
||||
|
||||
// Decode deprecated standard key
|
||||
$scopes = ['custom.write'];
|
||||
$decoded = Key::decode(
|
||||
project: new Document(['$id' => $projectId, 'keys' => [
|
||||
new Document([
|
||||
'secret' => 'abcd1234',
|
||||
'expire' => null,
|
||||
'name' => 'Standard key',
|
||||
'scopes' => ['custom.write']
|
||||
])
|
||||
]]),
|
||||
team: new Document(),
|
||||
user: new Document(),
|
||||
key: 'abcd1234',
|
||||
);
|
||||
$this->assertEquals($projectId, $decoded->getProjectId());
|
||||
$this->assertEquals('', $decoded->getTeamId());
|
||||
$this->assertEquals('', $decoded->getUserId());
|
||||
$this->assertEquals(API_KEY_STANDARD, $decoded->getType());
|
||||
$this->assertEquals(User::ROLE_APPS, $decoded->getRole());
|
||||
$this->assertEquals(\array_merge($scopes, $roleScopes), $decoded->getScopes());
|
||||
$this->assertEquals('Standard key', $decoded->getName());
|
||||
|
||||
// Decode invalid standard key
|
||||
$scopes = ['custom.write'];
|
||||
$decoded = Key::decode(
|
||||
project: new Document(['$id' => $projectId, 'keys' => [
|
||||
new Document([
|
||||
'secret' => 'standard_abcd1234',
|
||||
'expire' => null,
|
||||
'name' => 'Standard key',
|
||||
'scopes' => ['custom.write']
|
||||
])
|
||||
]]),
|
||||
team: new Document(),
|
||||
user: new Document(),
|
||||
key: 'standard_efgh5678',
|
||||
);
|
||||
$this->assertEquals($projectId, $decoded->getProjectId());
|
||||
$this->assertEquals('', $decoded->getTeamId());
|
||||
$this->assertEquals('', $decoded->getUserId());
|
||||
$this->assertEquals(API_KEY_STANDARD, $decoded->getType());
|
||||
$this->assertEquals(User::ROLE_GUESTS, $decoded->getRole());
|
||||
$this->assertEquals($guestRoleScopes, $decoded->getScopes());
|
||||
$this->assertEquals('UNKNOWN', $decoded->getName());
|
||||
|
||||
// Decode expired standard key
|
||||
$scopes = ['custom.write'];
|
||||
$yesterday = (new \DateTimeImmutable('-1 day'))->format('Y-m-d\TH:i:s\Z');
|
||||
$decoded = Key::decode(
|
||||
project: new Document(['$id' => $projectId, 'keys' => [
|
||||
new Document([
|
||||
'secret' => 'standard_abcd1234',
|
||||
'expire' => $yesterday,
|
||||
'name' => 'Standard key',
|
||||
'scopes' => $scopes
|
||||
])
|
||||
]]),
|
||||
team: new Document(),
|
||||
user: new Document(),
|
||||
key: 'standard_abcd1234',
|
||||
);
|
||||
$this->assertEquals(true, $decoded->isExpired());
|
||||
$this->assertEquals($projectId, $decoded->getProjectId());
|
||||
$this->assertEquals('', $decoded->getTeamId());
|
||||
$this->assertEquals('', $decoded->getUserId());
|
||||
$this->assertEquals(API_KEY_STANDARD, $decoded->getType());
|
||||
$this->assertEquals(User::ROLE_APPS, $decoded->getRole());
|
||||
$this->assertEquals(\array_merge($scopes, $roleScopes), $decoded->getScopes());
|
||||
$this->assertEquals('Standard key', $decoded->getName());
|
||||
|
||||
// Decode account key
|
||||
$userId = 'user123';
|
||||
$scopes = ['teams.write'];
|
||||
$decoded = Key::decode(
|
||||
project: new Document(['$id' => $projectId]),
|
||||
team: new Document(),
|
||||
user: new Document(['$id' => $userId, 'keys' => [
|
||||
new Document([
|
||||
'secret' => 'account_abcd1234',
|
||||
'expire' => null,
|
||||
'name' => 'Account key',
|
||||
'scopes' => $scopes
|
||||
])
|
||||
]]),
|
||||
key: 'account_abcd1234',
|
||||
);
|
||||
$this->assertEquals('', $decoded->getProjectId());
|
||||
$this->assertEquals('', $decoded->getTeamId());
|
||||
$this->assertEquals($userId, $decoded->getUserId());
|
||||
$this->assertEquals(API_KEY_ACCOUNT, $decoded->getType());
|
||||
$this->assertEquals(User::ROLE_USERS, $decoded->getRole());
|
||||
$this->assertEquals($scopes, $decoded->getScopes());
|
||||
$this->assertEquals('Account key', $decoded->getName());
|
||||
|
||||
// Decode invalid account key
|
||||
$scopes = ['teams.write'];
|
||||
$decoded = Key::decode(
|
||||
project: new Document(['$id' => $projectId]),
|
||||
team: new Document(),
|
||||
user: new Document(['$id' => $userId, 'keys' => [
|
||||
new Document([
|
||||
'secret' => 'account_abcd1234',
|
||||
'expire' => null,
|
||||
'name' => 'Account key',
|
||||
'scopes' => $scopes
|
||||
])
|
||||
]]),
|
||||
key: 'account_efgh5678',
|
||||
);
|
||||
$this->assertEquals($projectId, $decoded->getProjectId());
|
||||
$this->assertEquals('', $decoded->getTeamId());
|
||||
$this->assertEquals('', $decoded->getUserId());
|
||||
$this->assertEquals(API_KEY_ACCOUNT, $decoded->getType());
|
||||
$this->assertEquals(User::ROLE_GUESTS, $decoded->getRole());
|
||||
$this->assertEquals($guestRoleScopes, $decoded->getScopes());
|
||||
$this->assertEquals('UNKNOWN', $decoded->getName());
|
||||
|
||||
// Decode expired account key
|
||||
$scopes = ['teams.write'];
|
||||
$decoded = Key::decode(
|
||||
project: new Document(['$id' => $projectId]),
|
||||
team: new Document(),
|
||||
user: new Document(['$id' => $userId, 'keys' => [
|
||||
new Document([
|
||||
'secret' => 'account_abcd1234',
|
||||
'expire' => $yesterday,
|
||||
'name' => 'Account key',
|
||||
'scopes' => $scopes
|
||||
])
|
||||
]]),
|
||||
key: 'account_abcd1234',
|
||||
);
|
||||
$this->assertEquals(true, $decoded->isExpired());
|
||||
$this->assertEquals('', $decoded->getProjectId());
|
||||
$this->assertEquals('', $decoded->getTeamId());
|
||||
$this->assertEquals($userId, $decoded->getUserId());
|
||||
$this->assertEquals(API_KEY_ACCOUNT, $decoded->getType());
|
||||
$this->assertEquals(User::ROLE_USERS, $decoded->getRole());
|
||||
$this->assertEquals($scopes, $decoded->getScopes());
|
||||
$this->assertEquals('Account key', $decoded->getName());
|
||||
|
||||
// Decode organization key
|
||||
$teamId = 'team123';
|
||||
$scopes = ['projects.write'];
|
||||
$decoded = Key::decode(
|
||||
project: new Document(['$id' => $projectId]),
|
||||
team: new Document(['$id' => $teamId, 'keys' => [
|
||||
new Document([
|
||||
'secret' => 'organization_abcd1234',
|
||||
'expire' => null,
|
||||
'name' => 'Organization key',
|
||||
'scopes' => $scopes
|
||||
])
|
||||
]]),
|
||||
user: new Document(),
|
||||
key: 'organization_abcd1234',
|
||||
);
|
||||
$this->assertEquals('', $decoded->getProjectId());
|
||||
$this->assertEquals($teamId, $decoded->getTeamId());
|
||||
$this->assertEquals('', $decoded->getUserId());
|
||||
$this->assertEquals(API_KEY_ORGANIZATION, $decoded->getType());
|
||||
$this->assertEquals(User::ROLE_APPS, $decoded->getRole());
|
||||
$this->assertEquals($scopes, $decoded->getScopes());
|
||||
$this->assertEquals('Organization key', $decoded->getName());
|
||||
|
||||
// Decode invalid organization key
|
||||
$scopes = ['projects.write'];
|
||||
$decoded = Key::decode(
|
||||
project: new Document(['$id' => $projectId]),
|
||||
team: new Document(['$id' => $teamId, 'keys' => [
|
||||
new Document([
|
||||
'secret' => 'organization_abcd1234',
|
||||
'expire' => null,
|
||||
'name' => 'Organization key',
|
||||
'scopes' => $scopes
|
||||
])
|
||||
]]),
|
||||
user: new Document(),
|
||||
key: 'organization_efgh5678',
|
||||
);
|
||||
$this->assertEquals($projectId, $decoded->getProjectId());
|
||||
$this->assertEquals('', $decoded->getTeamId());
|
||||
$this->assertEquals('', $decoded->getUserId());
|
||||
$this->assertEquals(API_KEY_ORGANIZATION, $decoded->getType());
|
||||
$this->assertEquals(User::ROLE_GUESTS, $decoded->getRole());
|
||||
$this->assertEquals($guestRoleScopes, $decoded->getScopes());
|
||||
$this->assertEquals('UNKNOWN', $decoded->getName());
|
||||
|
||||
// Decode expired organization key
|
||||
$scopes = ['projects.write'];
|
||||
$decoded = Key::decode(
|
||||
project: new Document(['$id' => $projectId]),
|
||||
team: new Document(['$id' => $teamId, 'keys' => [
|
||||
new Document([
|
||||
'secret' => 'organization_abcd1234',
|
||||
'expire' => $yesterday,
|
||||
'name' => 'Organization key',
|
||||
'scopes' => $scopes
|
||||
])
|
||||
]]),
|
||||
user: new Document(),
|
||||
key: 'organization_abcd1234',
|
||||
);
|
||||
$this->assertEquals(true, $decoded->isExpired());
|
||||
$this->assertEquals('', $decoded->getProjectId());
|
||||
$this->assertEquals($teamId, $decoded->getTeamId());
|
||||
$this->assertEquals('', $decoded->getUserId());
|
||||
$this->assertEquals(API_KEY_ORGANIZATION, $decoded->getType());
|
||||
$this->assertEquals(User::ROLE_APPS, $decoded->getRole());
|
||||
$this->assertEquals($scopes, $decoded->getScopes());
|
||||
$this->assertEquals('Organization key', $decoded->getName());
|
||||
}
|
||||
|
||||
private static function generateKey(
|
||||
string $projectId,
|
||||
bool $usage,
|
||||
array $scopes,
|
||||
int $maxAge = 86400,
|
||||
?int $timestamp = null,
|
||||
array $extra = []
|
||||
): string {
|
||||
$jwt = new JWT(
|
||||
key: System::getEnv('_APP_OPENSSL_KEY_V1'),
|
||||
algo: 'HS256',
|
||||
maxAge: 86400,
|
||||
maxAge: $maxAge,
|
||||
leeway: 0,
|
||||
);
|
||||
$jwt->setTestTimestamp($timestamp);
|
||||
|
||||
$apiKey = $jwt->encode([
|
||||
$apiKey = $jwt->encode(\array_merge([
|
||||
'projectId' => $projectId,
|
||||
'usage' => $usage,
|
||||
'scopes' => $scopes,
|
||||
]);
|
||||
], $extra));
|
||||
|
||||
return API_KEY_DYNAMIC . '_' . $apiKey;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue