From 14e52058c73328a4350fb266e872e627f47905a3 Mon Sep 17 00:00:00 2001 From: Darshan Date: Tue, 8 Jul 2025 13:46:53 +0530 Subject: [PATCH 1/5] fix: file tokens not working on file-security. --- app/controllers/api/storage.php | 11 ++++++++--- app/controllers/shared/api.php | 1 - .../Tokens/Http/Tokens/Buckets/Files/Action.php | 1 + 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/app/controllers/api/storage.php b/app/controllers/api/storage.php index 98a5b105a3..c6e242296b 100644 --- a/app/controllers/api/storage.php +++ b/app/controllers/api/storage.php @@ -967,6 +967,7 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/preview') throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Imagick extension is missing'); } + /* @type Document $bucket */ $bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); $isAPIKey = Auth::isAppUser(Authorization::getRoles()); @@ -987,6 +988,7 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/preview') if ($fileSecurity && !$valid && !$isToken) { $file = $dbForProject->getDocument('bucket_' . $bucket->getSequence(), $fileId); } else { + /* @type Document $file */ $file = Authorization::skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getSequence(), $fileId)); } @@ -1157,7 +1159,7 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/download') ->inject('resourceToken') ->inject('deviceForFiles') ->action(function (string $bucketId, string $fileId, ?string $token, Request $request, Response $response, Database $dbForProject, string $mode, Document $resourceToken, Device $deviceForFiles) { - + /* @type Document $bucket */ $bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); $isAPIKey = Auth::isAppUser(Authorization::getRoles()); @@ -1175,9 +1177,10 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/download') throw new Exception(Exception::USER_UNAUTHORIZED); } - if ($fileSecurity && !$valid) { + if ($fileSecurity && !$valid && !$isToken) { $file = $dbForProject->getDocument('bucket_' . $bucket->getSequence(), $fileId); } else { + /* @type Document $file */ $file = Authorization::skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getSequence(), $fileId)); } @@ -1317,6 +1320,7 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/view') ->inject('resourceToken') ->inject('deviceForFiles') ->action(function (string $bucketId, string $fileId, ?string $token, Response $response, Request $request, Database $dbForProject, string $mode, Document $resourceToken, Device $deviceForFiles) { + /* @type Document $bucket */ $bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); $isAPIKey = Auth::isAppUser(Authorization::getRoles()); @@ -1334,9 +1338,10 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/view') throw new Exception(Exception::USER_UNAUTHORIZED); } - if ($fileSecurity && !$valid) { + if ($fileSecurity && !$valid && !$isToken) { $file = $dbForProject->getDocument('bucket_' . $bucket->getSequence(), $fileId); } else { + /* @type Document $file */ $file = Authorization::skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getSequence(), $fileId)); } diff --git a/app/controllers/shared/api.php b/app/controllers/shared/api.php index 76fe177b0b..86fb1e5822 100644 --- a/app/controllers/shared/api.php +++ b/app/controllers/shared/api.php @@ -576,7 +576,6 @@ App::init() $bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); $isToken = !$resourceToken->isEmpty() && $resourceToken->getAttribute('bucketInternalId') === $bucket->getSequence(); - $isAPIKey = Auth::isAppUser(Authorization::getRoles()); $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAppUser && !$isPrivilegedUser)) { diff --git a/src/Appwrite/Platform/Modules/Tokens/Http/Tokens/Buckets/Files/Action.php b/src/Appwrite/Platform/Modules/Tokens/Http/Tokens/Buckets/Files/Action.php index bcefaf353f..5708f1b83b 100644 --- a/src/Appwrite/Platform/Modules/Tokens/Http/Tokens/Buckets/Files/Action.php +++ b/src/Appwrite/Platform/Modules/Tokens/Http/Tokens/Buckets/Files/Action.php @@ -37,6 +37,7 @@ class Action extends UtopiaAction if ($file->isEmpty()) { throw new Exception(Exception::STORAGE_FILE_NOT_FOUND); } + return [ 'bucket' => $bucket, 'file' => $file, From 97d5f0f0302310595f9b246ed8dcb3dfb0ab9d7c Mon Sep 17 00:00:00 2001 From: Darshan Date: Tue, 8 Jul 2025 15:34:52 +0530 Subject: [PATCH 2/5] add: e2e. --- tests/e2e/Services/Tokens/TokensBase.php | 71 ++++++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/tests/e2e/Services/Tokens/TokensBase.php b/tests/e2e/Services/Tokens/TokensBase.php index af93f5fc73..b6da8f4c41 100644 --- a/tests/e2e/Services/Tokens/TokensBase.php +++ b/tests/e2e/Services/Tokens/TokensBase.php @@ -275,4 +275,75 @@ trait TokensBase $this->assertEquals($image->getImageHeight(), $original->getImageHeight()); $this->assertEquals('PNG', $image->getImageFormat()); } + + public function testDownloadFileWithFileSecurity(): void + { + $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(), + 'fileSecurity' => true, + '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'), + ] + ); + + $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'], + ] + ); + + $jwtToken = $token['body']['secret']; + + $fileDownloaded = $this->client->call( + Client::METHOD_GET, + '/storage/buckets/' . $bucketId . '/files/' . $fileId . '/download', + ['content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], + [ 'token' => $jwtToken ] + ); + + $this->assertEquals(200, $fileDownloaded['headers']['status-code']); + + $image = new \Imagick(); + $image->readImageBlob($fileDownloaded['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()); + } } From 7ea896301fdb954f43e573c03db1045488dc7b12 Mon Sep 17 00:00:00 2001 From: Darshan Date: Tue, 8 Jul 2025 15:42:38 +0530 Subject: [PATCH 3/5] update: test to include preview, view and download with file-security. --- tests/e2e/Services/Tokens/TokensBase.php | 46 +++++++++++++++--------- 1 file changed, 30 insertions(+), 16 deletions(-) diff --git a/tests/e2e/Services/Tokens/TokensBase.php b/tests/e2e/Services/Tokens/TokensBase.php index b6da8f4c41..7690653453 100644 --- a/tests/e2e/Services/Tokens/TokensBase.php +++ b/tests/e2e/Services/Tokens/TokensBase.php @@ -5,6 +5,8 @@ namespace Tests\E2E\Services\Tokens; use CURLFile; use Tests\E2E\Client; use Utopia\Database\Helpers\ID; +use Utopia\Database\Helpers\Permission; +use Utopia\Database\Helpers\Role; trait TokensBase { @@ -276,7 +278,7 @@ trait TokensBase $this->assertEquals('PNG', $image->getImageFormat()); } - public function testDownloadFileWithFileSecurity(): void + public function testFileAccessWithFileSecurity(): void { $bucket = $this->client->call( Client::METHOD_POST, @@ -309,6 +311,7 @@ trait TokensBase ], [ 'fileId' => ID::unique(), + 'permissions' => [ Permission::read(Role::label('devrel')) ], 'file' => new CURLFile(realpath(__DIR__ . '/../../../resources/logo.png'), 'image/png', 'logo.png'), ] ); @@ -327,23 +330,34 @@ trait TokensBase $jwtToken = $token['body']['secret']; - $fileDownloaded = $this->client->call( - Client::METHOD_GET, - '/storage/buckets/' . $bucketId . '/files/' . $fileId . '/download', - ['content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], - [ 'token' => $jwtToken ] - ); + $guestHeaders = ['content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ]; - $this->assertEquals(200, $fileDownloaded['headers']['status-code']); + $endpoints = ['preview', 'view', 'download']; - $image = new \Imagick(); - $image->readImageBlob($fileDownloaded['body']); - $original = new \Imagick(__DIR__ . '/../../../resources/logo.png'); + foreach ($endpoints as $endpoint) { + $response = $this->client->call( + Client::METHOD_GET, + "/storage/buckets/{$bucketId}/files/{$fileId}/$endpoint", + $guestHeaders, + ['token' => $jwtToken] + ); + + $this->assertNotEmpty($response['body']); + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals('image/png', $response['headers']['content-type']); + + if ($endpoint === 'download') { + $image = new \Imagick(); + $image->readImageBlob($response['body']); + $original = new \Imagick(__DIR__ . '/../../../resources/logo.png'); + + $this->assertEquals($original->getImageWidth(), $image->getImageWidth()); + $this->assertEquals($original->getImageHeight(), $image->getImageHeight()); + $this->assertEquals('PNG', $image->getImageFormat()); + } + } - $this->assertEquals($image->getImageWidth(), $original->getImageWidth()); - $this->assertEquals($image->getImageHeight(), $original->getImageHeight()); - $this->assertEquals('PNG', $image->getImageFormat()); } } From 020814e6728870b4e4c3ad2af96deecaf28f4aa8 Mon Sep 17 00:00:00 2001 From: Darshan Date: Tue, 8 Jul 2025 15:43:28 +0530 Subject: [PATCH 4/5] update: inline guest headers. --- tests/e2e/Services/Tokens/TokensBase.php | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/tests/e2e/Services/Tokens/TokensBase.php b/tests/e2e/Services/Tokens/TokensBase.php index 7690653453..1ac0452d52 100644 --- a/tests/e2e/Services/Tokens/TokensBase.php +++ b/tests/e2e/Services/Tokens/TokensBase.php @@ -330,18 +330,19 @@ trait TokensBase $jwtToken = $token['body']['secret']; - $guestHeaders = ['content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ]; - $endpoints = ['preview', 'view', 'download']; foreach ($endpoints as $endpoint) { $response = $this->client->call( Client::METHOD_GET, "/storage/buckets/{$bucketId}/files/{$fileId}/$endpoint", - $guestHeaders, - ['token' => $jwtToken] + [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], + [ + 'token' => $jwtToken + ] ); $this->assertNotEmpty($response['body']); From 7ca951ed81599b4865a76b3e3886102d071a0bd4 Mon Sep 17 00:00:00 2001 From: Darshan Date: Tue, 8 Jul 2025 15:43:51 +0530 Subject: [PATCH 5/5] remove: `{}` --- tests/e2e/Services/Tokens/TokensBase.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/e2e/Services/Tokens/TokensBase.php b/tests/e2e/Services/Tokens/TokensBase.php index 1ac0452d52..a4461c06c2 100644 --- a/tests/e2e/Services/Tokens/TokensBase.php +++ b/tests/e2e/Services/Tokens/TokensBase.php @@ -335,7 +335,7 @@ trait TokensBase foreach ($endpoints as $endpoint) { $response = $this->client->call( Client::METHOD_GET, - "/storage/buckets/{$bucketId}/files/{$fileId}/$endpoint", + "/storage/buckets/$bucketId/files/$fileId/$endpoint", [ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'],