diff --git a/Dockerfile b/Dockerfile index 6c2f5ef079..f71dc57ac1 100755 --- a/Dockerfile +++ b/Dockerfile @@ -16,7 +16,7 @@ FROM php:7.4-cli-alpine as step1 ENV TZ=Asia/Tel_Aviv \ PHP_REDIS_VERSION=5.3.1 \ - PHP_SWOOLE_VERSION=4.5.2 \ + PHP_SWOOLE_VERSION=4.5.3 \ PHP_XDEBUG_VERSION=sdebug_2_9-beta RUN \ diff --git a/app/config/collections.php b/app/config/collections.php index de11c5ccc3..64b63f74c6 100644 --- a/app/config/collections.php +++ b/app/config/collections.php @@ -1560,6 +1560,13 @@ $collections = [ ], ], ], + Database::SYSTEM_COLLECTION_RESERVED => [ + '$collection' => Database::SYSTEM_COLLECTION_COLLECTIONS, + '$id' => Database::SYSTEM_COLLECTION_RESERVED, + '$permissions' => ['read' => ['*']], + 'name' => 'Reserved', + 'structure' => true, + ], ]; /* diff --git a/app/controllers/api/database.php b/app/controllers/api/database.php index b4e306c338..2979fb34be 100644 --- a/app/controllers/api/database.php +++ b/app/controllers/api/database.php @@ -66,7 +66,7 @@ App::post('/v1/database/collections') 'rules' => $parsedRules, ]); } catch (AuthorizationException $exception) { - throw new Exception('Unauthorized action', 401); + throw new Exception('Unauthorized permissions', 401); } catch (StructureException $exception) { throw new Exception('Bad structure. '.$exception->getMessage(), 400); } catch (\Exception $exception) { @@ -263,7 +263,7 @@ App::put('/v1/database/collections/:collectionId') 'rules' => $parsedRules, ])); } catch (AuthorizationException $exception) { - throw new Exception('Unauthorized action', 401); + throw new Exception('Unauthorized permissions', 401); } catch (StructureException $exception) { throw new Exception('Bad structure. '.$exception->getMessage(), 400); } catch (\Exception $exception) { @@ -401,7 +401,7 @@ App::post('/v1/database/collections/:collectionId/documents') $authorization = new Authorization($parentDocument, 'write'); if (!$authorization->isValid($new->getPermissions())) { - throw new Exception('Unauthorized action', 401); + throw new Exception('Unauthorized permissions', 401); } $parentDocument @@ -426,7 +426,7 @@ App::post('/v1/database/collections/:collectionId/documents') try { $data = $projectDB->createDocument($data); } catch (AuthorizationException $exception) { - throw new Exception('Unauthorized action', 401); + throw new Exception('Unauthorized permissions', 401); } catch (StructureException $exception) { throw new Exception('Bad structure. '.$exception->getMessage(), 400); } catch (\Exception $exception) { @@ -613,7 +613,7 @@ App::patch('/v1/database/collections/:collectionId/documents/:documentId') try { $data = $projectDB->updateDocument($data); } catch (AuthorizationException $exception) { - throw new Exception('Unauthorized action', 401); + throw new Exception('Unauthorized permissions', 401); } catch (StructureException $exception) { throw new Exception('Bad structure. '.$exception->getMessage(), 400); } catch (\Exception $exception) { @@ -666,7 +666,7 @@ App::delete('/v1/database/collections/:collectionId/documents/:documentId') try { $projectDB->deleteDocument($documentId); } catch (AuthorizationException $exception) { - throw new Exception('Unauthorized action', 401); + throw new Exception('Unauthorized permissions', 401); } catch (StructureException $exception) { throw new Exception('Bad structure. '.$exception->getMessage(), 400); } catch (\Exception $exception) { diff --git a/app/controllers/api/users.php b/app/controllers/api/users.php index 9e5c1e4046..2371221003 100644 --- a/app/controllers/api/users.php +++ b/app/controllers/api/users.php @@ -403,3 +403,48 @@ App::delete('/v1/users/:userId/sessions') $response->noContent(); }, ['response', 'projectDB']); + +App::delete('/v1/users/:userId') + ->desc('Delete User') + ->groups(['api', 'users']) + ->label('scope', 'users.write') + ->label('sdk.platform', [APP_PLATFORM_SERVER]) + ->label('sdk.namespace', 'users') + ->label('sdk.method', 'deleteUser') + ->label('sdk.description', '/docs/references/users/delete-user.md') + ->label('abuse-limit', 100) + ->param('userId', '', function () {return new UID();}, 'User unique ID.') + ->action(function ($userId, $response, $projectDB, $deletes) { + /** @var Utopia\Response $response */ + /** @var Appwrite\Database\Database $projectDB */ + /** @var Appwrite\Event\Event $deletes */ + + $user = $projectDB->getDocument($userId); + + if (empty($user->getId()) || Database::SYSTEM_COLLECTION_USERS != $user->getCollection()) { + throw new Exception('User not found', 404); + } + if (!$projectDB->deleteDocument($userId)) { + throw new Exception('Failed to remove user from DB', 500); + } + + if (!$projectDB->deleteUniqueKey(md5('users:email='.$user->getAttribute('email', null)))) { + throw new Exception('Failed to remove unique key from DB', 500); + } + + $reservedId = $projectDB->createDocument([ + '$collection' => Database::SYSTEM_COLLECTION_RESERVED, + '$id' => $userId, + '$permissions' => [ + 'read' => ['*'], + ], + ]); + + if (false === $reservedId) { + throw new Exception('Failed saving reserved id to DB', 500); + } + + $deletes->setParam('document', $user); + + $response->noContent(); + }, ['response', 'projectDB', 'deletes']); diff --git a/app/workers/deletes.php b/app/workers/deletes.php index 0ef366b844..c7bc2b4387 100644 --- a/app/workers/deletes.php +++ b/app/workers/deletes.php @@ -29,6 +29,7 @@ class DeletesV1 { $projectId = $this->args['projectId']; $document = $this->args['document']; + $document = new Document($document); switch ($document->getCollection()) { @@ -38,6 +39,9 @@ class DeletesV1 case Database::SYSTEM_COLLECTION_FUNCTIONS: $this->deleteFunction($document, $projectId); break; + case Database::SYSTEM_COLLECTION_USERS: + $this->deleteUser($document, $projectId); + break; default: Console::error('No lazy delete operation available for document of type: '.$document->getCollection()); @@ -62,6 +66,23 @@ class DeletesV1 $cache->delete($cache->getRoot(), true); } + protected function deleteUser(Document $document, $projectId) + { + $tokens = $document->getAttribute('tokens', []); + + foreach ($tokens as $token) { + if (!$this->getProjectDB($projectId)->deleteDocument($token->getId())) { + throw new Exception('Failed to remove token from DB', 500); + } + } + + // Delete Memberships + $this->deleteByGroup([ + '$collection='.Database::SYSTEM_COLLECTION_MEMBERSHIPS, + 'userId='.$document->getId(), + ], $this->getProjectDB($projectId)); + } + protected function deleteFunction(Document $document, $projectId) { $projectDB = $this->getProjectDB($projectId); diff --git a/docs/references/users/delete-user.md b/docs/references/users/delete-user.md new file mode 100644 index 0000000000..7eb4963485 --- /dev/null +++ b/docs/references/users/delete-user.md @@ -0,0 +1 @@ +Delete a user by its unique ID. \ No newline at end of file diff --git a/src/Appwrite/Database/Adapter.php b/src/Appwrite/Database/Adapter.php index 1bd817c124..4dc7152571 100644 --- a/src/Appwrite/Database/Adapter.php +++ b/src/Appwrite/Database/Adapter.php @@ -88,6 +88,15 @@ abstract class Adapter */ abstract public function deleteDocument($id); + /** + * Delete Unique Key. + * + * @param int $key + * + * @return array + */ + abstract public function deleteUniqueKey($key); + /** * Create Namespace. * diff --git a/src/Appwrite/Database/Adapter/MySQL.php b/src/Appwrite/Database/Adapter/MySQL.php index 5cac8af6be..7acbf75df0 100644 --- a/src/Appwrite/Database/Adapter/MySQL.php +++ b/src/Appwrite/Database/Adapter/MySQL.php @@ -350,6 +350,26 @@ class MySQL extends Adapter return []; } + /** + * Delete Unique Key. + * + * @param int $key + * + * @return array + * + * @throws Exception + */ + public function deleteUniqueKey($key) + { + $st1 = $this->getPDO()->prepare('DELETE FROM `'.$this->getNamespace().'.database.unique` WHERE `key` = :key'); + + $st1->bindValue(':key', $key, PDO::PARAM_STR); + + $st1->execute(); + + return []; + } + /** * Create Relation. * diff --git a/src/Appwrite/Database/Adapter/Redis.php b/src/Appwrite/Database/Adapter/Redis.php index a3b12e2ecc..7fe6ee0689 100644 --- a/src/Appwrite/Database/Adapter/Redis.php +++ b/src/Appwrite/Database/Adapter/Redis.php @@ -153,6 +153,22 @@ class Redis extends Adapter return $data; } + /** + * Delete Unique Key. + * + * @param $key + * + * @return array + * + * @throws Exception + */ + public function deleteUniqueKey($key) + { + $data = $this->adapter->deleteUniqueKey($key); + + return $data; + } + /** * Create Namespace. * diff --git a/src/Appwrite/Database/Database.php b/src/Appwrite/Database/Database.php index d68bc07907..b298925f54 100644 --- a/src/Appwrite/Database/Database.php +++ b/src/Appwrite/Database/Database.php @@ -23,6 +23,7 @@ class Database const SYSTEM_COLLECTION_USAGES = 'usages'; //TODO add structure const SYSTEM_COLLECTION_DOMAINS = 'domains'; const SYSTEM_COLLECTION_CERTIFICATES = 'certificates'; + const SYSTEM_COLLECTION_RESERVED = 'reserved'; // Auth, Account and Users (private to user) const SYSTEM_COLLECTION_USERS = 'users'; @@ -350,6 +351,18 @@ class Database return new Document($this->adapter->deleteDocument($id)); } + /** + * @param int $key + * + * @return Document|false + * + * @throws AuthorizationException + */ + public function deleteUniqueKey($key) + { + return new Document($this->adapter->deleteUniqueKey($key)); + } + /** * @return array */ diff --git a/tests/e2e/Services/Users/UsersBase.php b/tests/e2e/Services/Users/UsersBase.php index 757ef0fdeb..747fd53916 100644 --- a/tests/e2e/Services/Users/UsersBase.php +++ b/tests/e2e/Services/Users/UsersBase.php @@ -168,6 +168,34 @@ trait UsersBase return $data; } + /** + * @depends testGetUser + */ + public function testDeleteUser(array $data):array + { + /** + * Test for SUCCESS + */ + $user = $this->client->call(Client::METHOD_DELETE, '/users/' . $data['userId'], array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals($user['headers']['status-code'], 204); + + /** + * Test for FAILURE + */ + $user = $this->client->call(Client::METHOD_DELETE, '/users/' . $data['userId'], array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals($user['headers']['status-code'], 404); + + return $data; + } + // TODO add test for session delete // TODO add test for all sessions delete } \ No newline at end of file