Merge pull request #9678 from appwrite/fix-missing-filter

Fix: FileTokens
This commit is contained in:
Matej Bačo 2025-05-06 16:08:23 +02:00 committed by GitHub
commit 06ff33cf7e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 506 additions and 94 deletions

View file

@ -156,6 +156,7 @@ jobs:
Sites,
Proxy,
Storage,
Tokens,
Teams,
Users,
Webhooks,

View file

@ -2461,8 +2461,19 @@ return [
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
]
'filters' => ['datetime'],
],
[
'$id' => ID::custom('accessedAt'),
'type' => Database::VAR_DATETIME,
'format' => '',
'size' => 0,
'signed' => false,
'required' => false,
'default' => null,
'array' => false,
'filters' => ['datetime'],
],
],
'indexes' => [
[
@ -2472,7 +2483,13 @@ return [
'lengths' => [],
'orders' => [Database::ORDER_ASC],
],
[
'$id' => '_key_accessedAt',
'type' => Database::INDEX_KEY,
'attributes' => ['accessedAt'],
'lengths' => [],
'orders' => [],
],
],
],
];

View file

@ -9483,34 +9483,45 @@
"description": "Token creation date in ISO 8601 format.",
"x-example": "2020-10-15T06:38:00.000+00:00"
},
"$permissions": {
"type": "array",
"description": "Token permissions. [Learn more about permissions](https:\/\/appwrite.io\/docs\/permissions).",
"items": {
"type": "string"
},
"x-example": [
"read(\"any\")"
]
},
"resourceId": {
"type": "string",
"description": "Resource ID.",
"x-example": "5e5ea5c168bb8:5e5ea5c168bb8"
},
"resourceInternalId": {
"type": "string",
"description": "File ID.",
"x-example": "1:1"
},
"resourceType": {
"type": "string",
"description": "Resource type.",
"x-example": "file"
"x-example": "files"
},
"expire": {
"type": "string",
"description": "Token expiration date in ISO 8601 format.",
"x-example": "2020-10-15T06:38:00.000+00:00"
},
"accessedAt": {
"type": "string",
"description": "Most recent access date in ISO 8601 format. This attribute is only updated again after 24 hours.",
"x-example": "2020-10-15T06:38:00.000+00:00"
}
},
"required": [
"$id",
"$createdAt",
"$permissions",
"resourceId",
"resourceInternalId",
"resourceType",
"expire"
"expire",
"accessedAt"
]
},
"team": {

View file

@ -38045,34 +38045,45 @@
"description": "Token creation date in ISO 8601 format.",
"x-example": "2020-10-15T06:38:00.000+00:00"
},
"$permissions": {
"type": "array",
"description": "Token permissions. [Learn more about permissions](https:\/\/appwrite.io\/docs\/permissions).",
"items": {
"type": "string"
},
"x-example": [
"read(\"any\")"
]
},
"resourceId": {
"type": "string",
"description": "Resource ID.",
"x-example": "5e5ea5c168bb8:5e5ea5c168bb8"
},
"resourceInternalId": {
"type": "string",
"description": "File ID.",
"x-example": "1:1"
},
"resourceType": {
"type": "string",
"description": "Resource type.",
"x-example": "file"
"x-example": "files"
},
"expire": {
"type": "string",
"description": "Token expiration date in ISO 8601 format.",
"x-example": "2020-10-15T06:38:00.000+00:00"
},
"accessedAt": {
"type": "string",
"description": "Most recent access date in ISO 8601 format. This attribute is only updated again after 24 hours.",
"x-example": "2020-10-15T06:38:00.000+00:00"
}
},
"required": [
"$id",
"$createdAt",
"$permissions",
"resourceId",
"resourceInternalId",
"resourceType",
"expire"
"expire",
"accessedAt"
]
},
"team": {

View file

@ -28004,34 +28004,45 @@
"description": "Token creation date in ISO 8601 format.",
"x-example": "2020-10-15T06:38:00.000+00:00"
},
"$permissions": {
"type": "array",
"description": "Token permissions. [Learn more about permissions](https:\/\/appwrite.io\/docs\/permissions).",
"items": {
"type": "string"
},
"x-example": [
"read(\"any\")"
]
},
"resourceId": {
"type": "string",
"description": "Resource ID.",
"x-example": "5e5ea5c168bb8:5e5ea5c168bb8"
},
"resourceInternalId": {
"type": "string",
"description": "File ID.",
"x-example": "1:1"
},
"resourceType": {
"type": "string",
"description": "Resource type.",
"x-example": "file"
"x-example": "files"
},
"expire": {
"type": "string",
"description": "Token expiration date in ISO 8601 format.",
"x-example": "2020-10-15T06:38:00.000+00:00"
},
"accessedAt": {
"type": "string",
"description": "Most recent access date in ISO 8601 format. This attribute is only updated again after 24 hours.",
"x-example": "2020-10-15T06:38:00.000+00:00"
}
},
"required": [
"$id",
"$createdAt",
"$permissions",
"resourceId",
"resourceInternalId",
"resourceType",
"expire"
"expire",
"accessedAt"
]
},
"team": {

View file

@ -9586,34 +9586,45 @@
"description": "Token creation date in ISO 8601 format.",
"x-example": "2020-10-15T06:38:00.000+00:00"
},
"$permissions": {
"type": "array",
"description": "Token permissions. [Learn more about permissions](https:\/\/appwrite.io\/docs\/permissions).",
"items": {
"type": "string"
},
"x-example": [
"read(\"any\")"
]
},
"resourceId": {
"type": "string",
"description": "Resource ID.",
"x-example": "5e5ea5c168bb8:5e5ea5c168bb8"
},
"resourceInternalId": {
"type": "string",
"description": "File ID.",
"x-example": "1:1"
},
"resourceType": {
"type": "string",
"description": "Resource type.",
"x-example": "file"
"x-example": "files"
},
"expire": {
"type": "string",
"description": "Token expiration date in ISO 8601 format.",
"x-example": "2020-10-15T06:38:00.000+00:00"
},
"accessedAt": {
"type": "string",
"description": "Most recent access date in ISO 8601 format. This attribute is only updated again after 24 hours.",
"x-example": "2020-10-15T06:38:00.000+00:00"
}
},
"required": [
"$id",
"$createdAt",
"$permissions",
"resourceId",
"resourceInternalId",
"resourceType",
"expire"
"expire",
"accessedAt"
]
},
"team": {

View file

@ -38290,34 +38290,45 @@
"description": "Token creation date in ISO 8601 format.",
"x-example": "2020-10-15T06:38:00.000+00:00"
},
"$permissions": {
"type": "array",
"description": "Token permissions. [Learn more about permissions](https:\/\/appwrite.io\/docs\/permissions).",
"items": {
"type": "string"
},
"x-example": [
"read(\"any\")"
]
},
"resourceId": {
"type": "string",
"description": "Resource ID.",
"x-example": "5e5ea5c168bb8:5e5ea5c168bb8"
},
"resourceInternalId": {
"type": "string",
"description": "File ID.",
"x-example": "1:1"
},
"resourceType": {
"type": "string",
"description": "Resource type.",
"x-example": "file"
"x-example": "files"
},
"expire": {
"type": "string",
"description": "Token expiration date in ISO 8601 format.",
"x-example": "2020-10-15T06:38:00.000+00:00"
},
"accessedAt": {
"type": "string",
"description": "Most recent access date in ISO 8601 format. This attribute is only updated again after 24 hours.",
"x-example": "2020-10-15T06:38:00.000+00:00"
}
},
"required": [
"$id",
"$createdAt",
"$permissions",
"resourceId",
"resourceInternalId",
"resourceType",
"expire"
"expire",
"accessedAt"
]
},
"team": {

View file

@ -28318,34 +28318,45 @@
"description": "Token creation date in ISO 8601 format.",
"x-example": "2020-10-15T06:38:00.000+00:00"
},
"$permissions": {
"type": "array",
"description": "Token permissions. [Learn more about permissions](https:\/\/appwrite.io\/docs\/permissions).",
"items": {
"type": "string"
},
"x-example": [
"read(\"any\")"
]
},
"resourceId": {
"type": "string",
"description": "Resource ID.",
"x-example": "5e5ea5c168bb8:5e5ea5c168bb8"
},
"resourceInternalId": {
"type": "string",
"description": "File ID.",
"x-example": "1:1"
},
"resourceType": {
"type": "string",
"description": "Resource type.",
"x-example": "file"
"x-example": "files"
},
"expire": {
"type": "string",
"description": "Token expiration date in ISO 8601 format.",
"x-example": "2020-10-15T06:38:00.000+00:00"
},
"accessedAt": {
"type": "string",
"description": "Most recent access date in ISO 8601 format. This attribute is only updated again after 24 hours.",
"x-example": "2020-10-15T06:38:00.000+00:00"
}
},
"required": [
"$id",
"$createdAt",
"$permissions",
"resourceId",
"resourceInternalId",
"resourceType",
"expire"
"expire",
"accessedAt"
]
},
"team": {

View file

@ -1133,8 +1133,9 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/download')
->inject('response')
->inject('dbForProject')
->inject('mode')
->inject('resourceToken')
->inject('deviceForFiles')
->action(function (string $bucketId, string $fileId, Request $request, Response $response, Database $dbForProject, string $mode, Device $deviceForFiles) {
->action(function (string $bucketId, string $fileId, Request $request, Response $response, Database $dbForProject, string $mode, Document $resourceToken, Device $deviceForFiles) {
$bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
@ -1145,10 +1146,11 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/download')
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
}
$isToken = !$resourceToken->isEmpty() && $resourceToken->getAttribute('bucketInternalId') === $bucket->getInternalId();
$fileSecurity = $bucket->getAttribute('fileSecurity', false);
$validator = new Authorization(Database::PERMISSION_READ);
$valid = $validator->isValid($bucket->getRead());
if (!$fileSecurity && !$valid) {
if (!$fileSecurity && !$valid && !$isToken) {
throw new Exception(Exception::USER_UNAUTHORIZED);
}
@ -1158,6 +1160,10 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/download')
$file = Authorization::skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId));
}
if (!$resourceToken->isEmpty() && $resourceToken->getAttribute('fileInternalId') !== $file->getInternalId()) {
throw new Exception(Exception::USER_UNAUTHORIZED);
}
if ($file->isEmpty()) {
throw new Exception(Exception::STORAGE_FILE_NOT_FOUND);
}
@ -1282,8 +1288,9 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/view')
->inject('request')
->inject('dbForProject')
->inject('mode')
->inject('resourceToken')
->inject('deviceForFiles')
->action(function (string $bucketId, string $fileId, Response $response, Request $request, Database $dbForProject, string $mode, Device $deviceForFiles) {
->action(function (string $bucketId, string $fileId, Response $response, Request $request, Database $dbForProject, string $mode, Document $resourceToken, Device $deviceForFiles) {
$bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
@ -1293,10 +1300,11 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/view')
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
}
$isToken = !$resourceToken->isEmpty() && $resourceToken->getAttribute('bucketInternalId') === $bucket->getInternalId();
$fileSecurity = $bucket->getAttribute('fileSecurity', false);
$validator = new Authorization(Database::PERMISSION_READ);
$valid = $validator->isValid($bucket->getRead());
if (!$fileSecurity && !$valid) {
if (!$fileSecurity && !$valid && !$isToken) {
throw new Exception(Exception::USER_UNAUTHORIZED);
}
@ -1306,6 +1314,10 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/view')
$file = Authorization::skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId));
}
if (!$resourceToken->isEmpty() && $resourceToken->getAttribute('fileInternalId') !== $file->getInternalId()) {
throw new Exception(Exception::USER_UNAUTHORIZED);
}
if ($file->isEmpty()) {
throw new Exception(Exception::STORAGE_FILE_NOT_FOUND);
}

View file

@ -29,6 +29,7 @@ const APP_LIMIT_LIST_DEFAULT = 25; // Default maximum number of items to return
const APP_KEY_ACCESS = 24 * 60 * 60; // 24 hours
const APP_USER_ACCESS = 24 * 60 * 60; // 24 hours
const APP_PROJECT_ACCESS = 24 * 60 * 60; // 24 hours
const APP_RESOURCE_TOKEN_ACCESS = 24 * 60 * 60; // 24 hours
const APP_FILE_ACCESS = 24 * 60 * 60; // 24 hours
const APP_CACHE_UPDATE = 24 * 60 * 60; // 24 hours
const APP_CACHE_BUSTER = 4318;
@ -257,3 +258,10 @@ const RESOURCE_TYPE_PROVIDERS = 'providers';
const RESOURCE_TYPE_TOPICS = 'topics';
const RESOURCE_TYPE_SUBSCRIBERS = 'subscribers';
const RESOURCE_TYPE_MESSAGES = 'messages';
// Resource types for Tokens
const TOKENS_RESOURCE_TYPE_FILES = 'files';
const TOKENS_RESOURCE_TYPE_SITES = 'sites';
const TOKENS_RESOURCE_TYPE_FUNCTIONS = 'functions';
const TOKENS_RESOURCE_TYPE_DATABASES = 'databases';

View file

@ -929,7 +929,7 @@ App::setResource('resourceToken', function ($project, $dbForProject, $request) {
return new Document([]);
}
if ($token->getAttribute('resourceType') === 'file') {
if ($token->getAttribute('resourceType') === TOKENS_RESOURCE_TYPE_FILES) {
$internalIds = explode(':', $token->getAttribute('resourceInternalId'));
$ids = explode(':', $token->getAttribute('resourceId'));
@ -937,6 +937,12 @@ App::setResource('resourceToken', function ($project, $dbForProject, $request) {
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(), $token));
}
return new Document([
'bucketId' => $ids[0],
'fileId' => $ids[1],

View file

@ -8,7 +8,7 @@ use Appwrite\Platform\Modules\Functions;
use Appwrite\Platform\Modules\Projects;
use Appwrite\Platform\Modules\Proxy;
use Appwrite\Platform\Modules\Sites;
use Appwrite\Platform\Modules\Storage;
use Appwrite\Platform\Modules\Tokens;
use Utopia\Platform\Platform;
class Appwrite extends Platform
@ -21,6 +21,6 @@ class Appwrite extends Platform
$this->addModule(new Sites\Module());
$this->addModule(new Console\Module());
$this->addModule(new Proxy\Module());
$this->addModule(new Storage\Module());
$this->addModule(new Tokens\Module());
}
}

View file

@ -1,6 +1,6 @@
<?php
namespace Appwrite\Platform\Modules\Storage\Http\Tokens\Buckets\Files;
namespace Appwrite\Platform\Modules\Tokens\Http\Tokens\Buckets\Files;
use Appwrite\Auth\Auth;
use Appwrite\Extend\Exception;

View file

@ -1,6 +1,6 @@
<?php
namespace Appwrite\Platform\Modules\Storage\Http\Tokens\Buckets\Files;
namespace Appwrite\Platform\Modules\Tokens\Http\Tokens\Buckets\Files;
use Appwrite\Auth\Auth;
use Appwrite\Event\Event;
@ -98,7 +98,7 @@ class Create extends Action
'secret' => Auth::tokenGenerator(128),
'resourceId' => $bucketId . ':' . $fileId,
'resourceInternalId' => $bucket->getInternalId() . ':' . $file->getInternalId(),
'resourceType' => 'files',
'resourceType' => TOKENS_RESOURCE_TYPE_FILES,
'expire' => $expire,
'$permissions' => $permissions
]));

View file

@ -1,6 +1,6 @@
<?php
namespace Appwrite\Platform\Modules\Storage\Http\Tokens\Buckets\Files;
namespace Appwrite\Platform\Modules\Tokens\Http\Tokens\Buckets\Files;
use Appwrite\Extend\Exception as ExtendException;
use Appwrite\SDK\AuthType;
@ -63,7 +63,7 @@ class XList extends Action
['bucket' => $bucket, 'file' => $file] = $this->getFileAndBucket($dbForProject, $bucketId, $fileId);
$queries = Query::parseQueries($queries);
$queries[] = Query::equal('resourceType', ["files"]);
$queries[] = Query::equal('resourceType', [TOKENS_RESOURCE_TYPE_FILES]);
$queries[] = Query::equal('resourceInternalId', [$bucket->getInternalId() . ':' . $file->getInternalId()]);
// Get cursor document if there was a cursor query
$cursor = \array_filter($queries, function ($query) {

View file

@ -1,6 +1,6 @@
<?php
namespace Appwrite\Platform\Modules\Storage\Http\Tokens;
namespace Appwrite\Platform\Modules\Tokens\Http\Tokens;
use Appwrite\Event\Event;
use Appwrite\Extend\Exception;

View file

@ -1,6 +1,6 @@
<?php
namespace Appwrite\Platform\Modules\Storage\Http\Tokens;
namespace Appwrite\Platform\Modules\Tokens\Http\Tokens;
use Appwrite\Extend\Exception;
use Appwrite\SDK\AuthType;

View file

@ -1,6 +1,6 @@
<?php
namespace Appwrite\Platform\Modules\Storage\Http\Tokens\JWT;
namespace Appwrite\Platform\Modules\Tokens\Http\Tokens\JWT;
use Ahc\Jwt\JWT;
use Appwrite\Extend\Exception;

View file

@ -1,6 +1,6 @@
<?php
namespace Appwrite\Platform\Modules\Storage\Http\Tokens;
namespace Appwrite\Platform\Modules\Tokens\Http\Tokens;
use Appwrite\Auth\Auth;
use Appwrite\Event\Event;

View file

@ -1,8 +1,8 @@
<?php
namespace Appwrite\Platform\Modules\Storage;
namespace Appwrite\Platform\Modules\Tokens;
use Appwrite\Platform\Modules\Storage\Services\Http;
use Appwrite\Platform\Modules\Tokens\Services\Http;
use Utopia\Platform;
class Module extends Platform\Module

View file

@ -1,13 +1,13 @@
<?php
namespace Appwrite\Platform\Modules\Storage\Services;
namespace Appwrite\Platform\Modules\Tokens\Services;
use Appwrite\Platform\Modules\Storage\Http\Tokens\Buckets\Files\Create as CreateFileToken;
use Appwrite\Platform\Modules\Storage\Http\Tokens\Buckets\Files\XList as ListFileTokens;
use Appwrite\Platform\Modules\Storage\Http\Tokens\Delete as DeleteToken;
use Appwrite\Platform\Modules\Storage\Http\Tokens\Get as GetToken;
use Appwrite\Platform\Modules\Storage\Http\Tokens\JWT\Get as GetTokenJWT;
use Appwrite\Platform\Modules\Storage\Http\Tokens\Update as UpdateToken;
use Appwrite\Platform\Modules\Tokens\Http\Tokens\Buckets\Files\Create as CreateFileToken;
use Appwrite\Platform\Modules\Tokens\Http\Tokens\Buckets\Files\XList as ListFileTokens;
use Appwrite\Platform\Modules\Tokens\Http\Tokens\Delete as DeleteToken;
use Appwrite\Platform\Modules\Tokens\Http\Tokens\Get as GetToken;
use Appwrite\Platform\Modules\Tokens\Http\Tokens\JWT\Get as GetTokenJWT;
use Appwrite\Platform\Modules\Tokens\Http\Tokens\Update as UpdateToken;
use Utopia\Platform\Service;
class Http extends Service

View file

@ -22,23 +22,24 @@ class ResourceToken extends Model
'default' => '',
'example' => self::TYPE_DATETIME_EXAMPLE,
])
->addRule('$permissions', [
'type' => self::TYPE_STRING,
'description' => 'Token permissions. [Learn more about permissions](https://appwrite.io/docs/permissions).',
'default' => '',
'example' => ['read("any")'],
'array' => true,
])
->addRule('resourceId', [
'type' => self::TYPE_STRING,
'description' => 'Resource ID.',
'default' => '',
'example' => '5e5ea5c168bb8:5e5ea5c168bb8',
])
->addRule('resourceInternalId', [
'type' => self::TYPE_STRING,
'description' => 'File ID.',
'default' => '',
'example' => '1:1',
])
->addRule('resourceType', [
'type' => self::TYPE_STRING,
'description' => 'Resource type.',
'default' => '',
'example' => 'file',
'example' => TOKENS_RESOURCE_TYPE_FILES,
])
->addRule('expire', [
'type' => self::TYPE_DATETIME,
@ -46,6 +47,12 @@ class ResourceToken extends Model
'default' => '',
'example' => self::TYPE_DATETIME_EXAMPLE,
])
->addRule('accessedAt', [
'type' => self::TYPE_DATETIME,
'description' => 'Most recent access date in ISO 8601 format. This attribute is only updated again after ' . APP_RESOURCE_TOKEN_ACCESS / 60 / 60 . ' hours.',
'default' => '',
'example' => self::TYPE_DATETIME_EXAMPLE
])
;
}

View file

@ -2,6 +2,287 @@
namespace Tests\E2E\Services\Tokens;
use CURLFile;
use Tests\E2E\Client;
use Utopia\Database\Helpers\ID;
trait TokensBase
{
public function testCreateBucketAndFile(): array
{
$bucket = $this->client->call(
Client::METHOD_POST,
'/storage/buckets',
[
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
],
[
'name' => 'Test Bucket',
'bucketId' => ID::unique(),
'allowedFileExtensions' => ['jpg', 'png', 'jfif'],
]
);
$this->assertEquals(201, $bucket['headers']['status-code']);
$this->assertNotEmpty($bucket['body']['$id']);
$bucketId = $bucket['body']['$id'];
$file = $this->client->call(
Client::METHOD_POST,
'/storage/buckets/' . $bucketId . '/files',
[
'content-type' => 'multipart/form-data',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
],
[
'fileId' => ID::unique(),
'file' => new CURLFile(realpath(__DIR__ . '/../../../resources/logo.png'), 'image/png', 'logo.png'),
]
);
$this->assertEquals(201, $file['headers']['status-code']);
$this->assertNotEmpty($file['body']['$id']);
$fileId = $file['body']['$id'];
$token = $this->client->call(
Client::METHOD_POST,
'/tokens/buckets/' . $bucketId . '/files/' . $fileId,
[
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
]
);
$this->assertEquals(201, $token['headers']['status-code']);
$this->assertEquals($bucketId . ':' . $fileId, $token['body']['resourceId']);
$this->assertEquals(TOKENS_RESOURCE_TYPE_FILES, $token['body']['resourceType']);
return [
'fileId' => $fileId,
'bucketId' => $bucketId,
'tokenId' => $token['body']['$id'],
'guestHeaders' => [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
],
];
}
/**
* @depends testCreateBucketAndFile
*/
public function testFailuresWithoutToken(array $data): array
{
$fileId = $data['fileId'];
$bucketId = $data['bucketId'];
$guestHeaders = $data['guestHeaders'];
// File preview. Should fail as an anonymous user with no form of any access to the file.
$failedPreview = $this->client->call(
Client::METHOD_GET,
'/storage/buckets/' . $bucketId . '/files/' . $fileId . '/preview',
$guestHeaders
);
$this->assertEquals(401, $failedPreview['body']['code']);
$this->assertEquals(401, $failedPreview['headers']['status-code']);
$this->assertEquals('user_unauthorized', $failedPreview['body']['type']);
$this->assertEquals('The current user is not authorized to perform the requested action.', $failedPreview['body']['message']);
// Extended file preview. Should fail as an anonymous user with no form of any access to the file.
$failedCustomPreview = $this->client->call(
Client::METHOD_GET,
'/storage/buckets/' . $bucketId . '/files/' . $fileId . '/preview',
$guestHeaders,
[
'width' => 300,
'height' => 100,
'borderRadius' => '50',
'opacity' => '0.5',
'output' => 'png',
'rotation' => '45'
]
);
$this->assertEquals(401, $failedCustomPreview['body']['code']);
$this->assertEquals(401, $failedCustomPreview['headers']['status-code']);
$this->assertEquals('user_unauthorized', $failedCustomPreview['body']['type']);
$this->assertEquals('The current user is not authorized to perform the requested action.', $failedCustomPreview['body']['message']);
// File view. Should fail as an anonymous user with no form of any access to the file.
$failedView = $this->client->call(
Client::METHOD_GET,
'/storage/buckets/' . $bucketId . '/files/' . $fileId . '/view',
$guestHeaders
);
$this->assertEquals(401, $failedView['body']['code']);
$this->assertEquals(401, $failedView['headers']['status-code']);
$this->assertEquals('user_unauthorized', $failedView['body']['type']);
$this->assertEquals('The current user is not authorized to perform the requested action.', $failedView['body']['message']);
// File download. Should fail as an anonymous user with no form of any access to the file.
$failedDownload = $this->client->call(
Client::METHOD_GET,
'/storage/buckets/' . $bucketId . '/files/' . $fileId . '/download',
$guestHeaders
);
$this->assertEquals(401, $failedDownload['body']['code']);
$this->assertEquals(401, $failedDownload['headers']['status-code']);
$this->assertEquals('user_unauthorized', $failedDownload['body']['type']);
$this->assertEquals('The current user is not authorized to perform the requested action.', $failedDownload['body']['message']);
return $data;
}
/**
* @depends testCreateBucketAndFile
*/
public function testPreviewFileWithToken(array $data): array
{
$fileId = $data['fileId'];
$tokenId = $data['tokenId'];
$bucketId = $data['bucketId'];
$guestHeaders = $data['guestHeaders'];
$adminHeaders = array_merge($guestHeaders, ['x-appwrite-key' => $this->getProject()['apiKey']]);
// Generate JWT as an admin user.
$tokenJWT = $this->client->call(
Client::METHOD_GET,
'/tokens/' . $tokenId . '/jwt/',
$adminHeaders
);
$this->assertEquals(200, $tokenJWT['headers']['status-code']);
$this->assertArrayHasKey('jwt', $tokenJWT['body']);
$tokenJWT = $tokenJWT['body']['jwt'];
// Generate a preview
$filePreview = $this->client->call(
Client::METHOD_GET,
'/storage/buckets/' . $bucketId . '/files/' . $fileId . '/preview',
$guestHeaders,
[
'token' => $tokenJWT
]
);
$this->assertEquals(200, $filePreview['headers']['status-code']);
$this->assertEquals('image/png', $filePreview['headers']['content-type']);
$this->assertNotEmpty($filePreview['body']);
$image = new \Imagick();
$image->readImageBlob($filePreview['body']);
$original = new \Imagick(__DIR__ . '/../../../resources/logo.png');
$this->assertEquals($image->getImageWidth(), $original->getImageWidth());
$this->assertEquals($image->getImageHeight(), $original->getImageHeight());
$this->assertEquals('PNG', $image->getImageFormat());
$data['jwtToken'] = $tokenJWT;
return $data;
}
/**
* @depends testPreviewFileWithToken
*/
public function testCustomPreviewFileWithToken(array $data): array
{
$fileId = $data['fileId'];
$bucketId = $data['bucketId'];
$jwtToken = $data['jwtToken'];
$guestHeaders = $data['guestHeaders'];
// Generate an extended preview
$customFilePreview = $this->client->call(
Client::METHOD_GET,
'/storage/buckets/' . $bucketId . '/files/' . $fileId . '/preview/',
$guestHeaders,
[
'width' => 300,
'height' => 100,
'borderRadius' => '50',
'opacity' => '0.5',
'output' => 'png',
'rotation' => '45',
'token' => $jwtToken
]
);
$this->assertEquals(200, $customFilePreview['headers']['status-code']);
$this->assertEquals('image/png', $customFilePreview['headers']['content-type']);
$this->assertNotEmpty($customFilePreview['body']);
$image = new \Imagick();
$image->readImageBlob($customFilePreview['body']);
$original = new \Imagick(__DIR__ . '/../../../resources/logo-after.png');
$this->assertEquals($image->getImageWidth(), $original->getImageWidth());
$this->assertEquals($image->getImageHeight(), $original->getImageHeight());
$this->assertEquals('PNG', $image->getImageFormat());
return $data;
}
/**
* @depends testPreviewFileWithToken
*/
public function testViewFileWithToken(array $data): void
{
$fileId = $data['fileId'];
$bucketId = $data['bucketId'];
$jwtToken = $data['jwtToken'];
$guestHeaders = $data['guestHeaders'];
$fileView = $this->client->call(
Client::METHOD_GET,
'/storage/buckets/' . $bucketId . '/files/' . $fileId . '/view',
$guestHeaders,
[
'token' => $jwtToken
]
);
$this->assertEquals(200, $fileView['headers']['status-code']);
$image = new \Imagick();
$image->readImageBlob($fileView['body']);
$original = new \Imagick(__DIR__ . '/../../../resources/logo.png');
$this->assertEquals($image->getImageWidth(), $original->getImageWidth());
$this->assertEquals($image->getImageHeight(), $original->getImageHeight());
$this->assertEquals('PNG', $image->getImageFormat());
}
/**
* @depends testPreviewFileWithToken
*/
public function testDownloadFileWithToken(array $data): void
{
$fileId = $data['fileId'];
$bucketId = $data['bucketId'];
$jwtToken = $data['jwtToken'];
$guestHeaders = $data['guestHeaders'];
$fileFailedDownload = $this->client->call(
Client::METHOD_GET,
'/storage/buckets/' . $bucketId . '/files/' . $fileId . '/download',
$guestHeaders,
[
'token' => $jwtToken
]
);
$this->assertEquals(200, $fileFailedDownload['headers']['status-code']);
$image = new \Imagick();
$image->readImageBlob($fileFailedDownload['body']);
$original = new \Imagick(__DIR__ . '/../../../resources/logo.png');
$this->assertEquals($image->getImageWidth(), $original->getImageWidth());
$this->assertEquals($image->getImageHeight(), $original->getImageHeight());
$this->assertEquals('PNG', $image->getImageFormat());
}
}

View file

@ -11,6 +11,7 @@ use Utopia\Database\DateTime;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Permission;
use Utopia\Database\Helpers\Role;
use Utopia\Database\Validator\Datetime as DatetimeValidator;
class TokensCustomServerTest extends Scope
{
@ -60,19 +61,19 @@ class TokensCustomServerTest extends Scope
$fileId = $file['body']['$id'];
$res = $this->client->call(Client::METHOD_POST, '/tokens/buckets/' . $bucketId . '/files/' . $fileId, array_merge([
$token = $this->client->call(Client::METHOD_POST, '/tokens/buckets/' . $bucketId . '/files/' . $fileId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id']
], $this->getHeaders()));
$this->assertEquals(201, $res['headers']['status-code']);
$this->assertEquals('files', $res['body']['resourceType']);
$this->assertEquals(201, $token['headers']['status-code']);
$this->assertEquals('files', $token['body']['resourceType']);
$data = [];
$data['fileId'] = $fileId;
$data['bucketId'] = $bucketId;
$data['tokenId'] = $res['body']['$id'];
return $data;
return [
'fileId' => $fileId,
'bucketId' => $bucketId,
'tokenId' => $token['body']['$id'],
];
}
/**
@ -82,15 +83,28 @@ class TokensCustomServerTest extends Scope
{
$tokenId = $data['tokenId'];
$expiry = DateTime::now();
$res = $this->client->call(Client::METHOD_PATCH, '/tokens/' . $tokenId, array_merge([
// Finite expiry
$expiry = DateTime::addSeconds(new \DateTime(), 3600);
$token = $this->client->call(Client::METHOD_PATCH, '/tokens/' . $tokenId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'expire' => $expiry,
]);
$this->assertEquals($expiry, $res['body']['expire']);
$dateValidator = new DatetimeValidator();
$this->assertTrue($dateValidator->isValid($token['body']['expire']));
// Infinite expiry
$token = $this->client->call(Client::METHOD_PATCH, '/tokens/' . $tokenId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'expire' => null,
]);
$this->assertEmpty($token['body']['expire']);
return $data;
}