Merge branch '1.5.x' of https://github.com/appwrite/appwrite into feat-mfa

This commit is contained in:
Torsten Dittmann
2024-02-02 13:30:11 +01:00
22 changed files with 92 additions and 59 deletions
+1
View File
@@ -99,3 +99,4 @@ _APP_ASSISTANT_OPENAI_API_KEY=
_APP_MESSAGE_SMS_TEST_DSN=
_APP_MESSAGE_EMAIL_TEST_DSN=
_APP_MESSAGE_PUSH_TEST_DSN=
_APP_WEBHOOK_MAX_FAILED_ATTEMPTS=10
+1 -1
View File
@@ -4,8 +4,8 @@
* List of server wide error codes and their respective messages.
*/
use Appwrite\Enum\MessageStatus;
use Appwrite\Extend\Exception;
use Appwrite\Messaging\Status as MessageStatus;
return [
/** General Errors */
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+20 -28
View File
@@ -11,7 +11,7 @@ use Appwrite\Auth\Validator\Phone;
use Appwrite\Detector\Detector;
use Appwrite\Event\Event;
use Appwrite\Event\Mail;
use Appwrite\Auth\SecurityPhrase;
use Appwrite\Auth\Phrase;
use Appwrite\Extend\Exception;
use Appwrite\Network\Validator\Email;
use Utopia\Validator\Host;
@@ -468,8 +468,6 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
->label('abuse-limit', 50)
->label('abuse-key', 'ip:{ip}')
->label('docs', false)
->label('usage.metric', 'sessions.{scope}.requests.create')
->label('usage.params', ['provider:{request.provider}'])
->param('provider', '', new WhiteList(\array_keys(Config::getParam('oAuthProviders')), true), 'OAuth2 provider.')
->param('code', '', new Text(2048, 0), 'OAuth2 code. This is a temporary code that the will be later exchanged for an access token.', true)
->param('state', '', new Text(2048), 'OAuth2 state params.', true)
@@ -895,7 +893,6 @@ App::get('/v1/account/identities')
->desc('List Identities')
->groups(['api', 'account'])
->label('scope', 'accounts.read')
->label('usage.metric', 'users.{scope}.requests.read')
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT])
->label('sdk.namespace', 'account')
->label('sdk.method', 'listIdentities')
@@ -999,7 +996,7 @@ App::post('/v1/account/tokens/magic-url')
->param('userId', '', new CustomId(), 'User ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
->param('email', '', new Email(), 'User email.')
->param('url', '', fn($clients) => new Host($clients), 'URL to redirect the user back to your app from the magic URL login. Only URLs from hostnames in your project platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.', true, ['clients'])
->param('securityPhrase', false, new Boolean(), 'Toggle for security phrase. If enabled, email will be send with a randomly generated phrase and the phrase will also be included in the response. Confirming phrases match increases the security of authentication flow.', true)
->param('phrase', false, new Boolean(), 'Toggle for security phrase. If enabled, email will be send with a randomly generated phrase and the phrase will also be included in the response. Confirming phrases match increases the security of your authentication flow.', true)
->inject('request')
->inject('response')
->inject('user')
@@ -1008,14 +1005,14 @@ App::post('/v1/account/tokens/magic-url')
->inject('locale')
->inject('queueForEvents')
->inject('queueForMails')
->action(function (string $userId, string $email, string $url, bool $securityPhrase, Request $request, Response $response, Document $user, Document $project, Database $dbForProject, Locale $locale, Event $queueForEvents, Mail $queueForMails) {
->action(function (string $userId, string $email, string $url, bool $phrase, Request $request, Response $response, Document $user, Document $project, Database $dbForProject, Locale $locale, Event $queueForEvents, Mail $queueForMails) {
if (empty(App::getEnv('_APP_SMTP_HOST'))) {
throw new Exception(Exception::GENERAL_SMTP_DISABLED, 'SMTP disabled');
}
if ($securityPhrase === true) {
$securityPhrase = SecurityPhrase::generate();
if ($phrase === true) {
$phrase = Phrase::generate();
}
$roles = Authorization::getRoles();
@@ -1127,7 +1124,7 @@ App::post('/v1/account/tokens/magic-url')
->setParam('{{thanks}}', $locale->getText("emails.magicSession.thanks"))
->setParam('{{signature}}', $locale->getText("emails.magicSession.signature"));
if (!empty($securityPhrase)) {
if (!empty($phrase)) {
$message->setParam('{{securityPhrase}}', $locale->getText("emails.magicSession.securityPhrase"));
} else {
$message->setParam('{{securityPhrase}}', '');
@@ -1191,7 +1188,7 @@ App::post('/v1/account/tokens/magic-url')
'agentDevice' => '<strong>' . ( $agentDevice['deviceBrand'] ?? $agentDevice['deviceBrand'] ?? 'UNKNOWN') . '</strong>',
'agentClient' => '<strong>' . ($agentClient['clientName'] ?? 'UNKNOWN') . '</strong>',
'agentOs' => '<strong>' . ($agentOs['osName'] ?? 'UNKNOWN') . '</strong>',
'phrase' => '<strong>' . (!empty($securityPhrase) ? $securityPhrase : '') . '</strong>'
'phrase' => '<strong>' . (!empty($phrase) ? $phrase : '') . '</strong>'
];
$queueForMails
@@ -1211,8 +1208,8 @@ App::post('/v1/account/tokens/magic-url')
// Hide secret for clients
$token->setAttribute('secret', ($isPrivilegedUser || $isAppUser) ? $tokenSecret : '');
if (!empty($securityPhrase)) {
$token->setAttribute('securityPhrase', $securityPhrase);
if (!empty($phrase)) {
$token->setAttribute('phrase', $phrase);
}
$response
@@ -1240,7 +1237,7 @@ App::post('/v1/account/tokens/email')
->label('abuse-key', 'url:{url},email:{param-email}')
->param('userId', '', new CustomId(), 'User ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
->param('email', '', new Email(), 'User email.')
->param('securityPhrase', false, new Boolean(), 'Toggle for security phrase. If enabled, email will be send with a randomly generated phrase and the phrase will also be included in the response. Confirming phrases match increases the security of authentication flow.', true)
->param('phrase', false, new Boolean(), 'Toggle for security phrase. If enabled, email will be send with a randomly generated phrase and the phrase will also be included in the response. Confirming phrases match increases the security of your authentication flow.', true)
->inject('request')
->inject('response')
->inject('user')
@@ -1249,13 +1246,13 @@ App::post('/v1/account/tokens/email')
->inject('locale')
->inject('queueForEvents')
->inject('queueForMails')
->action(function (string $userId, string $email, bool $securityPhrase, Request $request, Response $response, Document $user, Document $project, Database $dbForProject, Locale $locale, Event $queueForEvents, Mail $queueForMails) {
->action(function (string $userId, string $email, bool $phrase, Request $request, Response $response, Document $user, Document $project, Database $dbForProject, Locale $locale, Event $queueForEvents, Mail $queueForMails) {
if (empty(App::getEnv('_APP_SMTP_HOST'))) {
throw new Exception(Exception::GENERAL_SMTP_DISABLED, 'SMTP disabled');
}
if ($securityPhrase === true) {
$securityPhrase = SecurityPhrase::generate();
if ($phrase === true) {
$phrase = Phrase::generate();
}
$roles = Authorization::getRoles();
@@ -1355,7 +1352,7 @@ App::post('/v1/account/tokens/email')
->setParam('{{thanks}}', $locale->getText("emails.otpSession.thanks"))
->setParam('{{signature}}', $locale->getText("emails.otpSession.signature"));
if (!empty($securityPhrase)) {
if (!empty($phrase)) {
$message->setParam('{{securityPhrase}}', $locale->getText("emails.otpSession.securityPhrase"));
} else {
$message->setParam('{{securityPhrase}}', '');
@@ -1419,7 +1416,7 @@ App::post('/v1/account/tokens/email')
'agentDevice' => '<strong>' . ( $agentDevice['deviceBrand'] ?? $agentDevice['deviceBrand'] ?? 'UNKNOWN') . '</strong>',
'agentClient' => '<strong>' . ($agentClient['clientName'] ?? 'UNKNOWN') . '</strong>',
'agentOs' => '<strong>' . ($agentOs['osName'] ?? 'UNKNOWN') . '</strong>',
'phrase' => '<strong>' . (!empty($securityPhrase) ? $securityPhrase : '') . '</strong>'
'phrase' => '<strong>' . (!empty($phrase) ? $phrase : '') . '</strong>'
];
$queueForMails
@@ -1439,8 +1436,8 @@ App::post('/v1/account/tokens/email')
// Hide secret for clients
$token->setAttribute('secret', ($isPrivilegedUser || $isAppUser) ? $tokenSecret : '');
if (!empty($securityPhrase)) {
$token->setAttribute('securityPhrase', $securityPhrase);
if (!empty($phrase)) {
$token->setAttribute('phrase', $phrase);
}
$response
@@ -1565,7 +1562,6 @@ App::put('/v1/account/sessions/magic-url')
->label('audits.event', 'session.create')
->label('audits.resource', 'user/{response.userId}')
->label('audits.userId', '{response.userId}')
->label('usage.metric', 'sessions.{scope}.requests.create')
->label('sdk.auth', [])
->label('sdk.namespace', 'account')
->label('sdk.method', ['updateMagicURLSession', 'updatePhoneSession'])
@@ -1595,7 +1591,6 @@ App::post('/v1/account/sessions/token')
->label('audits.event', 'session.create')
->label('audits.resource', 'user/{response.userId}')
->label('audits.userId', '{response.userId}')
->label('usage.metric', 'sessions.{scope}.requests.create')
->label('sdk.auth', [])
->label('sdk.namespace', 'account')
->label('sdk.method', 'createSession')
@@ -2046,7 +2041,6 @@ App::get('/v1/account')
->desc('Get account')
->groups(['api', 'account'])
->label('scope', 'accounts.read')
->label('usage.metric', 'users.{scope}.requests.read')
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT])
->label('sdk.namespace', 'account')
->label('sdk.method', 'get')
@@ -2059,6 +2053,9 @@ App::get('/v1/account')
->inject('response')
->inject('user')
->action(function (Response $response, Document $user) {
if ($user->isEmpty()) {
throw new Exception(Exception::USER_NOT_FOUND);
}
$response->dynamic($user, Response::MODEL_ACCOUNT);
});
@@ -2067,7 +2064,6 @@ App::get('/v1/account/prefs')
->desc('Get account preferences')
->groups(['api', 'account'])
->label('scope', 'accounts.read')
->label('usage.metric', 'users.{scope}.requests.read')
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT])
->label('sdk.namespace', 'account')
->label('sdk.method', 'getPrefs')
@@ -2090,7 +2086,6 @@ App::get('/v1/account/sessions')
->desc('List sessions')
->groups(['api', 'account'])
->label('scope', 'accounts.read')
->label('usage.metric', 'users.{scope}.requests.read')
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT])
->label('sdk.namespace', 'account')
->label('sdk.method', 'listSessions')
@@ -2126,7 +2121,6 @@ App::get('/v1/account/logs')
->desc('List logs')
->groups(['api', 'account'])
->label('scope', 'accounts.read')
->label('usage.metric', 'users.{scope}.requests.read')
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT])
->label('sdk.namespace', 'account')
->label('sdk.method', 'listLogs')
@@ -2187,7 +2181,6 @@ App::get('/v1/account/sessions/:sessionId')
->desc('Get session')
->groups(['api', 'account'])
->label('scope', 'accounts.read')
->label('usage.metric', 'users.{scope}.requests.read')
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT])
->label('sdk.namespace', 'account')
->label('sdk.method', 'getSession')
@@ -3883,7 +3876,6 @@ App::delete('/v1/account')
->label('scope', 'accounts.write')
->label('audits.event', 'user.delete')
->label('audits.resource', 'user/{response.$id}')
->label('usage.metric', 'users.{scope}.requests.delete')
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
->label('sdk.namespace', 'account')
->label('sdk.method', 'delete')
+1 -1
View File
@@ -2,11 +2,11 @@
use Appwrite\Auth\Validator\Phone;
use Appwrite\Detector\Detector;
use Appwrite\Enum\MessageStatus;
use Appwrite\Event\Delete;
use Appwrite\Event\Event;
use Appwrite\Event\Messaging;
use Appwrite\Extend\Exception;
use Appwrite\Messaging\Status as MessageStatus;
use Appwrite\Network\Validator\Email;
use Appwrite\Permission;
use Appwrite\Role;
-1
View File
@@ -1620,7 +1620,6 @@ App::post('/v1/users/:userId/tokens')
->label('scope', 'users.write')
->label('audits.event', 'tokens.create')
->label('audits.resource', 'user/{request.userId}')
->label('usage.metric', 'tokens.requests.create')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'users')
->label('sdk.method', 'createToken')
+3 -3
View File
@@ -15,7 +15,7 @@ $image = $this->getParam('image', '');
services:
traefik:
image: traefik:2.9
image: traefik:2.11
container_name: appwrite-traefik
<<: *x-logging
command:
@@ -735,7 +735,7 @@ services:
- OPR_EXECUTOR_STORAGE_WASABI_BUCKET=$_APP_STORAGE_WASABI_BUCKET
mariadb:
image: mariadb:10.7 # fix issues when upgrading using: mysql_upgrade -u root -p
image: mariadb:10.11 # fix issues when upgrading using: mysql_upgrade -u root -p
container_name: appwrite-mariadb
<<: *x-logging
restart: unless-stopped
@@ -751,7 +751,7 @@ services:
command: 'mysqld --innodb-flush-method=fsync'
redis:
image: redis:7.0.4-alpine
image: redis:7.2.4-alpine
container_name: appwrite-redis
<<: *x-logging
restart: unless-stopped
+3 -2
View File
@@ -14,7 +14,7 @@ version: '3'
services:
traefik:
image: traefik:2.9
image: traefik:2.11
<<: *x-logging
container_name: appwrite-traefik
command:
@@ -297,6 +297,7 @@ services:
- _APP_REDIS_PASS
- _APP_LOGGING_PROVIDER
- _APP_LOGGING_CONFIG
- _APP_WEBHOOK_MAX_FAILED_ATTEMPTS
appwrite-worker-deletes:
entrypoint: worker-deletes
@@ -915,7 +916,7 @@ services:
# - SMARTHOST_PORT=587
redis:
image: redis:7.0.4-alpine
image: redis:7.2.4-alpine
<<: *x-logging
container_name: appwrite-redis
command: >
@@ -2,7 +2,7 @@
namespace Appwrite\Auth;
class SecurityPhrase
class Phrase
{
public static function generate(): string
{
@@ -1,8 +1,8 @@
<?php
namespace Appwrite\Enum;
namespace Appwrite\Messaging;
class MessageStatus
class Status
{
/**
* Message that is not ready to be sent
+1 -1
View File
@@ -2,8 +2,8 @@
namespace Appwrite\Platform\Workers;
use Appwrite\Enum\MessageStatus;
use Appwrite\Extend\Exception;
use Appwrite\Messaging\Status as MessageStatus;
use Utopia\App;
use Utopia\CLI\Console;
use Utopia\Database\Helpers\ID;
+1 -2
View File
@@ -16,7 +16,6 @@ use Utopia\Queue\Message;
class Webhooks extends Action
{
private array $errors = [];
private const MAX_FAILED_ATTEMPTS = 10;
private const MAX_FILE_SIZE = 5242880; // 5 MB
public static function getName(): string
@@ -157,7 +156,7 @@ class Webhooks extends Action
$webhook->setAttribute('logs', $logs);
if ($attempts >= self::MAX_FAILED_ATTEMPTS) {
if ($attempts >= \intval(App::getEnv('_APP_WEBHOOK_MAX_FAILED_ATTEMPTS', '10'))) {
$webhook->setAttribute('enabled', false);
$this->sendEmailAlert($attempts, $statusCode, $webhook, $project, $dbForConsole, $queueForMails);
}
+1 -1
View File
@@ -40,7 +40,7 @@ class Token extends Model
'default' => '',
'example' => self::TYPE_DATETIME_EXAMPLE,
])
->addRule('securityPhrase', [
->addRule('phrase', [
'type' => self::TYPE_STRING,
'description' => 'Security phrase of a token. Empty if security phrase was not requested when creating a token. It includes randomly generated phrase which is also sent in the external resource such as email.',
'default' => '',
+5 -5
View File
@@ -148,7 +148,7 @@ trait AccountBase
$this->assertNotEmpty($response['body']['userId']);
$this->assertNotEmpty($response['body']['expire']);
$this->assertEmpty($response['body']['secret']);
$this->assertEmpty($response['body']['securityPhrase']);
$this->assertEmpty($response['body']['phrase']);
$userId = $response['body']['userId'];
@@ -208,21 +208,21 @@ trait AccountBase
]), [
'userId' => ID::unique(),
'email' => 'otpuser@appwrite.io',
'securityPhrase' => true
'phrase' => true
]);
$this->assertEquals($response['headers']['status-code'], 201);
$this->assertNotEmpty($response['body']['securityPhrase']);
$this->assertNotEmpty($response['body']['phrase']);
$this->assertEmpty($response['body']['secret']);
$this->assertEquals($userId, $response['body']['userId']);
$securityPhrase = $response['body']['securityPhrase'];
$phrase = $response['body']['phrase'];
$lastEmail = $this->getLastEmail();
$this->assertEquals('otpuser@appwrite.io', $lastEmail['to'][0]['address']);
$this->assertEquals('OTP for ' . $this->getProject()['name'] . ' Login', $lastEmail['subject']);
$this->assertStringContainsStringIgnoringCase('security phrase', $lastEmail['text']);
$this->assertStringContainsStringIgnoringCase($securityPhrase, $lastEmail['text']);
$this->assertStringContainsStringIgnoringCase($phrase, $lastEmail['text']);
$response = $this->client->call(Client::METHOD_POST, '/account/tokens/email', array_merge([
'origin' => 'http://localhost',
@@ -2331,7 +2331,7 @@ class AccountCustomClientTest extends Scope
$this->assertEquals(201, $response['headers']['status-code']);
$this->assertNotEmpty($response['body']['$id']);
$this->assertEmpty($response['body']['secret']);
$this->assertEmpty($response['body']['securityPhrase']);
$this->assertEmpty($response['body']['phrase']);
$this->assertEquals(true, (new DatetimeValidator())->isValid($response['body']['expire']));
$userId = $response['body']['userId'];
@@ -2399,15 +2399,15 @@ class AccountCustomClientTest extends Scope
]), [
'userId' => ID::unique(),
'email' => $email,
'securityPhrase' => true
'phrase' => true
]);
$this->assertEquals(201, $response['headers']['status-code']);
$this->assertNotEmpty($response['body']['$id']);
$this->assertNotEmpty($response['body']['securityPhrase']);
$this->assertNotEmpty($response['body']['phrase']);
$lastEmail = $this->getLastEmail();
$this->assertStringContainsStringIgnoringCase($response['body']['securityPhrase'], $lastEmail['text']);
$this->assertStringContainsStringIgnoringCase($response['body']['phrase'], $lastEmail['text']);
$data['token'] = $token;
$data['id'] = $userId;
@@ -120,6 +120,47 @@ class AccountCustomServerTest extends Scope
]);
}
/**
* @depends testCreateAccountSession
*/
public function testGetAccount($data): array
{
$email = $data['email'] ?? '';
$name = $data['name'] ?? '';
$session = $data['session'] ?? '';
/**
* Test for SUCCESS
*/
$response = $this->client->call(Client::METHOD_GET, '/account', array_merge([
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-session' => $session,
]));
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertNotEmpty($response['body']);
$this->assertNotEmpty($response['body']['$id']);
$this->assertEquals(true, (new DatetimeValidator())->isValid($response['body']['registration']));
$this->assertEquals($response['body']['email'], $email);
$this->assertEquals($response['body']['name'], $name);
$this->assertArrayHasKey('accessedAt', $response['body']);
$this->assertNotEmpty($response['body']['accessedAt']);
/**
* Test for FAILURE
*/
$response = $this->client->call(Client::METHOD_GET, '/account', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(404, $response['headers']['status-code']);
return $data;
}
public function testCreateAnonymousAccount()
{
/**
@@ -2,7 +2,7 @@
namespace Tests\E2E\Services\Messaging;
use Appwrite\Enum\MessageStatus;
use Appwrite\Messaging\Status as MessageStatus;
use Tests\E2E\Client;
use Utopia\App;
use Utopia\Database\Helpers\ID;