mirror of
https://github.com/appwrite/appwrite.git
synced 2026-05-26 13:51:13 +00:00
Merge branch '1.6.x' of github.com:appwrite/appwrite into mock-numbers
This commit is contained in:
@@ -2355,7 +2355,8 @@ App::post('/v1/account/tokens/phone')
|
||||
->dynamic($token, Response::MODEL_TOKEN);
|
||||
});
|
||||
|
||||
App::post('/v1/account/jwt')
|
||||
App::post('/v1/account/jwts')
|
||||
->alias('/v1/account/jwt')
|
||||
->desc('Create JWT')
|
||||
->groups(['api', 'account', 'auth'])
|
||||
->label('scope', 'account')
|
||||
@@ -2388,15 +2389,11 @@ App::post('/v1/account/jwt')
|
||||
throw new Exception(Exception::USER_SESSION_NOT_FOUND);
|
||||
}
|
||||
|
||||
$jwt = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 900, 10); // Instantiate with key, algo, maxAge and leeway.
|
||||
$jwt = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 900, 0);
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||
->dynamic(new Document(['jwt' => $jwt->encode([
|
||||
// 'uid' => 1,
|
||||
// 'aud' => 'http://site.com',
|
||||
// 'scopes' => ['user'],
|
||||
// 'iss' => 'http://api.mysite.com',
|
||||
'userId' => $user->getId(),
|
||||
'sessionId' => $current->getId(),
|
||||
])]), Response::MODEL_JWT);
|
||||
|
||||
@@ -151,6 +151,7 @@ App::post('/v1/functions')
|
||||
->param('logging', true, new Boolean(), 'Whether executions will be logged. When set to false, executions will not be logged, but will reduce resource used by your Appwrite project.', 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('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)
|
||||
@@ -169,7 +170,7 @@ App::post('/v1/functions')
|
||||
->inject('queueForBuilds')
|
||||
->inject('dbForConsole')
|
||||
->inject('gitHub')
|
||||
->action(function (string $functionId, string $name, string $runtime, array $execute, array $events, string $schedule, int $timeout, bool $enabled, bool $logging, string $entrypoint, string $commands, string $installationId, string $providerRepositoryId, string $providerBranch, bool $providerSilentMode, string $providerRootDirectory, string $templateRepository, string $templateOwner, string $templateRootDirectory, string $templateBranch, Request $request, Response $response, Database $dbForProject, Document $project, Document $user, Event $queueForEvents, Build $queueForBuilds, Database $dbForConsole, GitHub $github) use ($redeployVcs) {
|
||||
->action(function (string $functionId, string $name, string $runtime, array $execute, array $events, string $schedule, int $timeout, bool $enabled, bool $logging, string $entrypoint, string $commands, array $scopes, string $installationId, string $providerRepositoryId, string $providerBranch, bool $providerSilentMode, string $providerRootDirectory, string $templateRepository, string $templateOwner, string $templateRootDirectory, string $templateBranch, Request $request, Response $response, Database $dbForProject, Document $project, Document $user, Event $queueForEvents, Build $queueForBuilds, Database $dbForConsole, GitHub $github) use ($redeployVcs) {
|
||||
$functionId = ($functionId == 'unique()') ? ID::unique() : $functionId;
|
||||
|
||||
$allowList = \array_filter(\explode(',', System::getEnv('_APP_FUNCTIONS_RUNTIMES', '')));
|
||||
@@ -219,6 +220,7 @@ App::post('/v1/functions')
|
||||
'timeout' => $timeout,
|
||||
'entrypoint' => $entrypoint,
|
||||
'commands' => $commands,
|
||||
'scopes' => $scopes,
|
||||
'search' => implode(' ', [$functionId, $name, $runtime]),
|
||||
'version' => 'v3',
|
||||
'installationId' => $installation->getId(),
|
||||
@@ -682,6 +684,7 @@ App::put('/v1/functions/:functionId')
|
||||
->param('logging', true, new Boolean(), 'Whether executions will be logged. When set to false, executions will not be logged, but will reduce resource used by your Appwrite project.', 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('installationId', '', new Text(128, 0), 'Appwrite Installation ID for VCS (Version Controle 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)
|
||||
@@ -695,7 +698,7 @@ App::put('/v1/functions/:functionId')
|
||||
->inject('queueForBuilds')
|
||||
->inject('dbForConsole')
|
||||
->inject('gitHub')
|
||||
->action(function (string $functionId, string $name, string $runtime, array $execute, array $events, string $schedule, int $timeout, bool $enabled, bool $logging, string $entrypoint, string $commands, string $installationId, string $providerRepositoryId, string $providerBranch, bool $providerSilentMode, string $providerRootDirectory, Request $request, Response $response, Database $dbForProject, Document $project, Event $queueForEvents, Build $queueForBuilds, Database $dbForConsole, GitHub $github) use ($redeployVcs) {
|
||||
->action(function (string $functionId, string $name, string $runtime, array $execute, array $events, string $schedule, int $timeout, bool $enabled, bool $logging, string $entrypoint, string $commands, array $scopes, string $installationId, string $providerRepositoryId, string $providerBranch, bool $providerSilentMode, string $providerRootDirectory, Request $request, Response $response, Database $dbForProject, Document $project, Event $queueForEvents, Build $queueForBuilds, Database $dbForConsole, GitHub $github) use ($redeployVcs) {
|
||||
// TODO: If only branch changes, re-deploy
|
||||
|
||||
$function = $dbForProject->getDocument('functions', $functionId);
|
||||
@@ -807,6 +810,7 @@ App::put('/v1/functions/:functionId')
|
||||
'logging' => $logging,
|
||||
'entrypoint' => $entrypoint,
|
||||
'commands' => $commands,
|
||||
'scopes' => $scopes,
|
||||
'installationId' => $installation->getId(),
|
||||
'installationInternalId' => $installation->getInternalId(),
|
||||
'providerRepositoryId' => $providerRepositoryId,
|
||||
@@ -1430,9 +1434,10 @@ App::delete('/v1/functions/:functionId/deployments/:deploymentId')
|
||||
$response->noContent();
|
||||
});
|
||||
|
||||
App::post('/v1/functions/:functionId/deployments/:deploymentId/builds/:buildId')
|
||||
App::post('/v1/functions/:functionId/deployments/:deploymentId/build')
|
||||
->alias('/v1/functions/:functionId/deployments/:deploymentId/builds/:buildId')
|
||||
->groups(['api', 'functions'])
|
||||
->desc('Create build')
|
||||
->desc('Rebuild deployment')
|
||||
->label('scope', 'functions.write')
|
||||
->label('event', 'functions.[functionId].deployments.[deploymentId].update')
|
||||
->label('audits.event', 'deployment.update')
|
||||
@@ -1440,12 +1445,11 @@ App::post('/v1/functions/:functionId/deployments/:deploymentId/builds/:buildId')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.namespace', 'functions')
|
||||
->label('sdk.method', 'createBuild')
|
||||
->label('sdk.description', '/docs/references/functions/create-build.md')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_NOCONTENT)
|
||||
->label('sdk.response.model', Response::MODEL_NONE)
|
||||
->param('functionId', '', new UID(), 'Function ID.')
|
||||
->param('deploymentId', '', new UID(), 'Deployment ID.')
|
||||
->param('buildId', '', new UID(), 'Build unique ID.')
|
||||
->param('buildId', '', new UID(), 'Build unique ID.', true) // added as optional param for backward compatibility
|
||||
->inject('request')
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
@@ -1453,25 +1457,17 @@ App::post('/v1/functions/:functionId/deployments/:deploymentId/builds/:buildId')
|
||||
->inject('queueForEvents')
|
||||
->inject('queueForBuilds')
|
||||
->action(function (string $functionId, string $deploymentId, string $buildId, Request $request, Response $response, Database $dbForProject, Document $project, Event $queueForEvents, Build $queueForBuilds) {
|
||||
|
||||
$function = $dbForProject->getDocument('functions', $functionId);
|
||||
|
||||
if ($function->isEmpty()) {
|
||||
throw new Exception(Exception::FUNCTION_NOT_FOUND);
|
||||
}
|
||||
|
||||
$deployment = $dbForProject->getDocument('deployments', $deploymentId);
|
||||
|
||||
if ($deployment->isEmpty()) {
|
||||
throw new Exception(Exception::DEPLOYMENT_NOT_FOUND);
|
||||
}
|
||||
|
||||
$build = Authorization::skip(fn () => $dbForProject->getDocument('builds', $buildId));
|
||||
|
||||
if ($build->isEmpty()) {
|
||||
throw new Exception(Exception::BUILD_NOT_FOUND);
|
||||
}
|
||||
|
||||
$deploymentId = ID::unique();
|
||||
|
||||
$deployment->removeAttribute('$internalId');
|
||||
@@ -1496,6 +1492,86 @@ App::post('/v1/functions/:functionId/deployments/:deploymentId/builds/:buildId')
|
||||
$response->noContent();
|
||||
});
|
||||
|
||||
App::patch('/v1/functions/:functionId/deployments/:deploymentId/build')
|
||||
->groups(['api', 'functions'])
|
||||
->desc('Cancel deployment')
|
||||
->label('scope', 'functions.write')
|
||||
->label('audits.event', 'deployment.update')
|
||||
->label('audits.resource', 'function/{request.functionId}')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.namespace', 'functions')
|
||||
->label('sdk.method', 'updateDeploymentBuild')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_OK)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_BUILD)
|
||||
->param('functionId', '', new UID(), 'Function ID.')
|
||||
->param('deploymentId', '', new UID(), 'Deployment ID.')
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('project')
|
||||
->inject('queueForEvents')
|
||||
->action(function (string $functionId, string $deploymentId, Response $response, Database $dbForProject, Document $project, Event $queueForEvents) {
|
||||
$function = $dbForProject->getDocument('functions', $functionId);
|
||||
|
||||
if ($function->isEmpty()) {
|
||||
throw new Exception(Exception::FUNCTION_NOT_FOUND);
|
||||
}
|
||||
|
||||
$deployment = $dbForProject->getDocument('deployments', $deploymentId);
|
||||
|
||||
if ($deployment->isEmpty()) {
|
||||
throw new Exception(Exception::DEPLOYMENT_NOT_FOUND);
|
||||
}
|
||||
|
||||
$build = Authorization::skip(fn () => $dbForProject->getDocument('builds', $deployment->getAttribute('buildId', '')));
|
||||
|
||||
if ($build->isEmpty()) {
|
||||
$buildId = ID::unique();
|
||||
$build = $dbForProject->createDocument('builds', new Document([
|
||||
'$id' => $buildId,
|
||||
'$permissions' => [],
|
||||
'startTime' => DateTime::now(),
|
||||
'deploymentInternalId' => $deployment->getInternalId(),
|
||||
'deploymentId' => $deployment->getId(),
|
||||
'status' => 'canceled',
|
||||
'path' => '',
|
||||
'runtime' => $function->getAttribute('runtime'),
|
||||
'source' => $deployment->getAttribute('path', ''),
|
||||
'sourceType' => '',
|
||||
'logs' => '',
|
||||
'duration' => 0,
|
||||
'size' => 0
|
||||
]));
|
||||
|
||||
$deployment->setAttribute('buildId', $build->getId());
|
||||
$deployment->setAttribute('buildInternalId', $build->getInternalId());
|
||||
$deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), $deployment);
|
||||
} else {
|
||||
if (\in_array($build->getAttribute('status'), ['ready', 'failed'])) {
|
||||
throw new Exception(Exception::BUILD_ALREADY_COMPLETED);
|
||||
}
|
||||
|
||||
$startTime = new \DateTime($build->getAttribute('startTime'));
|
||||
$endTime = new \DateTime('now');
|
||||
$duration = $endTime->getTimestamp() - $startTime->getTimestamp();
|
||||
|
||||
$build = $dbForProject->updateDocument('builds', $build->getId(), $build->setAttributes([
|
||||
'endTime' => DateTime::now(),
|
||||
'duration' => $duration,
|
||||
'status' => 'canceled'
|
||||
]));
|
||||
}
|
||||
|
||||
$executor = new Executor(App::getEnv('_APP_EXECUTOR_HOST'));
|
||||
$deleteBuild = $executor->deleteRuntime($project->getId(), $deploymentId . "-build");
|
||||
|
||||
$queueForEvents
|
||||
->setParam('functionId', $function->getId())
|
||||
->setParam('deploymentId', $deployment->getId());
|
||||
|
||||
$response->dynamic($build, Response::MODEL_BUILD);
|
||||
});
|
||||
|
||||
App::post('/v1/functions/:functionId/executions')
|
||||
->groups(['api', 'functions'])
|
||||
->desc('Create execution')
|
||||
@@ -1582,7 +1658,8 @@ App::post('/v1/functions/:functionId/executions')
|
||||
}
|
||||
|
||||
if (!$current->isEmpty()) {
|
||||
$jwtObj = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 900, 10); // Instantiate with key, algo, maxAge and leeway.
|
||||
$jwtExpiry = $function->getAttribute('timeout', 900);
|
||||
$jwtObj = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', $jwtExpiry, 0);
|
||||
$jwt = $jwtObj->encode([
|
||||
'userId' => $user->getId(),
|
||||
'sessionId' => $current->getId(),
|
||||
@@ -1590,6 +1667,14 @@ App::post('/v1/functions/:functionId/executions')
|
||||
}
|
||||
}
|
||||
|
||||
$jwtExpiry = $function->getAttribute('timeout', 900);
|
||||
$jwtObj = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', $jwtExpiry, 0);
|
||||
$apiKey = $jwtObj->encode([
|
||||
'projectId' => $project->getId(),
|
||||
'scopes' => $function->getAttribute('scopes', [])
|
||||
]);
|
||||
|
||||
$headers['x-appwrite-key'] = API_KEY_DYNAMIC . '_' . $apiKey;
|
||||
$headers['x-appwrite-trigger'] = 'http';
|
||||
$headers['x-appwrite-user-id'] = $user->getId() ?? '';
|
||||
$headers['x-appwrite-user-jwt'] = $jwt ?? '';
|
||||
@@ -1694,8 +1779,13 @@ App::post('/v1/functions/:functionId/executions')
|
||||
$vars[$var->getAttribute('key')] = $var->getAttribute('value', '');
|
||||
}
|
||||
|
||||
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') == 'disabled' ? 'http' : 'https';
|
||||
$hostname = System::getEnv('_APP_DOMAIN');
|
||||
$endpoint = $protocol . '://' . $hostname . "/v1";
|
||||
|
||||
// Appwrite vars
|
||||
$vars = \array_merge($vars, [
|
||||
'APPWRITE_FUNCTION_API_ENDPOINT' => $endpoint,
|
||||
'APPWRITE_FUNCTION_ID' => $functionId,
|
||||
'APPWRITE_FUNCTION_NAME' => $function->getAttribute('name'),
|
||||
'APPWRITE_FUNCTION_DEPLOYMENT' => $deployment->getId(),
|
||||
|
||||
@@ -2939,11 +2939,9 @@ App::post('/v1/messaging/messages/push')
|
||||
$expiry = (new \DateTime())->add(new \DateInterval('P15D'))->format('U');
|
||||
}
|
||||
|
||||
$encoder = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'));
|
||||
$encoder = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', \intval($expiry), 0);
|
||||
|
||||
$jwt = $encoder->encode([
|
||||
'iat' => \time(),
|
||||
'exp' => $expiry,
|
||||
'bucketId' => $bucket->getId(),
|
||||
'fileId' => $file->getId(),
|
||||
'projectId' => $project->getId(),
|
||||
@@ -3801,11 +3799,9 @@ App::patch('/v1/messaging/messages/push/:messageId')
|
||||
$expiry = (new \DateTime())->add(new \DateInterval('P15D'))->format('U');
|
||||
}
|
||||
|
||||
$encoder = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'));
|
||||
$encoder = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', \intval($expiry), 0);
|
||||
|
||||
$jwt = $encoder->encode([
|
||||
'iat' => \time(),
|
||||
'exp' => $expiry,
|
||||
'bucketId' => $bucket->getId(),
|
||||
'fileId' => $file->getId(),
|
||||
'projectId' => $project->getId(),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<?php
|
||||
|
||||
use Ahc\Jwt\JWT;
|
||||
use Appwrite\Auth\Auth;
|
||||
use Appwrite\Auth\Validator\MockNumber;
|
||||
use Appwrite\Event\Delete;
|
||||
@@ -1239,7 +1240,7 @@ App::post('/v1/projects/:projectId/keys')
|
||||
'expire' => $expire,
|
||||
'sdks' => [],
|
||||
'accessedAt' => null,
|
||||
'secret' => \bin2hex(\random_bytes(128)),
|
||||
'secret' => API_KEY_STANDARD . '_' . \bin2hex(\random_bytes(128)),
|
||||
]);
|
||||
|
||||
$key = $dbForConsole->createDocument('keys', $key);
|
||||
@@ -1400,6 +1401,41 @@ App::delete('/v1/projects/:projectId/keys/:keyId')
|
||||
$response->noContent();
|
||||
});
|
||||
|
||||
// JWT Keys
|
||||
|
||||
App::post('/v1/projects/:projectId/jwts')
|
||||
->groups(['api', 'projects'])
|
||||
->desc('Create JWT')
|
||||
->label('scope', 'projects.write')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
|
||||
->label('sdk.namespace', 'projects')
|
||||
->label('sdk.method', 'createJWT')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_CREATED)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_JWT)
|
||||
->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('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('dbForConsole')
|
||||
->action(function (string $projectId, array $scopes, int $duration, Response $response, Database $dbForConsole) {
|
||||
|
||||
$project = $dbForConsole->getDocument('projects', $projectId);
|
||||
|
||||
if ($project->isEmpty()) {
|
||||
throw new Exception(Exception::PROJECT_NOT_FOUND);
|
||||
}
|
||||
|
||||
$jwt = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', $duration, 0);
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||
->dynamic(new Document(['jwt' => API_KEY_DYNAMIC . '_' . $jwt->encode([
|
||||
'projectId' => $project->getId(),
|
||||
'scopes' => $scopes
|
||||
])]), Response::MODEL_JWT);
|
||||
});
|
||||
|
||||
// Platforms
|
||||
|
||||
App::post('/v1/projects/:projectId/platforms')
|
||||
|
||||
@@ -1309,7 +1309,7 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/push')
|
||||
->action(function (string $bucketId, string $fileId, string $jwt, Response $response, Request $request, Database $dbForProject, Document $project, string $mode, Device $deviceForFiles) {
|
||||
$bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
|
||||
|
||||
$decoder = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'));
|
||||
$decoder = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 3600, 0);
|
||||
|
||||
try {
|
||||
$decoded = $decoder->decode($jwt);
|
||||
@@ -1320,8 +1320,7 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/push')
|
||||
if (
|
||||
$decoded['projectId'] !== $project->getId() ||
|
||||
$decoded['bucketId'] !== $bucketId ||
|
||||
$decoded['fileId'] !== $fileId ||
|
||||
$decoded['exp'] < \time()
|
||||
$decoded['fileId'] !== $fileId
|
||||
) {
|
||||
throw new Exception(Exception::USER_UNAUTHORIZED);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<?php
|
||||
|
||||
use Ahc\Jwt\JWT;
|
||||
use Appwrite\Auth\Auth;
|
||||
use Appwrite\Auth\MFA\Type;
|
||||
use Appwrite\Auth\MFA\Type\TOTP;
|
||||
@@ -39,6 +40,7 @@ use Utopia\Database\Validator\Query\Limit;
|
||||
use Utopia\Database\Validator\Query\Offset;
|
||||
use Utopia\Database\Validator\UID;
|
||||
use Utopia\Locale\Locale;
|
||||
use Utopia\System\System;
|
||||
use Utopia\Validator\ArrayList;
|
||||
use Utopia\Validator\Assoc;
|
||||
use Utopia\Validator\Boolean;
|
||||
@@ -2095,6 +2097,60 @@ App::delete('/v1/users/identities/:identityId')
|
||||
return $response->noContent();
|
||||
});
|
||||
|
||||
App::post('/v1/users/:userId/jwts')
|
||||
->desc('Create user JWT')
|
||||
->groups(['api', 'users'])
|
||||
->label('scope', 'users.write')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.namespace', 'users')
|
||||
->label('sdk.method', 'createJWT')
|
||||
->label('sdk.description', '/docs/references/users/create-user-jwt.md')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_CREATED)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_JWT)
|
||||
->param('userId', '', new UID(), 'User ID.')
|
||||
->param('sessionId', 'recent', new UID(), 'Session ID. Use the string \'recent\' to use the most recent session. Defaults to the most recent session.', true)
|
||||
->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('dbForProject')
|
||||
->action(function (string $userId, string $sessionId, int $duration, Response $response, Database $dbForProject) {
|
||||
|
||||
$user = $dbForProject->getDocument('users', $userId);
|
||||
|
||||
if ($user->isEmpty()) {
|
||||
throw new Exception(Exception::USER_NOT_FOUND);
|
||||
}
|
||||
|
||||
$sessions = $user->getAttribute('sessions', []);
|
||||
$session = new Document();
|
||||
|
||||
if($sessionId === 'recent') {
|
||||
// Get most recent
|
||||
$session = \count($sessions) > 0 ? $sessions[\count($sessions) - 1] : new Document();
|
||||
} else {
|
||||
// Find by ID
|
||||
foreach ($sessions as $loopSession) { /** @var Utopia\Database\Document $loopSession */
|
||||
if ($loopSession->getId() == $sessionId) {
|
||||
$session = $loopSession;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($session->isEmpty()) {
|
||||
throw new Exception(Exception::USER_SESSION_NOT_FOUND);
|
||||
}
|
||||
|
||||
$jwt = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', $duration, 0);
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||
->dynamic(new Document(['jwt' => $jwt->encode([
|
||||
'userId' => $user->getId(),
|
||||
'sessionId' => $session->getId()
|
||||
])]), Response::MODEL_JWT);
|
||||
});
|
||||
|
||||
App::get('/v1/users/usage')
|
||||
->desc('Get users usage stats')
|
||||
->groups(['api', 'users'])
|
||||
|
||||
@@ -6,6 +6,7 @@ use Appwrite\Extend\Exception;
|
||||
use Appwrite\Utopia\Request;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Utopia\App;
|
||||
use Utopia\Config\Config;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Helpers\ID;
|
||||
@@ -154,6 +155,55 @@ App::patch('/v1/mock/functions-v2')
|
||||
$response->noContent();
|
||||
});
|
||||
|
||||
App::post('/v1/mock/api-key-unprefixed')
|
||||
->desc('Create API Key (without standard prefix)')
|
||||
->groups(['mock', 'api', 'projects'])
|
||||
->label('scope', 'projects.write')
|
||||
->label('docs', false)
|
||||
->param('projectId', '', new UID(), 'Project ID.')
|
||||
->inject('response')
|
||||
->inject('dbForConsole')
|
||||
->action(function (string $projectId, Response $response, Database $dbForConsole) {
|
||||
$isDevelopment = System::getEnv('_APP_ENV', 'development') === 'development';
|
||||
|
||||
if (!$isDevelopment) {
|
||||
throw new Exception(Exception::GENERAL_NOT_IMPLEMENTED);
|
||||
}
|
||||
|
||||
$project = $dbForConsole->getDocument('projects', $projectId);
|
||||
|
||||
if ($project->isEmpty()) {
|
||||
throw new Exception(Exception::PROJECT_NOT_FOUND);
|
||||
}
|
||||
|
||||
$scopes = array_keys(Config::getParam('scopes'));
|
||||
|
||||
$key = new Document([
|
||||
'$id' => ID::unique(),
|
||||
'$permissions' => [
|
||||
Permission::read(Role::any()),
|
||||
Permission::update(Role::any()),
|
||||
Permission::delete(Role::any()),
|
||||
],
|
||||
'projectInternalId' => $project->getInternalId(),
|
||||
'projectId' => $project->getId(),
|
||||
'name' => 'Outdated key',
|
||||
'scopes' => $scopes,
|
||||
'expire' => null,
|
||||
'sdks' => [],
|
||||
'accessedAt' => null,
|
||||
'secret' => \bin2hex(\random_bytes(128)),
|
||||
]);
|
||||
|
||||
$key = $dbForConsole->createDocument('keys', $key);
|
||||
|
||||
$dbForConsole->purgeCachedDocument('projects', $project->getId());
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||
->dynamic($key, Response::MODEL_KEY);
|
||||
});
|
||||
|
||||
App::get('/v1/mock/github/callback')
|
||||
->desc('Create installation document using GitHub installation id')
|
||||
->groups(['mock', 'api', 'vcs'])
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
use Ahc\Jwt\JWT;
|
||||
use Ahc\Jwt\JWTException;
|
||||
use Appwrite\Auth\Auth;
|
||||
use Appwrite\Auth\MFA\Type\TOTP;
|
||||
use Appwrite\Event\Audit;
|
||||
@@ -195,56 +197,99 @@ App::init()
|
||||
$scope = $route->getLabel('scope', 'none'); // Allowed scope for chosen route
|
||||
$scopes = $roles[$role]['scopes']; // Allowed scopes for user role
|
||||
|
||||
$authKey = $request->getHeader('x-appwrite-key', '');
|
||||
$apiKey = $request->getHeader('x-appwrite-key', '');
|
||||
|
||||
if (!empty($authKey)) { // API Key authentication
|
||||
// API Key authentication
|
||||
if (!empty($apiKey)) {
|
||||
// Do not allow API key and session to be set at the same time
|
||||
if (!$user->isEmpty()) {
|
||||
throw new Exception(Exception::USER_API_KEY_AND_SESSION_SET);
|
||||
}
|
||||
|
||||
// Check if given key match project API keys
|
||||
$key = $project->find('secret', $authKey, 'keys');
|
||||
if ($key) {
|
||||
$user = new Document([
|
||||
'$id' => '',
|
||||
'status' => true,
|
||||
'email' => 'app.' . $project->getId() . '@service.' . $request->getHostname(),
|
||||
'password' => '',
|
||||
'name' => $project->getAttribute('name', 'Untitled'),
|
||||
]);
|
||||
if(!\str_contains($apiKey, '_')) {
|
||||
$keyType = API_KEY_STANDARD;
|
||||
$authKey = $apiKey;
|
||||
} else {
|
||||
[ $keyType, $authKey ] = \explode('_', $apiKey, 2);
|
||||
}
|
||||
|
||||
$role = Auth::USER_ROLE_APPS;
|
||||
$scopes = \array_merge($roles[$role]['scopes'], $key->getAttribute('scopes', []));
|
||||
if($keyType === API_KEY_DYNAMIC) {
|
||||
// Dynamic key
|
||||
|
||||
$expire = $key->getAttribute('expire');
|
||||
if (!empty($expire) && $expire < DateTime::formatTz(DateTime::now())) {
|
||||
throw new Exception(Exception::PROJECT_KEY_EXPIRED);
|
||||
$jwtObj = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 3600, 0);
|
||||
|
||||
try {
|
||||
$payload = $jwtObj->decode($authKey);
|
||||
} catch (JWTException $error) {
|
||||
throw new Exception(Exception::API_KEY_EXPIRED);
|
||||
}
|
||||
|
||||
Authorization::setRole(Auth::USER_ROLE_APPS);
|
||||
Authorization::setDefaultStatus(false); // Cancel security segmentation for API keys.
|
||||
$projectId = $payload['projectId'] ?? '';
|
||||
$tokenScopes = $payload['scopes'] ?? [];
|
||||
|
||||
$accessedAt = $key->getAttribute('accessedAt', '');
|
||||
if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_KEY_ACCCESS)) > $accessedAt) {
|
||||
$key->setAttribute('accessedAt', DateTime::now());
|
||||
$dbForConsole->updateDocument('keys', $key->getId(), $key);
|
||||
$dbForConsole->purgeCachedDocument('projects', $project->getId());
|
||||
// JWT includes project ID for better security
|
||||
if ($projectId === $project->getId()) {
|
||||
$user = new Document([
|
||||
'$id' => '',
|
||||
'status' => true,
|
||||
'email' => 'app.' . $project->getId() . '@service.' . $request->getHostname(),
|
||||
'password' => '',
|
||||
'name' => $project->getAttribute('name', 'Untitled'),
|
||||
]);
|
||||
|
||||
$role = Auth::USER_ROLE_APPS;
|
||||
$scopes = \array_merge($roles[$role]['scopes'], $tokenScopes);
|
||||
|
||||
Authorization::setRole(Auth::USER_ROLE_APPS);
|
||||
Authorization::setDefaultStatus(false); // Cancel security segmentation for API keys.
|
||||
}
|
||||
} elseif($keyType === API_KEY_STANDARD) {
|
||||
// No underline means no prefix. Backwards compatibility.
|
||||
// Regular key
|
||||
|
||||
$sdkValidator = new WhiteList($servers, true);
|
||||
$sdk = $request->getHeader('x-sdk-name', 'UNKNOWN');
|
||||
if ($sdkValidator->isValid($sdk)) {
|
||||
$sdks = $key->getAttribute('sdks', []);
|
||||
if (!in_array($sdk, $sdks)) {
|
||||
array_push($sdks, $sdk);
|
||||
$key->setAttribute('sdks', $sdks);
|
||||
// Check if given key match project API keys
|
||||
$key = $project->find('secret', $apiKey, 'keys');
|
||||
if ($key) {
|
||||
$user = new Document([
|
||||
'$id' => '',
|
||||
'status' => true,
|
||||
'email' => 'app.' . $project->getId() . '@service.' . $request->getHostname(),
|
||||
'password' => '',
|
||||
'name' => $project->getAttribute('name', 'Untitled'),
|
||||
]);
|
||||
|
||||
/** Update access time as well */
|
||||
$key->setAttribute('accessedAt', Datetime::now());
|
||||
$role = Auth::USER_ROLE_APPS;
|
||||
$scopes = \array_merge($roles[$role]['scopes'], $key->getAttribute('scopes', []));
|
||||
|
||||
$expire = $key->getAttribute('expire');
|
||||
if (!empty($expire) && $expire < DateTime::formatTz(DateTime::now())) {
|
||||
throw new Exception(Exception::PROJECT_KEY_EXPIRED);
|
||||
}
|
||||
|
||||
Authorization::setRole(Auth::USER_ROLE_APPS);
|
||||
Authorization::setDefaultStatus(false); // Cancel security segmentation for API keys.
|
||||
|
||||
$accessedAt = $key->getAttribute('accessedAt', '');
|
||||
if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_KEY_ACCCESS)) > $accessedAt) {
|
||||
$key->setAttribute('accessedAt', DateTime::now());
|
||||
$dbForConsole->updateDocument('keys', $key->getId(), $key);
|
||||
$dbForConsole->purgeCachedDocument('projects', $project->getId());
|
||||
}
|
||||
|
||||
$sdkValidator = new WhiteList($servers, true);
|
||||
$sdk = $request->getHeader('x-sdk-name', 'UNKNOWN');
|
||||
if ($sdkValidator->isValid($sdk)) {
|
||||
$sdks = $key->getAttribute('sdks', []);
|
||||
if (!in_array($sdk, $sdks)) {
|
||||
array_push($sdks, $sdk);
|
||||
$key->setAttribute('sdks', $sdks);
|
||||
|
||||
/** Update access time as well */
|
||||
$key->setAttribute('accessedAt', Datetime::now());
|
||||
$dbForConsole->updateDocument('keys', $key->getId(), $key);
|
||||
$dbForConsole->purgeCachedDocument('projects', $project->getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user