diff --git a/.env b/.env index f650d0bc88..9f6050fe50 100644 --- a/.env +++ b/.env @@ -1,6 +1,9 @@ _APP_ENV=production _APP_ENV=development _APP_LOCALE=en +_APP_CONSOLE_WHITELIST_ROOT=disabled +_APP_CONSOLE_WHITELIST_EMAILS= +_APP_CONSOLE_WHITELIST_IPS= _APP_SYSTEM_EMAIL_NAME=Appwrite _APP_SYSTEM_EMAIL_ADDRESS=team@appwrite.io _APP_SYSTEM_SECURITY_EMAIL_ADDRESS=security@appwrite.io diff --git a/CHANGES.md b/CHANGES.md index 89f4609190..bcfb3ec934 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,7 +1,7 @@ # Version 0.8.0 (Not Released Yet) ## Features - +- Refactoring SSL generation to work on every request so no domain environment variable is required for SSL generation (#1133) - Added Anonymous Login ([RFC-010](https://github.com/appwrite/rfc/blob/main/010-anonymous-login.md), #914) - Added events for functions and executions (#971) - Added JWT support (#784) @@ -12,7 +12,7 @@ - Added option to disable anonymous login (need to merge and apply changed) (#947) - Added option to disable JWT auth (#947) - Added option to disable team invites (#947) -- Option to limit number of users (good for app launches + god account PR) (#947) +- Option to limit number of users (good for app launches + root account PR) (#947) - Added 2 new endpoints to the projects API to allow new settings - Enabled 501 errors (Not Implemented) from the error handler - Added Python 3.9 as a new Cloud Functions runtime (#1044) diff --git a/Dockerfile b/Dockerfile index 83f5c07b3f..8c59bb7269 100755 --- a/Dockerfile +++ b/Dockerfile @@ -88,6 +88,13 @@ ENV _APP_SERVER=swoole \ _APP_DOMAIN_TARGET=localhost \ _APP_HOME=https://appwrite.io \ _APP_EDITION=community \ + _APP_CONSOLE_WHITELIST_ROOT=enabled \ + _APP_CONSOLE_WHITELIST_EMAILS= \ + _APP_CONSOLE_WHITELIST_IPS= \ + _APP_SYSTEM_EMAIL_NAME= \ + _APP_SYSTEM_EMAIL_ADDRESS= \ + _APP_SYSTEM_RESPONSE_FORMAT= \ + _APP_SYSTEM_SECURITY_EMAIL_ADDRESS= \ _APP_OPTIONS_ABUSE=enabled \ _APP_OPTIONS_FORCE_HTTPS=disabled \ _APP_OPENSSL_KEY_V1=your-secret-key \ diff --git a/app/config/collections.php b/app/config/collections.php index 09b27490c8..1cff0031a2 100644 --- a/app/config/collections.php +++ b/app/config/collections.php @@ -46,7 +46,7 @@ $collections = [ 'legalTaxId' => '', 'authWhitelistEmails' => (!empty(App::getEnv('_APP_CONSOLE_WHITELIST_EMAILS', null))) ? \explode(',', App::getEnv('_APP_CONSOLE_WHITELIST_EMAILS', null)) : [], 'authWhitelistIPs' => (!empty(App::getEnv('_APP_CONSOLE_WHITELIST_IPS', null))) ? \explode(',', App::getEnv('_APP_CONSOLE_WHITELIST_IPS', null)) : [], - 'authWhitelistDomains' => (!empty(App::getEnv('_APP_CONSOLE_WHITELIST_DOMAINS', null))) ? \explode(',', App::getEnv('_APP_CONSOLE_WHITELIST_DOMAINS', null)) : [], + 'usersAuthLimit' => (App::getEnv('_APP_CONSOLE_WHITELIST_ROOT', 'enabled') === 'enabled') ? 1 : 0, // limit signup to 1 user ], Database::SYSTEM_COLLECTION_COLLECTIONS => [ '$collection' => Database::SYSTEM_COLLECTION_COLLECTIONS, diff --git a/app/config/variables.php b/app/config/variables.php index a5f598d80d..57a9131274 100644 --- a/app/config/variables.php +++ b/app/config/variables.php @@ -63,9 +63,17 @@ return [ 'required' => true, 'question' => 'Enter a DNS A record hostname to serve as a CNAME for your custom domains.\nYou can use the same value as used for the Appwrite hostname.', ], + [ + 'name' => '_APP_CONSOLE_WHITELIST_ROOT', + 'description' => 'This option allows you to disable the creation of new users on the Appwrite console. When enabled only 1 user will be able to use the registration form. New users can be added by invting them to your project. By default this option is enabled.', + 'introduction' => '0.8.0', + 'default' => 'enabled', + 'required' => false, + 'question' => '', + ], [ 'name' => '_APP_CONSOLE_WHITELIST_EMAILS', - 'description' => 'This option allows you to limit creation of users to Appwrite console. This option is very useful for small teams or sole developers. To enable it, pass a list of allowed email addresses separated by a comma.', + 'description' => 'This option allows you to limit creation of new users on the Appwrite console. This option is very useful for small teams or sole developers. To enable it, pass a list of allowed email addresses separated by a comma.', 'introduction' => '', 'default' => '', 'required' => false, diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index 2cf9770fc8..294ae817bc 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -61,7 +61,6 @@ App::post('/v1/account') if ('console' === $project->getId()) { $whitlistEmails = $project->getAttribute('authWhitelistEmails'); $whitlistIPs = $project->getAttribute('authWhitelistIPs'); - $whitlistDomains = $project->getAttribute('authWhitelistDomains'); if (!empty($whitlistEmails) && !\in_array($email, $whitlistEmails)) { throw new Exception('Console registration is restricted to specific emails. Contact your administrator for more information.', 401); @@ -70,10 +69,6 @@ App::post('/v1/account') if (!empty($whitlistIPs) && !\in_array($request->getIP(), $whitlistIPs)) { throw new Exception('Console registration is restricted to specific IPs. Contact your administrator for more information.', 401); } - - if (!empty($whitlistDomains) && !\in_array(\substr(\strrchr($email, '@'), 1), $whitlistDomains)) { - throw new Exception('Console registration is restricted to specific domains. Contact your administrator for more information.', 401); - } } $limit = $project->getAttribute('usersAuthLimit', 0); diff --git a/app/controllers/api/health.php b/app/controllers/api/health.php index f18c31ef04..01a9050e0e 100644 --- a/app/controllers/api/health.php +++ b/app/controllers/api/health.php @@ -272,7 +272,7 @@ App::get('/v1/health/anti-virus') App::get('/v1/health/stats') // Currently only used internally ->desc('Get System Stats') ->groups(['api', 'health']) - ->label('scope', 'god') + ->label('scope', 'root') // ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) // ->label('sdk.namespace', 'health') // ->label('sdk.method', 'getStats') diff --git a/app/controllers/api/storage.php b/app/controllers/api/storage.php index 994af027d9..f0c63ce7b6 100644 --- a/app/controllers/api/storage.php +++ b/app/controllers/api/storage.php @@ -612,7 +612,7 @@ App::delete('/v1/storage/files/:fileId') // App::get('/v1/storage/files/:fileId/scan') // ->desc('Scan Storage') // ->groups(['api', 'storage']) -// ->label('scope', 'god') +// ->label('scope', 'root') // ->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT]) // ->label('sdk.namespace', 'storage') // ->label('sdk.method', 'getFileScan') diff --git a/app/controllers/general.php b/app/controllers/general.php index 1f7d4ebaeb..57d1df596b 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -14,8 +14,6 @@ use Appwrite\Database\Database; use Appwrite\Database\Document; use Appwrite\Database\Validator\Authorization; use Appwrite\Network\Validator\Origin; -use Utopia\Storage\Device\Local; -use Utopia\Storage\Storage; use Appwrite\Utopia\Response\Filters\V06; use Utopia\CLI\Console; @@ -23,15 +21,61 @@ Config::setParam('domainVerification', false); Config::setParam('cookieDomain', 'localhost'); Config::setParam('cookieSamesite', Response::COOKIE_SAMESITE_NONE); -App::init(function ($utopia, $request, $response, $console, $project, $user, $locale, $clients) { +App::init(function ($utopia, $request, $response, $console, $project, $consoleDB, $user, $locale, $clients) { /** @var Utopia\Swoole\Request $request */ /** @var Appwrite\Utopia\Response $response */ + /** @var Appwrite\Database\Database $consoleDB */ /** @var Appwrite\Database\Document $console */ /** @var Appwrite\Database\Document $project */ /** @var Appwrite\Database\Document $user */ /** @var Utopia\Locale\Locale $locale */ /** @var bool $mode */ /** @var array $clients */ + + $domain = $request->getHostname(); + $domains = Config::getParam('domains', []); + if (!array_key_exists($domain, $domains)) { + $domain = new Domain(!empty($domain) ? $domain : ''); + + if (empty($domain->get()) || !$domain->isKnown() || $domain->isTest()) { + $domains[$domain->get()] = false; + Console::warning($domain->get() . ' is not a publicly accessible domain. Skipping SSL certificate generation.'); + } else { + Authorization::disable(); + $dbDomain = $consoleDB->getCollectionFirst([ + 'limit' => 1, + 'offset' => 0, + 'filters' => [ + '$collection=' . Database::SYSTEM_COLLECTION_CERTIFICATES, + 'domain=' . $domain->get(), + ], + ]); + + if (empty($dbDomain)) { + $dbDomain = [ + '$collection' => Database::SYSTEM_COLLECTION_CERTIFICATES, + '$permissions' => [ + 'read' => [], + 'write' => [], + ], + 'domain' => $domain->get(), + ]; + $dbDomain = $consoleDB->createDocument($dbDomain); + Authorization::enable(); + + Console::info('Issuing a TLS certificate for the master domain (' . $domain->get() . ') in ~30 seconds..'); // TODO move this to installation script + + ResqueScheduler::enqueueAt(\time() + 30, 'v1-certificates', 'CertificatesV1', [ + 'document' => $dbDomain, + 'domain' => $domain->get(), + 'validateTarget' => false, + 'validateCNAME' => false, + ]); + } + $domains[$domain->get()] = true; + } + Config::setParam('domains', $domains); + } $localeParam = (string)$request->getParam('locale', $request->getHeader('x-appwrite-locale', '')); @@ -208,7 +252,7 @@ App::init(function ($utopia, $request, $response, $console, $project, $user, $lo } }, $user->getAttribute('memberships', [])); - // TDOO Check if user is god + // TDOO Check if user is root if (!\in_array($scope, $scopes)) { if (empty($project->getId()) || Database::SYSTEM_COLLECTION_PROJECTS !== $project->getCollection()) { // Check if permission is denied because project is missing @@ -226,7 +270,7 @@ App::init(function ($utopia, $request, $response, $console, $project, $user, $lo throw new Exception('Password reset is required', 412); } -}, ['utopia', 'request', 'response', 'console', 'project', 'user', 'locale', 'clients']); +}, ['utopia', 'request', 'response', 'console', 'project', 'consoleDB', 'user', 'locale', 'clients']); App::options(function ($request, $response) { /** @var Utopia\Swoole\Request $request */ @@ -424,4 +468,4 @@ include_once __DIR__ . '/shared/web.php'; foreach (Config::getParam('services', []) as $service) { include_once $service['controller']; -} \ No newline at end of file +} diff --git a/app/controllers/web/home.php b/app/controllers/web/home.php index cf3f3fe676..15b10a2f1b 100644 --- a/app/controllers/web/home.php +++ b/app/controllers/web/home.php @@ -1,5 +1,6 @@ label('permission', 'public') ->label('scope', 'home') ->inject('response') - ->action(function ($response) { + ->inject('consoleDB') + ->inject('project') + ->action(function ($response, $consoleDB, $project) { /** @var Appwrite\Utopia\Response $response */ + /** @var Appwrite\Database\Database $consoleDB */ + /** @var Appwrite\Database\Document $project */ - $response->redirect('/auth/signin'); + $response + ->addHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0') + ->addHeader('Expires', 0) + ->addHeader('Pragma', 'no-cache') + ; + + if ('console' === $project->getId() || $project->isEmpty()) { + $whitlistRoot = App::getEnv('_APP_CONSOLE_WHITELIST_ROOT', 'enabled'); + + if($whitlistRoot !== 'disabled') { + $consoleDB->getCollection([ // Count users + 'filters' => [ + '$collection='.Database::SYSTEM_COLLECTION_USERS, + ], + ]); + + $sum = $consoleDB->getSum(); + + if($sum !== 0) { + return $response->redirect('/auth/signin'); + } + } + } + + $response->redirect('/auth/signup'); }); App::get('/auth/signin') @@ -58,6 +87,10 @@ App::get('/auth/signin') $page = new View(__DIR__.'/../../views/home/auth/signin.phtml'); + $page + ->setParam('root', App::getEnv('_APP_CONSOLE_WHITELIST_ROOT', 'enabled')) + ; + $layout ->setParam('title', 'Sign In - '.APP_NAME) ->setParam('body', $page); @@ -72,6 +105,10 @@ App::get('/auth/signup') /** @var Utopia\View $layout */ $page = new View(__DIR__.'/../../views/home/auth/signup.phtml'); + $page + ->setParam('root', App::getEnv('_APP_CONSOLE_WHITELIST_ROOT', 'enabled')) + ; + $layout ->setParam('title', 'Sign Up - '.APP_NAME) ->setParam('body', $page); @@ -87,6 +124,10 @@ App::get('/auth/recovery') $page = new View(__DIR__.'/../../views/home/auth/recovery.phtml'); + $page + ->setParam('smtpEnabled', (!empty(App::getEnv('_APP_SMTP_HOST')))) + ; + $layout ->setParam('title', 'Password Recovery - '.APP_NAME) ->setParam('body', $page); diff --git a/app/http.php b/app/http.php index d144c2e54a..efa47ffbd1 100644 --- a/app/http.php +++ b/app/http.php @@ -12,6 +12,8 @@ use Swoole\Http\Request as SwooleRequest; use Swoole\Http\Response as SwooleResponse; use Utopia\App; use Utopia\CLI\Console; +use Utopia\Config\Config; +use Utopia\Domains\Domain; // xdebug_start_trace('/tmp/trace'); @@ -65,18 +67,6 @@ Files::load(__DIR__ . '/../public'); include __DIR__ . '/controllers/general.php'; -$domain = App::getEnv('_APP_DOMAIN', ''); - -Console::info('Issuing a TLS certificate for the master domain ('.$domain.') in 30 seconds. - Make sure your domain points to your server IP or restart your Appwrite server to try again.'); // TODO move this to installation script - -ResqueScheduler::enqueueAt(\time() + 30, 'v1-certificates', 'CertificatesV1', [ - 'document' => [], - 'domain' => $domain, - 'validateTarget' => false, - 'validateCNAME' => false, -]); - $http->on('request', function (SwooleRequest $swooleRequest, SwooleResponse $swooleResponse) { $request = new Request($swooleRequest); $response = new Response($swooleResponse); diff --git a/app/tasks/doctor.php b/app/tasks/doctor.php index 0f601bb5b5..095adfb951 100644 --- a/app/tasks/doctor.php +++ b/app/tasks/doctor.php @@ -61,12 +61,12 @@ $cli Console::log('🟢 Abuse protection is enabled'); } + $authWhitelistRoot = App::getEnv('_APP_CONSOLE_WHITELIST_ROOT', null); $authWhitelistEmails = App::getEnv('_APP_CONSOLE_WHITELIST_EMAILS', null); $authWhitelistIPs = App::getEnv('_APP_CONSOLE_WHITELIST_IPS', null); - $authWhitelistDomains = App::getEnv('_APP_CONSOLE_WHITELIST_DOMAINS', null); - if(empty($authWhitelistEmails) - && empty($authWhitelistDomains) + if(empty($authWhitelistRoot) + && empty($authWhitelistEmails) && empty($authWhitelistIPs) ) { Console::log('🔴 Console access limits are disabled'); diff --git a/app/views/console/comps/footer.phtml b/app/views/console/comps/footer.phtml index be5696ce48..065dabb14b 100644 --- a/app/views/console/comps/footer.phtml +++ b/app/views/console/comps/footer.phtml @@ -1,6 +1,6 @@ getParam('home', ''); -$version = $this->getParam('version', '').'.'.APP_CACHE_BUSTER; +$version = $this->getParam('version', '') . '.' . APP_CACHE_BUSTER; ?>