From 4f11e171db3c4d4199722db20d58c20762cbc3b6 Mon Sep 17 00:00:00 2001 From: Eldad Fux Date: Sat, 21 Mar 2020 23:10:06 +0200 Subject: [PATCH] Fixed document not being unique --- CHANGES.md | 7 ++++ app/controllers/api/account.php | 63 ++++++++++++++++------------ app/controllers/api/teams.php | 37 +++++++++------- app/controllers/api/users.php | 35 +++++++++------- app/db/SQL/all.sql | 13 ++++-- app/init.php | 2 +- app/tasks/upgrade.php | 9 ++++ docker-compose.yml | 2 +- src/Database/Adapter.php | 2 +- src/Database/Adapter/MySQL.php | 20 ++++++++- src/Database/Adapter/Redis.php | 4 +- src/Database/Database.php | 4 +- src/Database/Exception/Duplicate.php | 7 ++++ 13 files changed, 136 insertions(+), 69 deletions(-) create mode 100644 src/Database/Exception/Duplicate.php diff --git a/CHANGES.md b/CHANGES.md index 277f3d397..b57ae948b 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,10 @@ +# Version 0.5.3 (PRE-RELEASE) + +## Bug Fixes + +* Fixed bug where multiple unique attribute were allowed +* Blocked forms from being submitted unlimited times + # Version 0.5.2 (PRE-RELEASE) ## Bug Fixes diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index 46a151e7e..4ffdd752c 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -18,6 +18,7 @@ use Auth\Auth; use Auth\Validator\Password; use Database\Database; use Database\Document; +use Database\Exception\Duplicate; use Database\Validator\UID; use Database\Validator\Authorization; use DeviceDetector\DeviceDetector; @@ -88,21 +89,25 @@ $utopia->post('/v1/account') Authorization::disable(); - $user = $projectDB->createDocument([ - '$collection' => Database::SYSTEM_COLLECTION_USERS, - '$permissions' => [ - 'read' => ['*'], - 'write' => ['user:{self}'], - ], - 'email' => $email, - 'emailVerification' => false, - 'status' => Auth::USER_STATUS_UNACTIVATED, - 'password' => Auth::passwordHash($password), - 'password-update' => time(), - 'registration' => time(), - 'reset' => false, - 'name' => $name, - ]); + try { + $user = $projectDB->createDocument([ + '$collection' => Database::SYSTEM_COLLECTION_USERS, + '$permissions' => [ + 'read' => ['*'], + 'write' => ['user:{self}'], + ], + 'email' => $email, + 'emailVerification' => false, + 'status' => Auth::USER_STATUS_UNACTIVATED, + 'password' => Auth::passwordHash($password), + 'password-update' => time(), + 'registration' => time(), + 'reset' => false, + 'name' => $name, + ], ['email' => $email]); + } catch (Duplicate $th) { + throw new Exception('Account already exists', 409); + } Authorization::enable(); @@ -391,18 +396,22 @@ $utopia->get('/v1/account/sessions/oauth2/:provider/redirect') if (!$user || empty($user->getId())) { // Last option -> create user alone, generate random password Authorization::disable(); - $user = $projectDB->createDocument([ - '$collection' => Database::SYSTEM_COLLECTION_USERS, - '$permissions' => ['read' => ['*'], 'write' => ['user:{self}']], - 'email' => $email, - 'emailVerification' => true, - 'status' => Auth::USER_STATUS_ACTIVATED, // Email should already be authenticated by OAuth2 provider - 'password' => Auth::passwordHash(Auth::passwordGenerator()), - 'password-update' => time(), - 'registration' => time(), - 'reset' => false, - 'name' => $name, - ]); + try { + $user = $projectDB->createDocument([ + '$collection' => Database::SYSTEM_COLLECTION_USERS, + '$permissions' => ['read' => ['*'], 'write' => ['user:{self}']], + 'email' => $email, + 'emailVerification' => true, + 'status' => Auth::USER_STATUS_ACTIVATED, // Email should already be authenticated by OAuth2 provider + 'password' => Auth::passwordHash(Auth::passwordGenerator()), + 'password-update' => time(), + 'registration' => time(), + 'reset' => false, + 'name' => $name, + ], ['email' => $email]); + } catch (Duplicate $th) { + throw new Exception('Account already exists', 409); + } Authorization::enable(); diff --git a/app/controllers/api/teams.php b/app/controllers/api/teams.php index 6aaf64ab1..f7c402080 100644 --- a/app/controllers/api/teams.php +++ b/app/controllers/api/teams.php @@ -15,6 +15,7 @@ use Database\Database; use Database\Document; use Database\Validator\UID; use Database\Validator\Authorization; +use Database\Exception\Duplicate; use Template\Template; use Auth\Auth; @@ -243,22 +244,26 @@ $utopia->post('/v1/teams/:teamId/memberships') Authorization::disable(); - $invitee = $projectDB->createDocument([ - '$collection' => Database::SYSTEM_COLLECTION_USERS, - '$permissions' => [ - 'read' => ['user:{self}', '*'], - 'write' => ['user:{self}'], - ], - 'email' => $email, - 'emailVerification' => false, - 'status' => Auth::USER_STATUS_UNACTIVATED, - 'password' => Auth::passwordHash(Auth::passwordGenerator()), - 'password-update' => time(), - 'registration' => time(), - 'reset' => false, - 'name' => $name, - 'tokens' => [], - ]); + try { + $invitee = $projectDB->createDocument([ + '$collection' => Database::SYSTEM_COLLECTION_USERS, + '$permissions' => [ + 'read' => ['user:{self}', '*'], + 'write' => ['user:{self}'], + ], + 'email' => $email, + 'emailVerification' => false, + 'status' => Auth::USER_STATUS_UNACTIVATED, + 'password' => Auth::passwordHash(Auth::passwordGenerator()), + 'password-update' => time(), + 'registration' => time(), + 'reset' => false, + 'name' => $name, + 'tokens' => [], + ], ['email' => $email]); + } catch (Duplicate $th) { + throw new Exception('Account already exists', 409); + } Authorization::reset(); diff --git a/app/controllers/api/users.php b/app/controllers/api/users.php index a48cfb8e7..a38b545f0 100644 --- a/app/controllers/api/users.php +++ b/app/controllers/api/users.php @@ -15,6 +15,7 @@ use Utopia\Audit\Audit; use Utopia\Audit\Adapters\MySQL as AuditAdapter; use Utopia\Locale\Locale; use Database\Database; +use Database\Exception\Duplicate; use Database\Validator\UID; use DeviceDetector\DeviceDetector; use GeoIp2\Database\Reader; @@ -46,21 +47,25 @@ $utopia->post('/v1/users') throw new Exception('User already registered', 409); } - $user = $projectDB->createDocument([ - '$collection' => Database::SYSTEM_COLLECTION_USERS, - '$permissions' => [ - 'read' => ['*'], - 'write' => ['user:{self}'], - ], - 'email' => $email, - 'emailVerification' => false, - 'status' => Auth::USER_STATUS_UNACTIVATED, - 'password' => Auth::passwordHash($password), - 'password-update' => time(), - 'registration' => time(), - 'reset' => false, - 'name' => $name, - ]); + try { + $user = $projectDB->createDocument([ + '$collection' => Database::SYSTEM_COLLECTION_USERS, + '$permissions' => [ + 'read' => ['*'], + 'write' => ['user:{self}'], + ], + 'email' => $email, + 'emailVerification' => false, + 'status' => Auth::USER_STATUS_UNACTIVATED, + 'password' => Auth::passwordHash($password), + 'password-update' => time(), + 'registration' => time(), + 'reset' => false, + 'name' => $name, + ], ['email' => $email]); + } catch (Duplicate $th) { + throw new Exception('Account already exists', 409); + } $oauth2Keys = []; diff --git a/app/db/SQL/all.sql b/app/db/SQL/all.sql index 487044cbd..63ec92f42 100644 --- a/app/db/SQL/all.sql +++ b/app/db/SQL/all.sql @@ -5,7 +5,7 @@ USE `appwrite`; CREATE TABLE IF NOT EXISTS `template.abuse.abuse` ( `id` int(11) NOT NULL AUTO_INCREMENT, `_key` varchar(255) NOT NULL, - `_time` varchar(45) NOT NULL, + `_time` int(11) NOT NULL, `_count` int(11) NOT NULL DEFAULT '0', PRIMARY KEY (`id`), UNIQUE KEY `unique1` (`_key`,`_time`), @@ -15,7 +15,6 @@ CREATE TABLE IF NOT EXISTS `template.abuse.abuse` ( CREATE TABLE IF NOT EXISTS `template.audit.audit` ( `id` int(11) NOT NULL AUTO_INCREMENT, `userId` varchar(45) NOT NULL, - `userType` int(11) NOT NULL, `event` varchar(45) NOT NULL, `resource` varchar(45) DEFAULT NULL, `userAgent` text NOT NULL, @@ -25,7 +24,7 @@ CREATE TABLE IF NOT EXISTS `template.audit.audit` ( `data` longtext DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `id_UNIQUE` (`id`), - KEY `index_1` (`userId`,`userType`), + KEY `index_1` (`userId`), KEY `index_2` (`event`), KEY `index_3` (`resource`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; @@ -74,10 +73,18 @@ CREATE TABLE IF NOT EXISTS `template.database.relationships` ( KEY `relationships_end_nodes_id_idx` (`end`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +CREATE TABLE IF NOT EXISTS `template.database.unique` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `key` varchar(128) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `index1` (`key`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + /* Default App */ CREATE TABLE IF NOT EXISTS `app_console.database.documents` LIKE `template.database.documents`; CREATE TABLE IF NOT EXISTS `app_console.database.properties` LIKE `template.database.properties`; CREATE TABLE IF NOT EXISTS `app_console.database.relationships` LIKE `template.database.relationships`; +CREATE TABLE IF NOT EXISTS `app_console.database.unique` LIKE `template.database.unique`; CREATE TABLE IF NOT EXISTS `app_console.audit.audit` LIKE `template.audit.audit`; CREATE TABLE IF NOT EXISTS `app_console.abuse.abuse` LIKE `template.abuse.abuse`; \ No newline at end of file diff --git a/app/init.php b/app/init.php index a8b574323..59ac3310e 100644 --- a/app/init.php +++ b/app/init.php @@ -26,7 +26,7 @@ const APP_USERAGENT = APP_NAME.'-Server v%s. Please report abuse at %s'; const APP_MODE_ADMIN = 'admin'; const APP_PAGING_LIMIT = 15; const APP_CACHE_BUSTER = 55; -const APP_VERSION_STABLE = '0.5.2'; +const APP_VERSION_STABLE = '0.5.3'; const APP_STORAGE_UPLOADS = '/storage/uploads'; const APP_STORAGE_CACHE = '/storage/cache'; const APP_STORAGE_CERTIFICATES = '/storage/certificates'; diff --git a/app/tasks/upgrade.php b/app/tasks/upgrade.php index d00854bee..3652ae824 100644 --- a/app/tasks/upgrade.php +++ b/app/tasks/upgrade.php @@ -68,6 +68,15 @@ $callbacks = [ try { $statement = $db->prepare(" + + CREATE TABLE IF NOT EXISTS `template.database.unique` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `key` varchar(128) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `index1` (`key`) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + + CREATE TABLE IF NOT EXISTS `{$schema}`.`app_{$project->getId()}.database.unique` LIKE `template.database.unique`; ALTER TABLE `{$schema}`.`app_{$project->getId()}.audit.audit` DROP COLUMN IF EXISTS `userType`; ALTER TABLE `{$schema}`.`app_{$project->getId()}.audit.audit` DROP INDEX IF EXISTS `index_1`; ALTER TABLE `{$schema}`.`app_{$project->getId()}.audit.audit` ADD INDEX IF NOT EXISTS `index_1` (`userId` ASC); diff --git a/docker-compose.yml b/docker-compose.yml index b3048a619..73a912f7f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -84,7 +84,7 @@ services: - _APP_SMTP_PORT=25 mariadb: - image: appwrite/mariadb:1.0.2 # fix issues when upgrading using: mysql_upgrade -u root -p + image: appwrite/mariadb:1.0.3 # fix issues when upgrading using: mysql_upgrade -u root -p restart: unless-stopped networks: - appwrite diff --git a/src/Database/Adapter.php b/src/Database/Adapter.php index b61556e12..12f649ce1 100644 --- a/src/Database/Adapter.php +++ b/src/Database/Adapter.php @@ -68,7 +68,7 @@ abstract class Adapter * * @return array */ - abstract public function createDocument(array $data); + abstract public function createDocument(array $data, array $unique); /** * Update Document. diff --git a/src/Database/Adapter/MySQL.php b/src/Database/Adapter/MySQL.php index 5ad70a5ba..39f01d9cf 100644 --- a/src/Database/Adapter/MySQL.php +++ b/src/Database/Adapter/MySQL.php @@ -7,6 +7,7 @@ use Exception; use PDO; use Redis as Client; use Database\Adapter; +use Database\Exception\Duplicate; use Database\Validator\Authorization; class MySQL extends Adapter @@ -150,7 +151,7 @@ class MySQL extends Adapter * * @return array */ - public function createDocument(array $data = []) + public function createDocument(array $data = [], array $unique = []) { $order = 0; $data = array_merge(['$id' => null, '$permissions' => []], $data); // Merge data with default params @@ -178,6 +179,21 @@ class MySQL extends Adapter } } + /** + * Check Unique Keys + */ + foreach($unique as $key => $value) { + $st = $this->getPDO()->prepare('INSERT INTO `'.$this->getNamespace().'.database.unique` + SET `key` = :key; + '); + + $st->bindValue(':key', md5($data['$collection'].':'.$key.'='.$value), PDO::PARAM_STR); + + if(!$st->execute()) { + throw new Duplicate('Duplicated Property: '.$key.'='.$value); + } + } + // Add or update fields abstraction level $st1 = $this->getPDO()->prepare('INSERT INTO `'.$this->getNamespace().'.database.documents` SET uid = :uid, createdAt = :createdAt, updatedAt = :updatedAt, signature = :signature, revision = :revision, permissions = :permissions, status = 0 @@ -397,6 +413,7 @@ class MySQL extends Adapter $documents = 'app_'.$namespace.'.database.documents'; $properties = 'app_'.$namespace.'.database.properties'; $relationships = 'app_'.$namespace.'.database.relationships'; + $unique = 'app_'.$namespace.'.database.unique'; $audit = 'app_'.$namespace.'.audit.audit'; $abuse = 'app_'.$namespace.'.abuse.abuse'; @@ -404,6 +421,7 @@ class MySQL extends Adapter $this->getPDO()->prepare('CREATE TABLE `'.$documents.'` LIKE `template.database.documents`;')->execute(); $this->getPDO()->prepare('CREATE TABLE `'.$properties.'` LIKE `template.database.properties`;')->execute(); $this->getPDO()->prepare('CREATE TABLE `'.$relationships.'` LIKE `template.database.relationships`;')->execute(); + $this->getPDO()->prepare('CREATE TABLE `'.$unique.'` LIKE `template.database.unique`;')->execute(); $this->getPDO()->prepare('CREATE TABLE `'.$audit.'` LIKE `template.audit.audit`;')->execute(); $this->getPDO()->prepare('CREATE TABLE `'.$abuse.'` LIKE `template.abuse.abuse`;')->execute(); } catch (Exception $e) { diff --git a/src/Database/Adapter/Redis.php b/src/Database/Adapter/Redis.php index 16635d48f..e22aa3cb4 100644 --- a/src/Database/Adapter/Redis.php +++ b/src/Database/Adapter/Redis.php @@ -105,9 +105,9 @@ class Redis extends Adapter * * @throws Exception */ - public function createDocument(array $data = []) + public function createDocument(array $data = [], array $unique = []) { - $data = $this->adapter->createDocument($data); + $data = $this->adapter->createDocument($data, $unique); $this->getRedis()->expire($this->getNamespace().':document-'.$data['$id'], 0); $this->getRedis()->expire($this->getNamespace().':document-'.$data['$id'], 0); diff --git a/src/Database/Database.php b/src/Database/Database.php index 4962833e6..8f3e75ce3 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -182,7 +182,7 @@ class Database * @throws AuthorizationException * @throws StructureException */ - public function createDocument(array $data) + public function createDocument(array $data, array $unique = []) { $document = new Document($data); @@ -198,7 +198,7 @@ class Database throw new StructureException($validator->getDescription()); // var_dump($validator->getDescription()); return false; } - return new Document($this->adapter->createDocument($data)); + return new Document($this->adapter->createDocument($data, $unique)); } /** diff --git a/src/Database/Exception/Duplicate.php b/src/Database/Exception/Duplicate.php new file mode 100644 index 000000000..b1874aecf --- /dev/null +++ b/src/Database/Exception/Duplicate.php @@ -0,0 +1,7 @@ +