diff --git a/.env b/.env index 4ab721f2fd..c8c0fccb2b 100644 --- a/.env +++ b/.env @@ -4,6 +4,7 @@ _APP_WORKER_PER_CORE=6 _APP_CONSOLE_WHITELIST_ROOT=disabled _APP_CONSOLE_WHITELIST_EMAILS= _APP_CONSOLE_WHITELIST_IPS= +_APP_CONSOLE_COUNTRIES_DENYLIST=AQ _APP_CONSOLE_HOSTNAMES=localhost,appwrite.io,*.appwrite.io _APP_SYSTEM_EMAIL_NAME=Appwrite _APP_SYSTEM_EMAIL_ADDRESS=team@appwrite.io diff --git a/app/config/errors.php b/app/config/errors.php index 1f551b1ca9..79a092ec99 100644 --- a/app/config/errors.php +++ b/app/config/errors.php @@ -114,6 +114,11 @@ return [ 'description' => 'Value must be a valid phone number. Format this number with a leading \'+\' and a country code, e.g., +16175551212.', 'code' => 400, ], + Exception::GENERAL_REGION_ACCESS_DENIED => [ + 'name' => Exception::GENERAL_REGION_ACCESS_DENIED, + 'description' => 'Your location is not supported due to legal requirements.', + 'code' => 451, + ], /** User Errors */ Exception::USER_COUNT_EXCEEDED => [ diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index 5e94c22ec1..5b56f722b6 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -1220,9 +1220,9 @@ App::post('/v1/account/tokens/magic-url') App::post('/v1/account/tokens/email') ->desc('Create email token (OTP)') - ->groups(['api', 'account']) + ->groups(['api', 'account', 'auth']) ->label('scope', 'sessions.write') - ->label('auth.type', 'email') + ->label('auth.type', 'email-otp') ->label('audits.event', 'session.create') ->label('audits.resource', 'user/{response.userId}') ->label('audits.userId', '{response.userId}') diff --git a/app/controllers/general.php b/app/controllers/general.php index 74f12017b1..2a139622ad 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -736,6 +736,7 @@ App::error() case 412: // Error allowed publicly case 416: // Error allowed publicly case 429: // Error allowed publicly + case 451: // Error allowed publicly case 501: // Error allowed publicly case 503: // Error allowed publicly break; diff --git a/app/controllers/shared/api.php b/app/controllers/shared/api.php index d6cc702e75..85a74ecca8 100644 --- a/app/controllers/shared/api.php +++ b/app/controllers/shared/api.php @@ -22,6 +22,7 @@ use Utopia\Database\Database; use Utopia\Database\DateTime; use Utopia\Database\Document; use Utopia\Database\Validator\Authorization; +use MaxMind\Db\Reader; $parseLabel = function (string $label, array $responsePayload, array $requestParams, Document $user) { preg_match_all('/{(.*?)}/', $label, $matches); @@ -319,7 +320,17 @@ App::init() ->inject('utopia') ->inject('request') ->inject('project') - ->action(function (App $utopia, Request $request, Document $project) { + ->inject('geodb') + ->action(function (App $utopia, Request $request, Document $project, Reader $geodb) { + $denylist = App::getEnv('_APP_CONSOLE_COUNTRIES_DENYLIST', ''); + if (!empty($denylist) && $project->getId() === 'console') { + $countries = explode(',', $denylist); + $record = $geodb->get($request->getIP()) ?? []; + $country = $record['country']['iso_code'] ?? ''; + if (in_array($country, $countries)) { + throw new Exception(Exception::GENERAL_REGION_ACCESS_DENIED); + } + } $route = $utopia->getRoute(); @@ -368,6 +379,12 @@ App::init() } break; + case 'email-otp': + if (($auths['emailOTP'] ?? true) === false) { + throw new Exception(Exception::USER_AUTH_METHOD_UNSUPPORTED, 'Email OTP authentication is disabled for this project'); + } + break; + default: throw new Exception(Exception::USER_AUTH_METHOD_UNSUPPORTED, 'Unsupported authentication route'); break; diff --git a/app/controllers/shared/api/auth.php b/app/controllers/shared/api/auth.php index b603726be0..0b24f8b3a1 100644 --- a/app/controllers/shared/api/auth.php +++ b/app/controllers/shared/api/auth.php @@ -6,13 +6,24 @@ use Utopia\App; use Appwrite\Extend\Exception; use Utopia\Database\Document; use Utopia\Database\Validator\Authorization; +use MaxMind\Db\Reader; App::init() ->groups(['auth']) ->inject('utopia') ->inject('request') ->inject('project') - ->action(function (App $utopia, Request $request, Document $project) { + ->inject('geodb') + ->action(function (App $utopia, Request $request, Document $project, Reader $geodb) { + $denylist = App::getEnv('_APP_CONSOLE_COUNTRIES_DENYLIST', ''); + if (!empty($denylist && $project->getId() === 'console')) { + $countries = explode(',', $denylist); + $record = $geodb->get($request->getIP()) ?? []; + $country = $record['country']['iso_code'] ?? ''; + if (in_array($country, $countries)) { + throw new Exception(Exception::GENERAL_REGION_ACCESS_DENIED); + } + } $route = $utopia->match($request); @@ -61,6 +72,12 @@ App::init() } break; + case 'email-otp': + if (($auths['emailOTP'] ?? true) === false) { + throw new Exception(Exception::USER_AUTH_METHOD_UNSUPPORTED, 'Email OTP authentication is disabled for this project'); + } + break; + default: throw new Exception(Exception::USER_AUTH_METHOD_UNSUPPORTED, 'Unsupported authentication route'); break; diff --git a/composer.json b/composer.json index e1b861054c..2c78bcd4a3 100644 --- a/composer.json +++ b/composer.json @@ -65,7 +65,7 @@ "utopia-php/queue": "0.6.*", "utopia-php/registry": "0.5.*", "utopia-php/storage": "0.18.*", - "utopia-php/swoole": "0.5.*", + "utopia-php/swoole": "0.8.*", "utopia-php/vcs": "0.6.*", "utopia-php/websocket": "0.1.*", "matomo/device-detector": "6.1.*", diff --git a/composer.lock b/composer.lock index dc1e9085ea..226d55e38f 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": "3b43bf6f0fca50a3a2834e1bbaa90d63", + "content-hash": "e6e0d6874f4d718d96396d71a66864da", "packages": [ { "name": "adhocore/jwt", @@ -2432,28 +2432,28 @@ }, { "name": "utopia-php/swoole", - "version": "0.5.0", + "version": "0.8.2", "source": { "type": "git", "url": "https://github.com/utopia-php/swoole.git", - "reference": "c2a3a4f944a2f22945af3cbcb95b13f0769628b1" + "reference": "5fa9d42c608ad46a4ce42a6d2b2eae00592fccd4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/swoole/zipball/c2a3a4f944a2f22945af3cbcb95b13f0769628b1", - "reference": "c2a3a4f944a2f22945af3cbcb95b13f0769628b1", + "url": "https://api.github.com/repos/utopia-php/swoole/zipball/5fa9d42c608ad46a4ce42a6d2b2eae00592fccd4", + "reference": "5fa9d42c608ad46a4ce42a6d2b2eae00592fccd4", "shasum": "" }, "require": { "ext-swoole": "*", "php": ">=8.0", - "utopia-php/framework": "0.*.*" + "utopia-php/framework": "0.33.*" }, "require-dev": { "laravel/pint": "1.2.*", + "phpstan/phpstan": "^1.10", "phpunit/phpunit": "^9.3", - "swoole/ide-helper": "4.8.3", - "vimeo/psalm": "4.15.0" + "swoole/ide-helper": "5.0.2" }, "type": "library", "autoload": { @@ -2477,9 +2477,9 @@ ], "support": { "issues": "https://github.com/utopia-php/swoole/issues", - "source": "https://github.com/utopia-php/swoole/tree/0.5.0" + "source": "https://github.com/utopia-php/swoole/tree/0.8.2" }, - "time": "2022-10-19T22:19:07+00:00" + "time": "2024-02-01T14:54:12+00:00" }, { "name": "utopia-php/system", diff --git a/docker-compose.yml b/docker-compose.yml index 99400f92cb..72d81cc8a4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -188,6 +188,7 @@ services: - _APP_MESSAGE_SMS_TEST_DSN - _APP_MESSAGE_EMAIL_TEST_DSN - _APP_MESSAGE_PUSH_TEST_DSN + - _APP_CONSOLE_COUNTRIES_DENYLIST appwrite-realtime: entrypoint: realtime diff --git a/src/Appwrite/Extend/Exception.php b/src/Appwrite/Extend/Exception.php index fd8b1911a5..bd1397bc20 100644 --- a/src/Appwrite/Extend/Exception.php +++ b/src/Appwrite/Extend/Exception.php @@ -57,6 +57,7 @@ class Exception extends \Exception public const GENERAL_NOT_IMPLEMENTED = 'general_not_implemented'; public const GENERAL_INVALID_EMAIL = 'general_invalid_email'; public const GENERAL_INVALID_PHONE = 'general_invalid_phone'; + public const GENERAL_REGION_ACCESS_DENIED = 'general_region_access_denied'; /** Users */ public const USER_COUNT_EXCEEDED = 'user_count_exceeded'; diff --git a/tests/e2e/Services/Account/AccountBase.php b/tests/e2e/Services/Account/AccountBase.php index 55c00d43bc..ebb9ea644c 100644 --- a/tests/e2e/Services/Account/AccountBase.php +++ b/tests/e2e/Services/Account/AccountBase.php @@ -44,6 +44,21 @@ trait AccountBase /** * Test for FAILURE */ + // Deny request from blocked IP + $response = $this->client->call(Client::METHOD_POST, '/account', array_merge([ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => 'console', + 'x-forwarded-for' => '103.152.127.250' // Test IP for denied access region + ]), [ + 'userId' => ID::unique(), + 'email' => $email, + 'password' => $password, + 'name' => $name, + ]); + + $this->assertEquals(451, $response['headers']['status-code']); + $response = $this->client->call(Client::METHOD_POST, '/account', array_merge([ 'origin' => 'http://localhost', 'content-type' => 'application/json',