Reuse resource tokens for orchestrator artifacts

This commit is contained in:
Chirag Aggarwal
2026-05-23 09:17:44 +05:30
parent bb9b08d525
commit 570be46775
11 changed files with 146 additions and 111 deletions
+1
View File
@@ -485,6 +485,7 @@ const ADVISOR_REPORT_TYPES = [
// Resource types for Tokens
const TOKENS_RESOURCE_TYPE_FILES = 'files';
const TOKENS_RESOURCE_TYPE_DEPLOYMENT_ARTIFACTS = 'deploymentArtifacts';
const TOKENS_RESOURCE_TYPE_SITES = 'sites';
const TOKENS_RESOURCE_TYPE_FUNCTIONS = 'functions';
const TOKENS_RESOURCE_TYPE_DATABASES = 'databases';
+25
View File
@@ -1243,6 +1243,31 @@ return function (Container $context): void {
'fileInternalId' => $sequences[1],
]);
})(),
TOKENS_RESOURCE_TYPE_DEPLOYMENT_ARTIFACTS => (function () use ($token, $dbForProject, $authorization) {
$sequences = explode(':', $token->getAttribute('resourceInternalId'));
$ids = explode(':', $token->getAttribute('resourceId'));
if (count($sequences) !== 2 || count($ids) !== 4) {
return new Document([]);
}
$accessedAt = $token->getAttribute('accessedAt', 0);
if (empty($accessedAt) || DatabaseDateTime::formatTz(DatabaseDateTime::addSeconds(new \DateTime(), -APP_RESOURCE_TOKEN_ACCESS)) > $accessedAt) {
$token->setAttribute('accessedAt', DatabaseDateTime::now());
$authorization->skip(fn () => $dbForProject->updateDocument('resourceTokens', $token->getId(), new Document([
'accessedAt' => $token->getAttribute('accessedAt')
])));
}
return new Document([
'resourceType' => $ids[0],
'resourceId' => $ids[1],
'deploymentId' => $ids[2],
'purpose' => $ids[3],
'resourceInternalId' => $sequences[0],
'deploymentInternalId' => $sequences[1],
]);
})(),
default => throw new Exception(Exception::TOKEN_RESOURCE_TYPE_INVALID),
};
Generated
+4 -4
View File
@@ -1230,12 +1230,12 @@
"source": {
"type": "git",
"url": "https://github.com/open-runtimes/orchestrator-client-php.git",
"reference": "5b44ad8f056d180e63368f844d01d167f706b13e"
"reference": "85d933c3d56467b2fbc09eac7813de7c2b1c47b1"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/open-runtimes/orchestrator-client-php/zipball/5b44ad8f056d180e63368f844d01d167f706b13e",
"reference": "5b44ad8f056d180e63368f844d01d167f706b13e",
"url": "https://api.github.com/repos/open-runtimes/orchestrator-client-php/zipball/85d933c3d56467b2fbc09eac7813de7c2b1c47b1",
"reference": "85d933c3d56467b2fbc09eac7813de7c2b1c47b1",
"shasum": ""
},
"require": {
@@ -1294,7 +1294,7 @@
"source": "https://github.com/open-runtimes/orchestrator-client-php/tree/add-jobs-sdk",
"issues": "https://github.com/open-runtimes/orchestrator-client-php/issues"
},
"time": "2026-05-22T18:46:15+00:00"
"time": "2026-05-23T03:41:55+00:00"
},
{
"name": "open-telemetry/api",
-89
View File
@@ -1,89 +0,0 @@
<?php
namespace Appwrite\Builds;
use Appwrite\Extend\Exception;
class OrchestratorToken
{
public static function create(string $projectId, string $resourceId, string $deploymentId, string $purpose, int $ttl = 3600): string
{
$expires = \time() + $ttl;
$payload = [
'projectId' => $projectId,
'resourceId' => $resourceId,
'deploymentId' => $deploymentId,
'purpose' => $purpose,
'expires' => $expires,
];
$encoded = self::base64UrlEncode(\json_encode($payload, JSON_THROW_ON_ERROR));
$signature = self::sign($encoded);
return $encoded . '.' . $signature;
}
public static function verify(string $token, string $projectId, string $resourceId, string $deploymentId, string $purpose): void
{
[$encoded, $signature] = \array_pad(\explode('.', $token, 2), 2, '');
if (empty($encoded) || empty($signature) || !\hash_equals(self::sign($encoded), $signature)) {
throw new Exception(Exception::USER_UNAUTHORIZED, 'Invalid build artifact token.');
}
$payload = \json_decode(self::base64UrlDecode($encoded), true);
if (!\is_array($payload)) {
throw new Exception(Exception::USER_UNAUTHORIZED, 'Invalid build artifact token.');
}
if (($payload['expires'] ?? 0) < \time()) {
throw new Exception(Exception::USER_UNAUTHORIZED, 'Build artifact token expired.');
}
if (
($payload['projectId'] ?? '') !== $projectId ||
($payload['resourceId'] ?? '') !== $resourceId ||
($payload['deploymentId'] ?? '') !== $deploymentId ||
($payload['purpose'] ?? '') !== $purpose
) {
throw new Exception(Exception::USER_UNAUTHORIZED, 'Build artifact token mismatch.');
}
}
public static function verifySignature(string $body, string $signature): void
{
$expected = 'sha256=' . \hash_hmac('sha256', $body, self::secret());
if (empty($signature) || !\hash_equals($expected, $signature)) {
throw new Exception(Exception::USER_UNAUTHORIZED, 'Invalid orchestrator event signature.');
}
}
private static function sign(string $payload): string
{
return \hash_hmac('sha256', $payload, self::secret());
}
private static function secret(): string
{
$secret = \Utopia\System\System::getEnv('_APP_ORCHESTRATOR_CALLBACK_SECRET', '');
if (empty($secret)) {
$secret = \Utopia\System\System::getEnv('_APP_OPENSSL_KEY_V1', '');
}
return $secret;
}
private static function base64UrlEncode(string $value): string
{
return \rtrim(\strtr(\base64_encode($value), '+/', '-_'), '=');
}
private static function base64UrlDecode(string $value): string
{
$value .= \str_repeat('=', (4 - \strlen($value) % 4) % 4);
return \base64_decode(\strtr($value, '-_', '+/')) ?: '';
}
}
@@ -2,7 +2,6 @@
namespace Appwrite\Platform\Modules\Functions\Http\Deployments\Artifacts\Build;
use Appwrite\Builds\OrchestratorToken;
use Appwrite\Event\Publisher\Build as BuildPublisher;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Compute\Http\Deployments\Artifacts\Build\ChunkedBuildArtifact;
@@ -43,6 +42,7 @@ class Update extends Action
->inject('request')
->inject('dbForProject')
->inject('project')
->inject('resourceToken')
->inject('deviceForBuilds')
->inject('publisherForBuilds')
->inject('cache')
@@ -58,13 +58,13 @@ class Update extends Action
Request $request,
Database $dbForProject,
Document $project,
Document $resourceToken,
Device $deviceForBuilds,
BuildPublisher $publisherForBuilds,
Cache $cache,
callable $locks
) {
$token = $token ?: $request->getQuery('token', '');
OrchestratorToken::verify($token, $project->getId(), $functionId, $deploymentId, 'build');
$this->verifyArtifactToken($resourceToken, RESOURCE_TYPE_FUNCTIONS, $functionId, $deploymentId, 'build');
$function = $dbForProject->getAuthorization()->skip(fn () => $dbForProject->getDocument('functions', $functionId));
if ($function->isEmpty()) {
@@ -90,4 +90,20 @@ class Update extends Action
locks: $locks
);
}
private function verifyArtifactToken(Document $resourceToken, string $resourceType, string $resourceId, string $deploymentId, string $purpose): void
{
if ($resourceToken->isEmpty()) {
throw new Exception(Exception::USER_UNAUTHORIZED, 'Invalid build artifact token.');
}
if (
$resourceToken->getAttribute('resourceType') !== $resourceType ||
$resourceToken->getAttribute('resourceId') !== $resourceId ||
$resourceToken->getAttribute('deploymentId') !== $deploymentId ||
$resourceToken->getAttribute('purpose') !== $purpose
) {
throw new Exception(Exception::USER_UNAUTHORIZED, 'Build artifact token mismatch.');
}
}
}
@@ -2,7 +2,6 @@
namespace Appwrite\Platform\Modules\Functions\Http\Deployments\Artifacts\Source;
use Appwrite\Builds\OrchestratorToken;
use Appwrite\Extend\Exception;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
@@ -37,6 +36,7 @@ class Get extends Action
->inject('response')
->inject('dbForProject')
->inject('project')
->inject('resourceToken')
->inject('deviceForFunctions')
->callback($this->action(...));
}
@@ -48,9 +48,10 @@ class Get extends Action
Response $response,
Database $dbForProject,
Document $project,
Document $resourceToken,
Device $deviceForFunctions
) {
OrchestratorToken::verify($token, $project->getId(), $functionId, $deploymentId, 'source');
$this->verifyArtifactToken($resourceToken, RESOURCE_TYPE_FUNCTIONS, $functionId, $deploymentId, 'source');
$function = $dbForProject->getAuthorization()->skip(fn () => $dbForProject->getDocument('functions', $functionId));
if ($function->isEmpty()) {
@@ -89,4 +90,20 @@ class Get extends Action
$response->send($deviceForFunctions->read($path));
}
private function verifyArtifactToken(Document $resourceToken, string $resourceType, string $resourceId, string $deploymentId, string $purpose): void
{
if ($resourceToken->isEmpty()) {
throw new Exception(Exception::USER_UNAUTHORIZED, 'Invalid build artifact token.');
}
if (
$resourceToken->getAttribute('resourceType') !== $resourceType ||
$resourceToken->getAttribute('resourceId') !== $resourceId ||
$resourceToken->getAttribute('deploymentId') !== $deploymentId ||
$resourceToken->getAttribute('purpose') !== $purpose
) {
throw new Exception(Exception::USER_UNAUTHORIZED, 'Build artifact token mismatch.');
}
}
}
@@ -2,17 +2,18 @@
namespace Appwrite\Platform\Modules\Functions\Http\Deployments\Events;
use Appwrite\Builds\OrchestratorToken;
use Appwrite\Event\Message\Build as BuildMessage;
use Appwrite\Event\Publisher\Build as BuildPublisher;
use Appwrite\Extend\Exception;
use Appwrite\Utopia\Response;
use OpenRuntimes\Orchestrator\Callback\Signature;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Validator\UID;
use Utopia\Http\Adapter\Swoole\Request;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\System\System;
class Create extends Action
{
@@ -52,7 +53,10 @@ class Create extends Action
BuildPublisher $publisherForBuilds
) {
$body = $request->getRawPayload();
OrchestratorToken::verifySignature($body, $request->getHeader('x-signature-256', ''));
$secret = System::getEnv('_APP_ORCHESTRATOR_CALLBACK_SECRET', System::getEnv('_APP_OPENSSL_KEY_V1', ''));
if (! Signature::verify($body, $request->getHeader('x-signature-256', ''), $secret)) {
throw new Exception(Exception::USER_UNAUTHORIZED, 'Invalid orchestrator event signature.');
}
$event = \json_decode($body, true);
if (!\is_array($event)) {
@@ -3,7 +3,6 @@
namespace Appwrite\Platform\Modules\Functions\Workers;
use Ahc\Jwt\JWT;
use Appwrite\Builds\OrchestratorToken;
use Appwrite\Event\Event;
use Appwrite\Event\Message\Func as FunctionMessage;
use Appwrite\Event\Message\Usage as UsageMessage;
@@ -16,6 +15,7 @@ use Appwrite\Extend\Exception as AppwriteException;
use Appwrite\Filter\BranchDomain as BranchDomainFilter;
use Appwrite\Usage\Context;
use Appwrite\Utopia\Response\Model\Deployment;
use Appwrite\Utopia\Response\Model\ResourceToken as ResourceTokenModel;
use Appwrite\Vcs\Comment;
use Exception;
use Executor\Exception\Timeout as ExecutorTimeout;
@@ -29,6 +29,7 @@ use OpenRuntimes\Orchestrator\DTO\JobRequest;
use OpenRuntimes\Orchestrator\Enum\CallbackEvent;
use OpenRuntimes\Orchestrator\Exception\TimeoutException as OrchestratorTimeout;
use Swoole\Coroutine as Co;
use Utopia\Auth\Proofs\Token;
use Utopia\Cache\Cache;
use Utopia\Config\Config;
use Utopia\Console;
@@ -39,6 +40,7 @@ use Utopia\Database\Exception\Conflict;
use Utopia\Database\Exception\Duplicate;
use Utopia\Database\Exception\Restricted;
use Utopia\Database\Exception\Structure;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Query;
use Utopia\Detector\Detection\Rendering\SSR;
use Utopia\Detector\Detection\Rendering\XStatic;
@@ -722,6 +724,7 @@ class Builds extends Action
if ($buildsBackend === 'orchestrator') {
$this->createOrchestratorBuild(
project: $project,
dbForProject: $dbForProject,
resource: $resource,
deployment: $deployment,
runtime: $runtime,
@@ -1326,6 +1329,7 @@ class Builds extends Action
protected function createOrchestratorBuild(
Document $project,
Database $dbForProject,
Document $resource,
Document $deployment,
array $runtime,
@@ -1344,8 +1348,9 @@ class Builds extends Action
$callbackBase = \rtrim(System::getEnv('_APP_ORCHESTRATOR_APPWRITE_CALLBACK_ENDPOINT', $base), '/');
$resourcePath = $resource->getCollection();
$sourceToken = OrchestratorToken::create($project->getId(), $resourceId, $deploymentId, 'source', $timeout + 300);
$buildToken = OrchestratorToken::create($project->getId(), $resourceId, $deploymentId, 'build', $timeout + 300);
$callbackSecret = System::getEnv('_APP_ORCHESTRATOR_CALLBACK_SECRET', System::getEnv('_APP_OPENSSL_KEY_V1', ''));
$sourceToken = $this->createDeploymentArtifactToken($dbForProject, $resource, $deployment, 'source', $timeout + 300);
$buildToken = $this->createDeploymentArtifactToken($dbForProject, $resource, $deployment, 'build', $timeout + 300);
$projectQuery = 'project=' . \rawurlencode($project->getId());
$appwriteProjectHeader = ['X-Appwrite-Project' => $project->getId()];
@@ -1443,7 +1448,7 @@ class Builds extends Action
CallbackEvent::Artifact,
CallbackEvent::Exit,
],
key: System::getEnv('_APP_ORCHESTRATOR_CALLBACK_SECRET', System::getEnv('_APP_OPENSSL_KEY_V1', '')),
key: $callbackSecret,
headers: $appwriteProjectHeader,
),
), $timeout);
@@ -1452,6 +1457,25 @@ class Builds extends Action
}
}
protected function createDeploymentArtifactToken(
Database $dbForProject,
Document $resource,
Document $deployment,
string $purpose,
int $duration
): string {
$token = $dbForProject->getAuthorization()->skip(fn () => $dbForProject->createDocument('resourceTokens', new Document([
'$id' => ID::unique(),
'secret' => (new Token(128))->generate(),
'resourceId' => "{$resource->getCollection()}:{$resource->getId()}:{$deployment->getId()}:{$purpose}",
'resourceInternalId' => "{$resource->getSequence()}:{$deployment->getSequence()}",
'resourceType' => TOKENS_RESOURCE_TYPE_DEPLOYMENT_ARTIFACTS,
'expire' => DateTime::formatTz(DateTime::addSeconds(new \DateTime(), $duration)),
])));
return (new ResourceTokenModel())->filter($token)->getAttribute('secret');
}
protected function applyOrchestratorEvent(
Realtime $queueForRealtime,
Context $usage,
@@ -2,7 +2,6 @@
namespace Appwrite\Platform\Modules\Sites\Http\Deployments\Artifacts\Build;
use Appwrite\Builds\OrchestratorToken;
use Appwrite\Event\Publisher\Build as BuildPublisher;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Compute\Http\Deployments\Artifacts\Build\ChunkedBuildArtifact;
@@ -43,6 +42,7 @@ class Update extends Action
->inject('request')
->inject('dbForProject')
->inject('project')
->inject('resourceToken')
->inject('deviceForBuilds')
->inject('publisherForBuilds')
->inject('cache')
@@ -58,13 +58,13 @@ class Update extends Action
Request $request,
Database $dbForProject,
Document $project,
Document $resourceToken,
Device $deviceForBuilds,
BuildPublisher $publisherForBuilds,
Cache $cache,
callable $locks
) {
$token = $token ?: $request->getQuery('token', '');
OrchestratorToken::verify($token, $project->getId(), $siteId, $deploymentId, 'build');
$this->verifyArtifactToken($resourceToken, RESOURCE_TYPE_SITES, $siteId, $deploymentId, 'build');
$site = $dbForProject->getAuthorization()->skip(fn () => $dbForProject->getDocument('sites', $siteId));
if ($site->isEmpty()) {
@@ -90,4 +90,20 @@ class Update extends Action
locks: $locks
);
}
private function verifyArtifactToken(Document $resourceToken, string $resourceType, string $resourceId, string $deploymentId, string $purpose): void
{
if ($resourceToken->isEmpty()) {
throw new Exception(Exception::USER_UNAUTHORIZED, 'Invalid build artifact token.');
}
if (
$resourceToken->getAttribute('resourceType') !== $resourceType ||
$resourceToken->getAttribute('resourceId') !== $resourceId ||
$resourceToken->getAttribute('deploymentId') !== $deploymentId ||
$resourceToken->getAttribute('purpose') !== $purpose
) {
throw new Exception(Exception::USER_UNAUTHORIZED, 'Build artifact token mismatch.');
}
}
}
@@ -2,7 +2,6 @@
namespace Appwrite\Platform\Modules\Sites\Http\Deployments\Artifacts\Source;
use Appwrite\Builds\OrchestratorToken;
use Appwrite\Extend\Exception;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
@@ -37,6 +36,7 @@ class Get extends Action
->inject('response')
->inject('dbForProject')
->inject('project')
->inject('resourceToken')
->inject('deviceForSites')
->callback($this->action(...));
}
@@ -48,9 +48,10 @@ class Get extends Action
Response $response,
Database $dbForProject,
Document $project,
Document $resourceToken,
Device $deviceForSites
) {
OrchestratorToken::verify($token, $project->getId(), $siteId, $deploymentId, 'source');
$this->verifyArtifactToken($resourceToken, RESOURCE_TYPE_SITES, $siteId, $deploymentId, 'source');
$site = $dbForProject->getAuthorization()->skip(fn () => $dbForProject->getDocument('sites', $siteId));
if ($site->isEmpty()) {
@@ -89,4 +90,20 @@ class Get extends Action
$response->send($deviceForSites->read($path));
}
private function verifyArtifactToken(Document $resourceToken, string $resourceType, string $resourceId, string $deploymentId, string $purpose): void
{
if ($resourceToken->isEmpty()) {
throw new Exception(Exception::USER_UNAUTHORIZED, 'Invalid build artifact token.');
}
if (
$resourceToken->getAttribute('resourceType') !== $resourceType ||
$resourceToken->getAttribute('resourceId') !== $resourceId ||
$resourceToken->getAttribute('deploymentId') !== $deploymentId ||
$resourceToken->getAttribute('purpose') !== $purpose
) {
throw new Exception(Exception::USER_UNAUTHORIZED, 'Build artifact token mismatch.');
}
}
}
@@ -2,17 +2,18 @@
namespace Appwrite\Platform\Modules\Sites\Http\Deployments\Events;
use Appwrite\Builds\OrchestratorToken;
use Appwrite\Event\Message\Build as BuildMessage;
use Appwrite\Event\Publisher\Build as BuildPublisher;
use Appwrite\Extend\Exception;
use Appwrite\Utopia\Response;
use OpenRuntimes\Orchestrator\Callback\Signature;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Validator\UID;
use Utopia\Http\Adapter\Swoole\Request;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\System\System;
class Create extends Action
{
@@ -52,7 +53,10 @@ class Create extends Action
BuildPublisher $publisherForBuilds
) {
$body = $request->getRawPayload();
OrchestratorToken::verifySignature($body, $request->getHeader('x-signature-256', ''));
$secret = System::getEnv('_APP_ORCHESTRATOR_CALLBACK_SECRET', System::getEnv('_APP_OPENSSL_KEY_V1', ''));
if (! Signature::verify($body, $request->getHeader('x-signature-256', ''), $secret)) {
throw new Exception(Exception::USER_UNAUTHORIZED, 'Invalid orchestrator event signature.');
}
$event = \json_decode($body, true);
if (!\is_array($event)) {