From 10a9845898b57a7e2e8b9f1ab30b9d91b1e79005 Mon Sep 17 00:00:00 2001 From: Hemachandar Date: Mon, 3 Nov 2025 16:53:44 +0530 Subject: [PATCH 1/4] fix: Throw error when file token expiry is in the past --- phpunit.xml | 1 + .../Http/Tokens/Buckets/Files/Create.php | 11 +++++-- .../Modules/Tokens/Http/Tokens/Update.php | 11 +++++-- .../Tokens/TokensConsoleClientTest.php | 29 ++++++++++++++++-- .../Tokens/TokensCustomServerTest.php | 30 ++++++++++++++++--- 5 files changed, 71 insertions(+), 11 deletions(-) diff --git a/phpunit.xml b/phpunit.xml index 4c4e55ea4e..a8578995c1 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -31,6 +31,7 @@ ./tests/e2e/Services/Locale ./tests/e2e/Services/Projects ./tests/e2e/Services/Storage + ./tests/e2e/Services/Tokens ./tests/e2e/Services/Webhooks ./tests/e2e/Services/Messaging ./tests/e2e/Services/Migrations diff --git a/src/Appwrite/Platform/Modules/Tokens/Http/Tokens/Buckets/Files/Create.php b/src/Appwrite/Platform/Modules/Tokens/Http/Tokens/Buckets/Files/Create.php index fe7a0187e9..0d905838a7 100644 --- a/src/Appwrite/Platform/Modules/Tokens/Http/Tokens/Buckets/Files/Create.php +++ b/src/Appwrite/Platform/Modules/Tokens/Http/Tokens/Buckets/Files/Create.php @@ -17,7 +17,7 @@ use Utopia\Database\Validator\Authorization; use Utopia\Database\Validator\Datetime as DatetimeValidator; use Utopia\Database\Validator\UID; use Utopia\Platform\Scope\HTTP; -use Utopia\Validator\Nullable; +use Utopia\Validator\Text; class Create extends Action { @@ -61,7 +61,7 @@ class Create extends Action )) ->param('bucketId', '', new UID(), 'Storage bucket unique ID. You can create a new storage bucket using the Storage service [server integration](https://appwrite.io/docs/server/storage#createBucket).') ->param('fileId', '', new UID(), 'File unique ID.') - ->param('expire', null, new Nullable(new DatetimeValidator()), 'Token expiry date', true) + ->param('expire', null, new Text(100), 'Token expiry date', true) ->inject('response') ->inject('dbForProject') ->inject('queueForEvents') @@ -70,6 +70,13 @@ class Create extends Action public function action(string $bucketId, string $fileId, ?string $expire, Response $response, Database $dbForProject, Event $queueForEvents): void { + if (!is_null($expire)) { + $validator = new DatetimeValidator(requireDateInFuture: true, precision: DateTimeValidator::PRECISION_DAYS, offset: 0); + if (!$validator->isValid($expire)) { + throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Token expiry date must be a valid date, and at least 1 day from now'); + } + } + /** * @var Document $bucket diff --git a/src/Appwrite/Platform/Modules/Tokens/Http/Tokens/Update.php b/src/Appwrite/Platform/Modules/Tokens/Http/Tokens/Update.php index 7a15708011..b1b670532d 100644 --- a/src/Appwrite/Platform/Modules/Tokens/Http/Tokens/Update.php +++ b/src/Appwrite/Platform/Modules/Tokens/Http/Tokens/Update.php @@ -14,7 +14,7 @@ use Utopia\Database\Validator\Datetime as DatetimeValidator; use Utopia\Database\Validator\UID; use Utopia\Platform\Action; use Utopia\Platform\Scope\HTTP; -use Utopia\Validator\Nullable; +use Utopia\Validator\Text; class Update extends Action { @@ -57,7 +57,7 @@ class Update extends Action contentType: ContentType::JSON )) ->param('tokenId', '', new UID(), 'Token unique ID.') - ->param('expire', null, new Nullable(new DatetimeValidator()), 'File token expiry date', true) + ->param('expire', null, new Text(100), 'File token expiry date', true) ->inject('response') ->inject('dbForProject') ->inject('queueForEvents') @@ -66,6 +66,13 @@ class Update extends Action public function action(string $tokenId, ?string $expire, Response $response, Database $dbForProject, Event $queueForEvents) { + if (!is_null($expire)) { + $validator = new DatetimeValidator(requireDateInFuture: true, precision: DateTimeValidator::PRECISION_DAYS, offset: 0); + if (!$validator->isValid($expire)) { + throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Token expiry date must be a valid date, and at least 1 day from now'); + } + } + $token = $dbForProject->getDocument('resourceTokens', $tokenId); if ($token->isEmpty()) { diff --git a/tests/e2e/Services/Tokens/TokensConsoleClientTest.php b/tests/e2e/Services/Tokens/TokensConsoleClientTest.php index c0f94a55bf..ab3790abca 100644 --- a/tests/e2e/Services/Tokens/TokensConsoleClientTest.php +++ b/tests/e2e/Services/Tokens/TokensConsoleClientTest.php @@ -9,7 +9,6 @@ use Tests\E2E\Client; use Tests\E2E\Scopes\ProjectCustom; use Tests\E2E\Scopes\Scope; use Tests\E2E\Scopes\SideServer; -use Utopia\Database\DateTime; use Utopia\Database\Helpers\ID; use Utopia\Database\Helpers\Permission; use Utopia\Database\Helpers\Role; @@ -63,10 +62,23 @@ class TokensConsoleClientTest extends Scope $fileId = $file['body']['$id']; + // Failure case: Expire date is in the past $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->getHeaders()), [ + 'expire' => '2022-11-02', + ]); + $this->assertEquals(400, $token['headers']['status-code']); + $this->assertEquals('Token expiry date must be a valid date, and at least 1 day from now', $token['body']['message']); + + // Success case: No expire date + $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()), [ + 'expire' => null, + ]); $this->assertEquals(201, $token['headers']['status-code']); $this->assertEquals('files', $token['body']['resourceType']); @@ -107,8 +119,19 @@ class TokensConsoleClientTest extends Scope { $tokenId = $data['tokenId']; + // Failure case: Expire date is in the past + $token = $this->client->call(Client::METHOD_PATCH, '/tokens/' . $tokenId, [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ], [ + 'expire' => '2022-11-02', + ]); + $this->assertEquals(400, $token['headers']['status-code']); + $this->assertEquals('Token expiry date must be a valid date, and at least 1 day from now', $token['body']['message']); + // Finite expiry - $expiry = DateTime::addSeconds(new \DateTime(), 3600); + $expiry = date('Y-m-d', strtotime("tomorrow")); $token = $this->client->call(Client::METHOD_PATCH, '/tokens/' . $tokenId, array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'] diff --git a/tests/e2e/Services/Tokens/TokensCustomServerTest.php b/tests/e2e/Services/Tokens/TokensCustomServerTest.php index fe8fa2bad9..eaf30049f9 100644 --- a/tests/e2e/Services/Tokens/TokensCustomServerTest.php +++ b/tests/e2e/Services/Tokens/TokensCustomServerTest.php @@ -7,7 +7,6 @@ use Tests\E2E\Client; use Tests\E2E\Scopes\ProjectCustom; use Tests\E2E\Scopes\Scope; use Tests\E2E\Scopes\SideServer; -use Utopia\Database\DateTime; use Utopia\Database\Helpers\ID; use Utopia\Database\Helpers\Permission; use Utopia\Database\Helpers\Role; @@ -61,6 +60,17 @@ class TokensCustomServerTest extends Scope $fileId = $file['body']['$id']; + // Failure case: Expire date is in the past + $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()), [ + 'expire' => '2022-11-02', + ]); + $this->assertEquals(400, $token['headers']['status-code']); + $this->assertEquals('Token expiry date must be a valid date, and at least 1 day from now', $token['body']['message']); + + // Success case: No expire date $token = $this->client->call(Client::METHOD_POST, '/tokens/buckets/' . $bucketId . '/files/' . $fileId, array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'] @@ -83,8 +93,19 @@ class TokensCustomServerTest extends Scope { $tokenId = $data['tokenId']; - // Finite expiry - $expiry = DateTime::addSeconds(new \DateTime(), 3600); + // Failure case: Expire date is in the past + $token = $this->client->call(Client::METHOD_PATCH, '/tokens/' . $tokenId, [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ], [ + 'expire' => '2022-11-02', + ]); + $this->assertEquals(400, $token['headers']['status-code']); + $this->assertEquals('Token expiry date must be a valid date, and at least 1 day from now', $token['body']['message']); + + // Success case: Finite expiry + $expiry = date('Y-m-d', strtotime("tomorrow")); $token = $this->client->call(Client::METHOD_PATCH, '/tokens/' . $tokenId, [ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], @@ -94,9 +115,10 @@ class TokensCustomServerTest extends Scope ]); $dateValidator = new DatetimeValidator(); + $this->assertEquals(200, $token['headers']['status-code']); $this->assertTrue($dateValidator->isValid($token['body']['expire'])); - // Infinite expiry + // Success case: Infinite expiry $token = $this->client->call(Client::METHOD_PATCH, '/tokens/' . $tokenId, array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], From 60e57eb9c59185ae6f83d5855cdbb0927870552b Mon Sep 17 00:00:00 2001 From: Hemachandar Date: Mon, 3 Nov 2025 17:29:48 +0530 Subject: [PATCH 2/4] lint --- .../Modules/Tokens/Http/Tokens/Buckets/Files/Create.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Appwrite/Platform/Modules/Tokens/Http/Tokens/Buckets/Files/Create.php b/src/Appwrite/Platform/Modules/Tokens/Http/Tokens/Buckets/Files/Create.php index 0d905838a7..aa06753420 100644 --- a/src/Appwrite/Platform/Modules/Tokens/Http/Tokens/Buckets/Files/Create.php +++ b/src/Appwrite/Platform/Modules/Tokens/Http/Tokens/Buckets/Files/Create.php @@ -76,7 +76,7 @@ class Create extends Action throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Token expiry date must be a valid date, and at least 1 day from now'); } } - + /** * @var Document $bucket From 08c9db7c60289a33f51c49351edcb85a70f346c8 Mon Sep 17 00:00:00 2001 From: Hemachandar Date: Wed, 12 Nov 2025 19:23:57 +0530 Subject: [PATCH 3/4] use inline validator --- .../Tokens/Http/Tokens/Buckets/Files/Create.php | 12 ++---------- .../Platform/Modules/Tokens/Http/Tokens/Update.php | 11 ++--------- 2 files changed, 4 insertions(+), 19 deletions(-) diff --git a/src/Appwrite/Platform/Modules/Tokens/Http/Tokens/Buckets/Files/Create.php b/src/Appwrite/Platform/Modules/Tokens/Http/Tokens/Buckets/Files/Create.php index aa06753420..e4de4c1380 100644 --- a/src/Appwrite/Platform/Modules/Tokens/Http/Tokens/Buckets/Files/Create.php +++ b/src/Appwrite/Platform/Modules/Tokens/Http/Tokens/Buckets/Files/Create.php @@ -17,7 +17,7 @@ use Utopia\Database\Validator\Authorization; use Utopia\Database\Validator\Datetime as DatetimeValidator; use Utopia\Database\Validator\UID; use Utopia\Platform\Scope\HTTP; -use Utopia\Validator\Text; +use Utopia\Validator\Nullable; class Create extends Action { @@ -61,7 +61,7 @@ class Create extends Action )) ->param('bucketId', '', new UID(), 'Storage bucket unique ID. You can create a new storage bucket using the Storage service [server integration](https://appwrite.io/docs/server/storage#createBucket).') ->param('fileId', '', new UID(), 'File unique ID.') - ->param('expire', null, new Text(100), 'Token expiry date', true) + ->param('expire', null, new Nullable(new DatetimeValidator(requireDateInFuture: true)), 'Token expiry date', true) ->inject('response') ->inject('dbForProject') ->inject('queueForEvents') @@ -70,14 +70,6 @@ class Create extends Action public function action(string $bucketId, string $fileId, ?string $expire, Response $response, Database $dbForProject, Event $queueForEvents): void { - if (!is_null($expire)) { - $validator = new DatetimeValidator(requireDateInFuture: true, precision: DateTimeValidator::PRECISION_DAYS, offset: 0); - if (!$validator->isValid($expire)) { - throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Token expiry date must be a valid date, and at least 1 day from now'); - } - } - - /** * @var Document $bucket * @var Document $file diff --git a/src/Appwrite/Platform/Modules/Tokens/Http/Tokens/Update.php b/src/Appwrite/Platform/Modules/Tokens/Http/Tokens/Update.php index b1b670532d..fef2c38a81 100644 --- a/src/Appwrite/Platform/Modules/Tokens/Http/Tokens/Update.php +++ b/src/Appwrite/Platform/Modules/Tokens/Http/Tokens/Update.php @@ -14,7 +14,7 @@ use Utopia\Database\Validator\Datetime as DatetimeValidator; use Utopia\Database\Validator\UID; use Utopia\Platform\Action; use Utopia\Platform\Scope\HTTP; -use Utopia\Validator\Text; +use Utopia\Validator\Nullable; class Update extends Action { @@ -57,7 +57,7 @@ class Update extends Action contentType: ContentType::JSON )) ->param('tokenId', '', new UID(), 'Token unique ID.') - ->param('expire', null, new Text(100), 'File token expiry date', true) + ->param('expire', null, new Nullable(new DatetimeValidator(requireDateInFuture: true)), 'File token expiry date', true) ->inject('response') ->inject('dbForProject') ->inject('queueForEvents') @@ -66,13 +66,6 @@ class Update extends Action public function action(string $tokenId, ?string $expire, Response $response, Database $dbForProject, Event $queueForEvents) { - if (!is_null($expire)) { - $validator = new DatetimeValidator(requireDateInFuture: true, precision: DateTimeValidator::PRECISION_DAYS, offset: 0); - if (!$validator->isValid($expire)) { - throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Token expiry date must be a valid date, and at least 1 day from now'); - } - } - $token = $dbForProject->getDocument('resourceTokens', $tokenId); if ($token->isEmpty()) { From bc19e667759720ea604d49ebac5b078f750531a4 Mon Sep 17 00:00:00 2001 From: Hemachandar Date: Wed, 12 Nov 2025 20:27:42 +0530 Subject: [PATCH 4/4] fix tests --- tests/e2e/Services/Tokens/TokensConsoleClientTest.php | 4 ++-- tests/e2e/Services/Tokens/TokensCustomServerTest.php | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/e2e/Services/Tokens/TokensConsoleClientTest.php b/tests/e2e/Services/Tokens/TokensConsoleClientTest.php index ab3790abca..f1480faba0 100644 --- a/tests/e2e/Services/Tokens/TokensConsoleClientTest.php +++ b/tests/e2e/Services/Tokens/TokensConsoleClientTest.php @@ -70,7 +70,7 @@ class TokensConsoleClientTest extends Scope 'expire' => '2022-11-02', ]); $this->assertEquals(400, $token['headers']['status-code']); - $this->assertEquals('Token expiry date must be a valid date, and at least 1 day from now', $token['body']['message']); + $this->assertStringContainsString('Value must be valid date in the future', $token['body']['message']); // Success case: No expire date $token = $this->client->call(Client::METHOD_POST, '/tokens/buckets/' . $bucketId . '/files/' . $fileId, array_merge([ @@ -128,7 +128,7 @@ class TokensConsoleClientTest extends Scope 'expire' => '2022-11-02', ]); $this->assertEquals(400, $token['headers']['status-code']); - $this->assertEquals('Token expiry date must be a valid date, and at least 1 day from now', $token['body']['message']); + $this->assertStringContainsString('Value must be valid date in the future', $token['body']['message']); // Finite expiry $expiry = date('Y-m-d', strtotime("tomorrow")); diff --git a/tests/e2e/Services/Tokens/TokensCustomServerTest.php b/tests/e2e/Services/Tokens/TokensCustomServerTest.php index eaf30049f9..779d5449b3 100644 --- a/tests/e2e/Services/Tokens/TokensCustomServerTest.php +++ b/tests/e2e/Services/Tokens/TokensCustomServerTest.php @@ -68,7 +68,7 @@ class TokensCustomServerTest extends Scope 'expire' => '2022-11-02', ]); $this->assertEquals(400, $token['headers']['status-code']); - $this->assertEquals('Token expiry date must be a valid date, and at least 1 day from now', $token['body']['message']); + $this->assertStringContainsString('Value must be valid date in the future', $token['body']['message']); // Success case: No expire date $token = $this->client->call(Client::METHOD_POST, '/tokens/buckets/' . $bucketId . '/files/' . $fileId, array_merge([ @@ -102,7 +102,7 @@ class TokensCustomServerTest extends Scope 'expire' => '2022-11-02', ]); $this->assertEquals(400, $token['headers']['status-code']); - $this->assertEquals('Token expiry date must be a valid date, and at least 1 day from now', $token['body']['message']); + $this->assertStringContainsString('Value must be valid date in the future', $token['body']['message']); // Success case: Finite expiry $expiry = date('Y-m-d', strtotime("tomorrow"));