diff --git a/app/config/collections.php b/app/config/collections.php index 6ad859a13..672e2b01f 100644 --- a/app/config/collections.php +++ b/app/config/collections.php @@ -1003,6 +1003,28 @@ $collections = [ 'array' => false, 'filters' => ['datetime'], ], + [ + '$id' => ID::custom('accessedAt'), + 'type' => Database::VAR_DATETIME, + 'format' => '', + 'size' => 0, + 'signed' => false, + 'required' => false, + 'default' => null, + 'array' => false, + 'filters' => ['datetime'], + ], + [ + '$id' => ID::custom('sdks'), + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => Database::LENGTH_KEY, + 'signed' => true, + 'required' => true, + 'default' => null, + 'array' => true, + 'filters' => [], + ], ], 'indexes' => [ [ @@ -1012,6 +1034,13 @@ $collections = [ 'lengths' => [Database::LENGTH_KEY], 'orders' => [Database::ORDER_ASC], ], + [ + '$id' => '_key_accessedAt', + 'type' => Database::INDEX_KEY, + 'attributes' => ['accessedAt'], + 'lengths' => [], + 'orders' => [], + ], ], ], diff --git a/app/controllers/api/projects.php b/app/controllers/api/projects.php index 86660bbdb..3b20d073c 100644 --- a/app/controllers/api/projects.php +++ b/app/controllers/api/projects.php @@ -865,6 +865,8 @@ App::post('/v1/projects/:projectId/keys') 'name' => $name, 'scopes' => $scopes, 'expire' => $expire, + 'sdks' => [], + 'accessedAt' => null, 'secret' => \bin2hex(\random_bytes(128)), ]); diff --git a/app/controllers/general.php b/app/controllers/general.php index ee08cca72..221e69678 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -32,6 +32,7 @@ use Appwrite\Utopia\Request\Filters\V12 as RequestV12; use Appwrite\Utopia\Request\Filters\V13 as RequestV13; use Appwrite\Utopia\Request\Filters\V14 as RequestV14; use Utopia\Validator\Text; +use Utopia\Validator\WhiteList; Config::setParam('domainVerification', false); Config::setParam('cookieDomain', 'localhost'); @@ -47,7 +48,8 @@ App::init() ->inject('user') ->inject('locale') ->inject('clients') - ->action(function (App $utopia, Request $request, Response $response, Document $console, Document $project, Database $dbForConsole, Document $user, Locale $locale, array $clients) { + ->inject('servers') + ->action(function (App $utopia, Request $request, Response $response, Document $console, Document $project, Database $dbForConsole, Document $user, Locale $locale, array $clients, array $servers) { /* * Request format */ @@ -303,6 +305,28 @@ App::init() 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->deleteCachedDocument('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->deleteCachedDocument('projects', $project->getId()); + } + } } } diff --git a/app/controllers/mock.php b/app/controllers/mock.php index c90675873..80b9a4f0a 100644 --- a/app/controllers/mock.php +++ b/app/controllers/mock.php @@ -7,6 +7,7 @@ use Utopia\Database\Document; use Appwrite\Network\Validator\Host; use Appwrite\Utopia\Request; use Appwrite\Utopia\Response; +use Appwrite\Utopia\Response\Model; use Utopia\App; use Utopia\Validator\ArrayList; use Utopia\Validator\Integer; @@ -395,6 +396,35 @@ App::get('/v1/mock/tests/general/empty') $response->noContent(); }); +/** Endpoint to test if required headers are sent from the SDK */ +App::get('/v1/mock/tests/general/headers') + ->desc('Get headers') + ->groups(['mock']) + ->label('scope', 'public') + ->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT]) + ->label('sdk.namespace', 'general') + ->label('sdk.method', 'headers') + ->label('sdk.description', 'Return headers from the request') + ->label('sdk.response.code', Response::STATUS_CODE_OK) + ->label('sdk.response.model', Response::MODEL_MOCK) + ->label('sdk.mock', true) + ->inject('request') + ->inject('response') + ->action(function (Request $request, Response $response) { + $res = [ + 'x-sdk-name' => $request->getHeader('x-sdk-name'), + 'x-sdk-platform' => $request->getHeader('x-sdk-platform'), + 'x-sdk-language' => $request->getHeader('x-sdk-language'), + 'x-sdk-version' => $request->getHeader('x-sdk-version'), + ]; + $res = array_map(function ($key, $value) { + return $key . ': ' . $value; + }, array_keys($res), $res); + $res = implode("; ", $res); + + $response->dynamic(new Document(['result' => $res]), Response::MODEL_MOCK); + }); + App::get('/v1/mock/tests/general/400-error') ->desc('400 Error') ->groups(['mock']) diff --git a/app/init.php b/app/init.php index 093e7ebc5..dacb9c2ae 100644 --- a/app/init.php +++ b/app/init.php @@ -90,6 +90,7 @@ const APP_LIMIT_COMPRESSION = 20000000; //20MB const APP_LIMIT_ARRAY_PARAMS_SIZE = 100; // Default maximum of how many elements can there be in API parameter that expects array value const APP_LIMIT_ARRAY_ELEMENT_SIZE = 4096; // Default maximum length of element in array parameter represented by maximum URL length. const APP_LIMIT_SUBQUERY = 1000; +const APP_KEY_ACCCESS = 24 * 60 * 60; // 24 hours const APP_CACHE_BUSTER = 402; const APP_VERSION_STABLE = '0.15.3'; const APP_DATABASE_ATTRIBUTE_EMAIL = 'email'; @@ -1015,3 +1016,14 @@ App::setResource('sms', function () { default => null }; }); + +App::setResource('servers', function () { + $platforms = Config::getParam('platforms'); + $server = $platforms[APP_PLATFORM_SERVER]; + + $languages = array_map(function ($language) { + return strtolower($language['name']); + }, $server['languages']); + + return $languages; +}); diff --git a/app/tasks/sdks.php b/app/tasks/sdks.php index d87316774..b000cc748 100644 --- a/app/tasks/sdks.php +++ b/app/tasks/sdks.php @@ -178,6 +178,7 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ->setLicense($license) ->setLicenseContent($licenseContent) ->setVersion($language['version']) + ->setPlatform($key) ->setGitURL($language['url']) ->setGitRepo($language['gitUrl']) ->setGitRepoName($language['gitRepoName']) diff --git a/composer.json b/composer.json index 2b9aa2d06..27b151a65 100644 --- a/composer.json +++ b/composer.json @@ -77,8 +77,8 @@ } ], "require-dev": { + "appwrite/sdk-generator": "dev-feat-new-headers", "ext-fileinfo": "*", - "appwrite/sdk-generator": "dev-master as 0.19.5", "phpunit/phpunit": "9.5.20", "squizlabs/php_codesniffer": "^3.6", "swoole/ide-helper": "4.8.9", diff --git a/composer.lock b/composer.lock index 37c654761..ad0eb287b 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "1145ff29befcc4aa21b5002da0b8319c", + "content-hash": "039de21eff3a27955696a9f6f645c548", "packages": [ { "name": "adhocore/jwt", @@ -2837,7 +2837,7 @@ "packages-dev": [ { "name": "appwrite/sdk-generator", - "version": "dev-master", + "version": "dev-feat-new-headers", "source": { "type": "git", "url": "https://github.com/appwrite/sdk-generator.git", @@ -2861,7 +2861,6 @@ "brianium/paratest": "^6.4", "phpunit/phpunit": "^9.5.21" }, - "default-branch": true, "type": "library", "autoload": { "psr-4": { @@ -2882,7 +2881,7 @@ "description": "Appwrite PHP library for generating API SDKs for multiple programming languages and platforms", "support": { "issues": "https://github.com/appwrite/sdk-generator/issues", - "source": "https://github.com/appwrite/sdk-generator/tree/master" + "source": "https://github.com/appwrite/sdk-generator/tree/feat-new-headers" }, "time": "2022-08-29T10:43:33+00:00" }, @@ -3534,16 +3533,16 @@ }, { "name": "phpunit/php-code-coverage", - "version": "9.2.16", + "version": "9.2.17", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "2593003befdcc10db5e213f9f28814f5aa8ac073" + "reference": "aa94dc41e8661fe90c7316849907cba3007b10d8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/2593003befdcc10db5e213f9f28814f5aa8ac073", - "reference": "2593003befdcc10db5e213f9f28814f5aa8ac073", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/aa94dc41e8661fe90c7316849907cba3007b10d8", + "reference": "aa94dc41e8661fe90c7316849907cba3007b10d8", "shasum": "" }, "require": { @@ -3599,7 +3598,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.16" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.17" }, "funding": [ { @@ -3607,7 +3606,7 @@ "type": "github" } ], - "time": "2022-08-20T05:26:47+00:00" + "time": "2022-08-30T12:24:04+00:00" }, { "name": "phpunit/php-file-iterator", @@ -5356,14 +5355,7 @@ "time": "2022-08-12T06:47:24+00:00" } ], - "aliases": [ - { - "package": "appwrite/sdk-generator", - "version": "9999999-dev", - "alias": "0.19.5", - "alias_normalized": "0.19.5.0" - } - ], + "aliases": [], "minimum-stability": "stable", "stability-flags": { "appwrite/sdk-generator": 20 diff --git a/src/Appwrite/Utopia/Response/Model/Key.php b/src/Appwrite/Utopia/Response/Model/Key.php index 4062454d3..d404db72d 100644 --- a/src/Appwrite/Utopia/Response/Model/Key.php +++ b/src/Appwrite/Utopia/Response/Model/Key.php @@ -58,6 +58,19 @@ class Key extends Model 'default' => '', 'example' => '919c2d18fb5d4...a2ae413da83346ad2', ]) + ->addRule('accessedAt', [ + 'type' => self::TYPE_DATETIME, + 'description' => 'Most recent access date in Unix timestamp.', + 'default' => '', + 'example' => self::TYPE_DATETIME_EXAMPLE + ]) + ->addRule('sdks', [ + 'type' => self::TYPE_STRING, + 'description' => 'List of SDK user agents that used this key.', + 'default' => null, + 'example' => 'appwrite:flutter', + 'array' => true + ]) ; } diff --git a/src/Appwrite/Utopia/Response/Model/User.php b/src/Appwrite/Utopia/Response/Model/User.php index 2c6cccbdd..4462fe63f 100644 --- a/src/Appwrite/Utopia/Response/Model/User.php +++ b/src/Appwrite/Utopia/Response/Model/User.php @@ -65,7 +65,7 @@ class User extends Model ->addRule('registration', [ 'type' => self::TYPE_DATETIME, 'description' => 'User registration date in Datetime.', - 'default' => null, + 'default' => '', 'example' => self::TYPE_DATETIME_EXAMPLE, ]) ->addRule('status', [ diff --git a/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php b/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php index 9b270c559..96979461f 100644 --- a/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php +++ b/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php @@ -851,6 +851,7 @@ class ProjectsConsoleClientTest extends Scope 'content-type' => 'application/json', 'x-appwrite-project' => $id, 'x-appwrite-key' => $keySecret, + 'x-sdk-name' => 'python' ])); $this->assertEquals(200, $response['headers']['status-code']); @@ -859,6 +860,7 @@ class ProjectsConsoleClientTest extends Scope 'content-type' => 'application/json', 'x-appwrite-project' => $id, 'x-appwrite-key' => $keySecret, + 'x-sdk-name' => 'php' ]), [ 'teamId' => ID::unique(), 'name' => 'Arsenal' @@ -866,6 +868,21 @@ class ProjectsConsoleClientTest extends Scope $this->assertEquals(201, $response['headers']['status-code']); + /** Check that the API key has been updated */ + $response = $this->client->call(Client::METHOD_GET, '/projects/' . $id . '/keys/' . $keyId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'cookie' => 'a_session_console=' . $this->getRoot()['session'], + ]), []); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertArrayHasKey('sdks', $response['body']); + $this->assertCount(2, $response['body']['sdks']); + $this->assertContains('python', $response['body']['sdks']); + $this->assertContains('php', $response['body']['sdks']); + $this->assertArrayHasKey('accessedAt', $response['body']); + $this->assertNotEmpty($response['body']['accessedAt']); + // Cleanup $response = $this->client->call(Client::METHOD_DELETE, '/projects/' . $id . '/keys/' . $keyId, array_merge([ @@ -1183,6 +1200,10 @@ class ProjectsConsoleClientTest extends Scope $this->assertContains('teams.read', $response['body']['scopes']); $this->assertContains('teams.write', $response['body']['scopes']); $this->assertNotEmpty($response['body']['secret']); + $this->assertArrayHasKey('sdks', $response['body']); + $this->assertEmpty($response['body']['sdks']); + $this->assertArrayHasKey('accessedAt', $response['body']); + $this->assertEmpty($response['body']['accessedAt']); $data = array_merge($data, [ 'keyId' => $response['body']['$id'], @@ -1252,6 +1273,10 @@ class ProjectsConsoleClientTest extends Scope $this->assertContains('teams.write', $response['body']['scopes']); $this->assertCount(2, $response['body']['scopes']); $this->assertNotEmpty($response['body']['secret']); + $this->assertArrayHasKey('sdks', $response['body']); + $this->assertEmpty($response['body']['sdks']); + $this->assertArrayHasKey('accessedAt', $response['body']); + $this->assertEmpty($response['body']['accessedAt']); /** * Test for FAILURE @@ -1360,6 +1385,10 @@ class ProjectsConsoleClientTest extends Scope $this->assertContains('users.write', $response['body']['scopes']); $this->assertContains('collections.read', $response['body']['scopes']); $this->assertCount(3, $response['body']['scopes']); + $this->assertArrayHasKey('sdks', $response['body']); + $this->assertEmpty($response['body']['sdks']); + $this->assertArrayHasKey('accessedAt', $response['body']); + $this->assertEmpty($response['body']['accessedAt']); $response = $this->client->call(Client::METHOD_GET, '/projects/' . $id . '/keys/' . $keyId, array_merge([ 'content-type' => 'application/json', @@ -1374,6 +1403,10 @@ class ProjectsConsoleClientTest extends Scope $this->assertContains('users.write', $response['body']['scopes']); $this->assertContains('collections.read', $response['body']['scopes']); $this->assertCount(3, $response['body']['scopes']); + $this->assertArrayHasKey('sdks', $response['body']); + $this->assertEmpty($response['body']['sdks']); + $this->assertArrayHasKey('accessedAt', $response['body']); + $this->assertEmpty($response['body']['accessedAt']); /** * Test for FAILURE diff --git a/tests/unit/Usage/StatsTest.php b/tests/unit/Usage/StatsTest.php index 29db96754..0b39dfdaa 100644 --- a/tests/unit/Usage/StatsTest.php +++ b/tests/unit/Usage/StatsTest.php @@ -1,6 +1,6 @@