diff --git a/app/controllers/api/databases.php b/app/controllers/api/databases.php index f1f3356ff1..6f6fd6b606 100644 --- a/app/controllers/api/databases.php +++ b/app/controllers/api/databases.php @@ -39,7 +39,6 @@ use Appwrite\Network\Validator\IP; use Appwrite\Network\Validator\URL; use Appwrite\Utopia\Database\Validator\CustomId; use Appwrite\Utopia\Database\Validator\Queries as QueriesValidator; -use Appwrite\Utopia\Database\Validator\OrderAttributes; use Appwrite\Utopia\Response; use Appwrite\Detector\Detector; use Appwrite\Event\Database as EventDatabase; @@ -505,11 +504,8 @@ App::post('/v1/databases/:databaseId/collections') $collectionId = $collectionId == 'unique()' ? ID::unique() : $collectionId; - /** - * Map aggregate permissions into the multiple permissions they represent, - * accounting for the resource type given that some types not allowed specific permissions. - */ - $permissions = PermissionsProcessor::aggregate($permissions, 'collection'); + // Map aggregate permissions into the multiple permissions they represent. + $permissions = Permission::aggregate($permissions); try { $dbForProject->createDocument('database_' . $database->getInternalId(), new Document([ @@ -525,9 +521,9 @@ App::post('/v1/databases/:databaseId/collections') $collection = $dbForProject->getDocument('database_' . $database->getInternalId(), $collectionId); $dbForProject->createCollection('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId()); - } catch (DuplicateException $th) { + } catch (DuplicateException) { throw new Exception(Exception::COLLECTION_ALREADY_EXISTS); - } catch (LimitException $th) { + } catch (LimitException) { throw new Exception(Exception::COLLECTION_LIMIT_EXCEEDED); } @@ -774,10 +770,8 @@ App::put('/v1/databases/:databaseId/collections/:collectionId') $permissions ??= $collection->getPermissions() ?? []; - /** - * Map aggregate permissions into the multiple permissions they represent. - */ - $permissions = PermissionsProcessor::aggregate($permissions, 'collection'); + // Map aggregate permissions into the multiple permissions they represent. + $permissions = Permission::aggregate($permissions); $enabled ??= $collection->getAttribute('enabled', true); @@ -1875,7 +1869,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/documents') throw new Exception(Exception::DOCUMENT_INVALID_STRUCTURE, '$id is not allowed for creating new documents, try update instead'); } - $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); + $database = Authorization::skip(fn() => $dbForProject->getDocument('databases', $databaseId)); if ($database->isEmpty()) { throw new Exception(Exception::DATABASE_NOT_FOUND); @@ -1889,25 +1883,21 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/documents') } } - $validator = new Authorization('create'); + $validator = new Authorization(Database::PERMISSION_CREATE); if (!$validator->isValid($collection->getCreate())) { throw new Exception(Exception::USER_UNAUTHORIZED); } - /** - * Map aggregate permissions into the multiple permissions they represent, - * accounting for the resource type given that some types not allowed specific permissions. - */ - $permissions = PermissionsProcessor::aggregate($permissions, 'document'); + $allowedPermissions = [ + Database::PERMISSION_READ, + Database::PERMISSION_UPDATE, + Database::PERMISSION_DELETE, + ]; - /** - * Add permissions for current the user for any missing types - * from the allowed permissions for this resource type. - */ - $allowedPermissions = \array_filter( - Database::PERMISSIONS, - fn ($permission) => $permission !== Database::PERMISSION_CREATE - ); + // Map aggregate permissions to into the set of individual permissions they represent. + $permissions = Permission::aggregate($permissions, $allowedPermissions); + + // Add permissions for current the user if none were provided. if (\is_null($permissions)) { $permissions = []; if (!empty($user->getId())) { @@ -1915,19 +1905,8 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/documents') $permissions[] = (new Permission($permission, 'user', $user->getId()))->toString(); } } - } else { - foreach ($allowedPermissions as $permission) { - /** - * If an allowed permission was not passed in the request, - * and there is a current user, add it for the current user. - */ - if (empty(\preg_grep("#^{$permission}\(.+\)$#", $permissions)) && !empty($user->getId())) { - $permissions[] = (new Permission($permission, 'user', $user->getId()))->toString(); - } - } } - // Users can only manage their own roles, API keys and Admin users can manage any // Users can only manage their own roles, API keys and Admin users can manage any $roles = Authorization::getRoles(); if (!Auth::isAppUser($roles) && !Auth::isPrivilegedUser($roles)) { @@ -2022,9 +2001,9 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents') } $documentSecurity = $collection->getAttribute('documentSecurity', false); - $validator = new Authorization('read'); + $validator = new Authorization(Database::PERMISSION_READ); $valid = $validator->isValid($collection->getRead()); - if (!$valid && !$documentSecurity) { + if (!$documentSecurity && !$valid) { throw new Exception(Exception::USER_UNAUTHORIZED); } @@ -2046,11 +2025,10 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents') } if (!empty($cursor)) { - if ($documentSecurity) { - $cursorDocument = $dbForProject->getDocument('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $cursor); - } else { - $cursorDocument = Authorization::skip(fn() => $dbForProject->getDocument('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $cursor)); - } + $cursorDocument = $documentSecurity + ? $dbForProject->getDocument('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $cursor) + : Authorization::skip(fn() => $dbForProject->getDocument('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $cursor)); + if ($cursorDocument->isEmpty()) { throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Document '{$cursor}' for the 'cursor' value not found."); } @@ -2068,7 +2046,7 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents') } } - if ($documentSecurity) { + if ($documentSecurity && !$valid) { $documents = $dbForProject->find('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $allQueries); $total = $dbForProject->count('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $filterQueries, APP_LIMIT_COUNT); } else { @@ -2129,25 +2107,22 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents/:documen } $documentSecurity = $collection->getAttribute('documentSecurity', false); - $validator = new Authorization('read'); + $validator = new Authorization(Database::PERMISSION_READ); $valid = $validator->isValid($collection->getRead()); - if (!$valid && !$documentSecurity) { + if (!$documentSecurity && !$valid) { throw new Exception(Exception::USER_UNAUTHORIZED); } - $document = $dbForProject->getDocument('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $documentId); + if ($documentSecurity && !$valid) { + $document = $dbForProject->getDocument('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $documentId); + } else { + $document = Authorization::skip(fn () => $dbForProject->getDocument('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $documentId)); + } if ($document->isEmpty()) { throw new Exception(Exception::DOCUMENT_NOT_FOUND); } - if ($documentSecurity) { - $valid |= $validator->isValid($document->getRead()); - if (!$valid) { - throw new Exception(Exception::USER_UNAUTHORIZED); - } - } - /** * Reset $collection attribute to remove prefix. */ @@ -2303,25 +2278,18 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/documents/:docum } $documentSecurity = $collection->getAttribute('documentSecurity', false); - $validator = new Authorization('update'); + $validator = new Authorization(Database::PERMISSION_UPDATE); $valid = $validator->isValid($collection->getUpdate()); - if (!$valid && !$documentSecurity) { + if (!$documentSecurity && !$valid) { throw new Exception(Exception::USER_UNAUTHORIZED); } - $document = $dbForProject->getDocument('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $documentId); + $document = Authorization::skip(fn() => $dbForProject->getDocument('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $documentId)); if ($document->isEmpty()) { throw new Exception(Exception::DOCUMENT_NOT_FOUND); } - if ($documentSecurity) { - $valid |= $validator->isValid($document->getUpdate()); - if (!$valid) { - throw new Exception(Exception::USER_UNAUTHORIZED); - } - } - // Users can only manage their own roles, API keys and Admin users can manage any $roles = Authorization::getRoles(); if (!Auth::isAppUser($roles) && !Auth::isPrivilegedUser($roles) && !\is_null($permissions)) { @@ -2343,11 +2311,12 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/documents/:docum } } - /** - * Map aggregate permissions into the multiple permissions they represent, - * accounting for the resource type given that some types not allowed specific permissions. - */ - $permissions = PermissionsProcessor::aggregate($permissions, 'document'); + // Map aggregate permissions into the multiple permissions they represent. + $permissions = Permission::aggregate($permissions, [ + Database::PERMISSION_READ, + Database::PERMISSION_UPDATE, + Database::PERMISSION_DELETE, + ]); if (\is_null($permissions)) { $permissions = $document->getPermissions() ?? []; @@ -2360,14 +2329,16 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/documents/:docum $data['$permissions'] = $permissions; try { - $document = $dbForProject->updateDocument('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $document->getId(), new Document($data)); + if ($documentSecurity && !$valid) { + $document = $dbForProject->updateDocument('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $document->getId(), new Document($data)); + } else { + $document = Authorization::skip(fn() => $dbForProject->updateDocument('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $document->getId(), new Document($data))); + } /** * Reset $collection attribute to remove prefix. */ $document->setAttribute('$collection', $collectionId); - } catch (AuthorizationException) { - throw new Exception(Exception::USER_UNAUTHORIZED); } catch (DuplicateException) { throw new Exception(Exception::DOCUMENT_ALREADY_EXISTS); } catch (StructureException $exception) { @@ -2430,27 +2401,24 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId/documents/:docu } $documentSecurity = $collection->getAttribute('documentSecurity', false); - $validator = new Authorization('delete'); + $validator = new Authorization(Database::PERMISSION_DELETE); $valid = $validator->isValid($collection->getDelete()); - if (!$valid && !$documentSecurity) { + if (!$documentSecurity && !$valid) { throw new Exception(Exception::USER_UNAUTHORIZED); } - $document = $dbForProject->getDocument('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $documentId); + $document = Authorization::skip(fn() => $dbForProject->getDocument('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $documentId)); if ($document->isEmpty()) { throw new Exception(Exception::DOCUMENT_NOT_FOUND); } - if ($documentSecurity) { - $valid |= $validator->isValid($document->getDelete()); - if (!$valid) { - throw new Exception(Exception::USER_UNAUTHORIZED); - } + if ($documentSecurity && !$valid) { + $dbForProject->deleteDocument('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $documentId); + } else { + Authorization::skip(fn() => $dbForProject->deleteDocument('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $documentId)); } - $dbForProject->deleteDocument('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $documentId); - $dbForProject->deleteCachedDocument('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $documentId); /** diff --git a/app/controllers/api/storage.php b/app/controllers/api/storage.php index de69bd1351..40e9248094 100644 --- a/app/controllers/api/storage.php +++ b/app/controllers/api/storage.php @@ -4,7 +4,6 @@ use Appwrite\Auth\Auth; use Appwrite\ClamAV\Network; use Appwrite\Event\Delete; use Appwrite\Event\Event; -use Appwrite\Permissions\PermissionsProcessor; use Appwrite\Utopia\Database\Validator\CustomId; use Appwrite\OpenSSL\OpenSSL; use Appwrite\Stats\Stats; @@ -73,11 +72,8 @@ App::post('/v1/storage/buckets') $bucketId = $bucketId === 'unique()' ? ID::unique() : $bucketId; - /** - * Map aggregate permissions into the multiple permissions they represent, - * accounting for the resource type given that some types not allowed specific permissions. - */ - $permissions = PermissionsProcessor::aggregate($permissions, 'bucket'); + // Map aggregate permissions into the multiple permissions they represent. + $permissions = Permission::aggregate($permissions); try { $files = Config::getParam('collections', [])['files'] ?? []; @@ -265,7 +261,8 @@ App::put('/v1/storage/buckets/:bucketId') * Map aggregate permissions into the multiple permissions they represent, * accounting for the resource type given that some types not allowed specific permissions. */ - $permissions = PermissionsProcessor::aggregate($permissions, 'bucket'); + // Map aggregate permissions into the multiple permissions they represent. + $permissions = Permission::aggregate($permissions); $bucket = $dbForProject->updateDocument('buckets', $bucket->getId(), $bucket ->setAttribute('name', $name) @@ -367,25 +364,21 @@ App::post('/v1/storage/buckets/:bucketId/files') throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND); } - $validator = new Authorization('create'); + $validator = new Authorization(Database::PERMISSION_CREATE); if (!$validator->isValid($bucket->getCreate())) { throw new Exception(Exception::USER_UNAUTHORIZED); } - /** - * Map aggregate permissions into the multiple permissions they represent, - * accounting for the resource type given that some types not allowed specific permissions. - */ - $permissions = PermissionsProcessor::aggregate($permissions, 'file'); + $allowedPermissions = [ + Database::PERMISSION_READ, + Database::PERMISSION_UPDATE, + Database::PERMISSION_DELETE, + ]; - /** - * Add permissions for current the user for any missing types - * from the allowed permissions for this resource type. - */ - $allowedPermissions = \array_filter( - Database::PERMISSIONS, - fn ($permission) => $permission !== Database::PERMISSION_CREATE - ); + // Map aggregate permissions to into the set of individual permissions they represent. + $permissions = Permission::aggregate($permissions, $allowedPermissions); + + // Add permissions for current the user if none were provided. if (\is_null($permissions)) { $permissions = []; if (!empty($user->getId())) { @@ -393,16 +386,6 @@ App::post('/v1/storage/buckets/:bucketId/files') $permissions[] = (new Permission($permission, 'user', $user->getId()))->toString(); } } - } else { - foreach ($allowedPermissions as $permission) { - /** - * If an allowed permission was not passed in the request, - * and there is a current user, add it for the current user. - */ - if (empty(\preg_grep("#^{$permission}\(.+\)$#", $permissions)) && !empty($user->getId())) { - $permissions[] = (new Permission($permission, 'user', $user->getId()))->toString(); - } - } } // Users can only manage their own roles, API keys and Admin users can manage any @@ -426,14 +409,6 @@ App::post('/v1/storage/buckets/:bucketId/files') } } - $file = $request->getFiles('file'); - - /** - * Validators - */ - $allowedFileExtensions = $bucket->getAttribute('allowedFileExtensions', []); - $fileExt = new FileExt($allowedFileExtensions); - $maximumFileSize = $bucket->getAttribute('maximumFileSize', 0); if ($maximumFileSize > (int) App::getEnv('_APP_STORAGE_LIMIT', 0)) { throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Maximum bucket file size is larger than _APP_STORAGE_LIMIT'); @@ -700,8 +675,10 @@ App::get('/v1/storage/buckets/:bucketId/files') throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND); } - $validator = new Authorization('read'); - if (!$validator->isValid($bucket->getRead())) { + $fileSecurity = $bucket->getAttribute('fileSecurity', false); + $validator = new Authorization(Database::PERMISSION_READ); + $valid = $validator->isValid($bucket->getRead()); + if (!$fileSecurity && !$valid) { throw new Exception(Exception::USER_UNAUTHORIZED); } @@ -716,7 +693,9 @@ App::get('/v1/storage/buckets/:bucketId/files') $queries[] = Query::offset($offset); $queries[] = $orderType === Database::ORDER_ASC ? Query::orderAsc('') : Query::orderDesc(''); if (!empty($cursor)) { - $cursorDocument = $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $cursor); + $cursorDocument = $fileSecurity + ? $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $cursor) + : Authorization::skip(fn() => $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $cursor)); if ($cursorDocument->isEmpty()) { throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "File '{$cursor}' for the 'cursor' value not found."); @@ -725,7 +704,7 @@ App::get('/v1/storage/buckets/:bucketId/files') $queries[] = $cursorDirection === Database::CURSOR_AFTER ? Query::cursorAfter($cursorDocument) : Query::cursorBefore($cursorDocument); } - if ($bucket->getAttribute('fileSecurity', false)) { + if ($fileSecurity && !$valid) { $files = $dbForProject->find('bucket_' . $bucket->getInternalId(), \array_merge($filterQueries, $queries)); $total = $dbForProject->count('bucket_' . $bucket->getInternalId(), $filterQueries, APP_LIMIT_COUNT); } else { @@ -771,25 +750,22 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId') } $fileSecurity = $bucket->getAttribute('fileSecurity', false); - $validator = new Authorization('read'); + $validator = new Authorization(Database::PERMISSION_READ); $valid = $validator->isValid($bucket->getRead()); if (!$valid && !$fileSecurity) { throw new Exception(Exception::USER_UNAUTHORIZED); } - $file = $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId); + if ($fileSecurity && !$valid) { + $file = $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId); + } else { + $file = Authorization::skip(fn() => $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId)); + } if ($file->isEmpty() || $file->getAttribute('bucketId') !== $bucketId) { throw new Exception(Exception::STORAGE_FILE_NOT_FOUND); } - if ($fileSecurity) { - $valid = $validator->isValid($file->getRead()); - if (!$valid) { - throw new Exception(Exception::USER_UNAUTHORIZED); - } - } - $usage ->setParam('storage.files.read', 1) ->setParam('bucketId', $bucketId) @@ -846,9 +822,9 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/preview') } $fileSecurity = $bucket->getAttribute('fileSecurity', false); - $validator = new Authorization('read'); + $validator = new Authorization(Database::PERMISSION_READ); $valid = $validator->isValid($bucket->getRead()); - if (!$valid && !$fileSecurity) { + if (!$fileSecurity && !$valid) { throw new Exception(Exception::USER_UNAUTHORIZED); } @@ -863,19 +839,16 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/preview') $date = \date('D, d M Y H:i:s', \time() + (60 * 60 * 24 * 45)) . ' GMT'; // 45 days cache $key = \md5($fileId . $width . $height . $gravity . $quality . $borderWidth . $borderColor . $borderRadius . $opacity . $rotation . $background . $output); - $file = Authorization::skip(fn() => $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId)); + if ($fileSecurity && !$valid) { + $file = $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId); + } else { + $file = Authorization::skip(fn() => $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId)); + } if ($file->isEmpty() || $file->getAttribute('bucketId') !== $bucketId) { throw new Exception(Exception::STORAGE_FILE_NOT_FOUND); } - if ($fileSecurity) { - $valid |= $validator->isValid($file->getRead()); - if (!$valid) { - throw new Exception(Exception::USER_UNAUTHORIZED); - } - } - $path = $file->getAttribute('path'); $type = \strtolower(\pathinfo($path, PATHINFO_EXTENSION)); $algorithm = $file->getAttribute('algorithm'); @@ -997,25 +970,22 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/download') } $fileSecurity = $bucket->getAttribute('fileSecurity', false); - $validator = new Authorization('read'); + $validator = new Authorization(Database::PERMISSION_READ); $valid = $validator->isValid($bucket->getRead()); - if (!$valid && !$fileSecurity) { + if (!$fileSecurity && !$valid) { throw new Exception(Exception::USER_UNAUTHORIZED); } - $file = $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId); + if ($fileSecurity && !$valid) { + $file = $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId); + } else { + $file = Authorization::skip(fn() => $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId)); + } if ($file->isEmpty() || $file->getAttribute('bucketId') !== $bucketId) { throw new Exception(Exception::STORAGE_FILE_NOT_FOUND); } - if ($fileSecurity) { - $valid |= $validator->isValid($file->getRead()); - if (!$valid) { - throw new Exception(Exception::USER_UNAUTHORIZED); - } - } - $path = $file->getAttribute('path', ''); if (!$deviceFiles->exists($path)) { @@ -1135,25 +1105,22 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/view') } $fileSecurity = $bucket->getAttribute('fileSecurity', false); - $validator = new Authorization('read'); + $validator = new Authorization(Database::PERMISSION_READ); $valid = $validator->isValid($bucket->getRead()); - if (!$valid && !$fileSecurity) { + if (!$fileSecurity && !$valid) { throw new Exception(Exception::USER_UNAUTHORIZED); } - $file = $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId); + if ($fileSecurity && !$valid) { + $file = $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId); + } else { + $file = Authorization::skip(fn() => $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId)); + } if ($file->isEmpty() || $file->getAttribute('bucketId') !== $bucketId) { throw new Exception(Exception::STORAGE_FILE_NOT_FOUND); } - if ($fileSecurity) { - $valid |= !$validator->isValid($file->getRead()); - if (!$valid) { - throw new Exception(Exception::USER_UNAUTHORIZED); - } - } - $mimes = Config::getParam('storage-mimes'); $path = $file->getAttribute('path', ''); @@ -1287,26 +1254,22 @@ App::put('/v1/storage/buckets/:bucketId/files/:fileId') } $fileSecurity = $bucket->getAttributes('fileSecurity', false); - $validator = new Authorization('update'); + $validator = new Authorization(Database::PERMISSION_UPDATE); $valid = $validator->isValid($bucket->getUpdate()); - if (!$valid && !$fileSecurity) { + if (!$fileSecurity && !$valid) { throw new Exception(Exception::USER_UNAUTHORIZED); } - $file = $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId); + if ($fileSecurity && !$valid) { + $file = $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId); + } else { + $file = Authorization::skip(fn() => $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId)); + } if ($file->isEmpty() || $file->getAttribute('bucketId') !== $bucketId) { throw new Exception(Exception::STORAGE_FILE_NOT_FOUND); } - if ($fileSecurity) { - $valid |= $validator->isValid($file->getUpdate()); - if (!$valid) { - throw new Exception(Exception::USER_UNAUTHORIZED); - } - } - - // Users can only manage their own roles, API keys and Admin users can manage any // Users can only manage their own roles, API keys and Admin users can manage any $roles = Authorization::getRoles(); if (!Auth::isAppUser($roles) && !Auth::isPrivilegedUser($roles)) { @@ -1328,11 +1291,12 @@ App::put('/v1/storage/buckets/:bucketId/files/:fileId') } } - /** - * Map aggregate permissions into the multiple permissions they represent, - * accounting for the resource type given that some types not allowed specific permissions. - */ - $permissions = PermissionsProcessor::aggregate($permissions, 'file'); + // Map aggregate permissions into the multiple permissions they represent. + $permissions = Permission::aggregate($permissions, [ + Database::PERMISSION_READ, + Database::PERMISSION_UPDATE, + Database::PERMISSION_DELETE, + ]); $file->setAttribute('$permissions', $permissions); @@ -1384,23 +1348,20 @@ App::delete('/v1/storage/buckets/:bucketId/files/:fileId') $fileSecurity = $bucket->getAttributes('fileSecurity', false); $validator = new Authorization('delete'); $valid = $validator->isValid($bucket->getDelete()); - if (!$valid && !$fileSecurity) { + if (!$fileSecurity && !$valid) { throw new Exception(Exception::USER_UNAUTHORIZED); } - $file = $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId); + if ($fileSecurity && !$valid) { + $file = $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId); + } else { + $file = Authorization::skip(fn() => $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId)); + } if ($file->isEmpty() || $file->getAttribute('bucketId') !== $bucketId) { throw new Exception(Exception::STORAGE_FILE_NOT_FOUND); } - if ($fileSecurity) { - $valid |= $validator->isValid($file->getDelete()); - if (!$valid) { - throw new Exception(Exception::USER_UNAUTHORIZED); - } - } - $deviceDeleted = false; if ($file->getAttribute('chunksTotal') !== $file->getAttribute('chunksUploaded')) { $deviceDeleted = $deviceFiles->abort( diff --git a/app/controllers/api/teams.php b/app/controllers/api/teams.php index f20f90049d..d0c9f6901b 100644 --- a/app/controllers/api/teams.php +++ b/app/controllers/api/teams.php @@ -65,8 +65,8 @@ App::post('/v1/teams') '$id' => $teamId, '$permissions' => [ Permission::read(Role::team($teamId)), - Permission::update(Role::team($teamId), 'owner'), - Permission::delete(Role::team($teamId), 'owner'), + Permission::update(Role::team($teamId, 'owner')), + Permission::delete(Role::team($teamId, 'owner')), ], 'name' => $name, 'total' => ($isPrivilegedUser || $isAppUser) ? 0 : 1, @@ -811,14 +811,6 @@ App::delete('/v1/teams/:teamId/memberships/:membershipId') throw new Exception(Exception::TEAM_NOT_FOUND); } - /** - * Force document security - */ - $validator = new Authorization('delete'); - if (!$validator->isValid($membership->getDelete())) { - throw new Exception(Exception::USER_UNAUTHORIZED); - } - try { $dbForProject->deleteDocument('memberships', $membership->getId()); } catch (AuthorizationException $exception) { diff --git a/app/http.php b/app/http.php index 509908148d..d99771be4a 100644 --- a/app/http.php +++ b/app/http.php @@ -173,6 +173,7 @@ $http->on('start', function (Server $http) use ($payloadSize, $register) { 'antivirus' => true, 'fileSecurity' => true, '$permissions' => [ + Permission::create(Role::any()), Permission::read(Role::any()), Permission::update(Role::any()), Permission::delete(Role::any()), diff --git a/app/tasks/maintenance.php b/app/tasks/maintenance.php index c0fa920787..dffc67c4cb 100644 --- a/app/tasks/maintenance.php +++ b/app/tasks/maintenance.php @@ -134,7 +134,7 @@ $cli (new Delete()) ->setType(DELETE_TYPE_CACHE_BY_TIMESTAMP) - ->setTimestamp(time() - $interval) + ->setDatetime(DateTime::addSeconds(new \DateTime(), -1 * $interval)) ->trigger(); } diff --git a/composer.json b/composer.json index a57c7443f4..ac0eaf3dd2 100644 --- a/composer.json +++ b/composer.json @@ -51,7 +51,7 @@ "utopia-php/cache": "0.6.*", "utopia-php/cli": "0.13.*", "utopia-php/config": "0.2.*", - "utopia-php/database": "0.22.*", + "utopia-php/database": "dev-refactor-permissions as 0.22.0", "utopia-php/locale": "0.4.*", "utopia-php/registry": "0.5.*", "utopia-php/preloader": "0.2.*", diff --git a/composer.lock b/composer.lock index 70b02383e7..f94da77170 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": "5c02453019092f62ceec9490ee7f117b", + "content-hash": "175fe4abafd8bde4053b91eea905c328", "packages": [ { "name": "adhocore/jwt", @@ -2052,16 +2052,16 @@ }, { "name": "utopia-php/database", - "version": "0.22.0", + "version": "dev-refactor-permissions", "source": { "type": "git", "url": "https://github.com/utopia-php/database.git", - "reference": "22c45ae83612e907203b7571cd8e3115ae3ae4c5" + "reference": "44dda6914c7be148eb59ce11847386ce39f7b106" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/database/zipball/22c45ae83612e907203b7571cd8e3115ae3ae4c5", - "reference": "22c45ae83612e907203b7571cd8e3115ae3ae4c5", + "url": "https://api.github.com/repos/utopia-php/database/zipball/44dda6914c7be148eb59ce11847386ce39f7b106", + "reference": "44dda6914c7be148eb59ce11847386ce39f7b106", "shasum": "" }, "require": { @@ -2110,9 +2110,9 @@ ], "support": { "issues": "https://github.com/utopia-php/database/issues", - "source": "https://github.com/utopia-php/database/tree/0.22.0" + "source": "https://github.com/utopia-php/database/tree/refactor-permissions" }, - "time": "2022-08-17T12:55:37+00:00" + "time": "2022-08-24T10:22:04+00:00" }, { "name": "utopia-php/domains", @@ -5354,10 +5354,17 @@ "version": "9999999-dev", "alias": "0.19.5", "alias_normalized": "0.19.5.0" + }, + { + "package": "utopia-php/database", + "version": "dev-refactor-permissions", + "alias": "0.22.0", + "alias_normalized": "0.22.0.0" } ], "minimum-stability": "stable", "stability-flags": { + "utopia-php/database": 20, "appwrite/sdk-generator": 20 }, "prefer-stable": false, diff --git a/src/Appwrite/Permissions/PermissionsProcessor.php b/src/Appwrite/Permissions/PermissionsProcessor.php deleted file mode 100644 index 7a7188498e..0000000000 --- a/src/Appwrite/Permissions/PermissionsProcessor.php +++ /dev/null @@ -1,62 +0,0 @@ - $permission) { - $permission = Permission::parse($permission); - foreach ($aggregates as $type => $subTypes) { - if ($permission->getPermission() != $type) { - continue; - } - foreach ($subTypes as $subType) { - $permissions[] = (new Permission( - $subType, - $permission->getRole(), - $permission->getIdentifier(), - $permission->getDimension() - ))->toString(); - } - unset($permissions[$i]); - } - } - return $permissions; - } - - private static function getAggregates($resource): array - { - $aggregates = []; - - switch ($resource) { - case 'document': - case 'file': - $aggregates['write'] = [ - Database::PERMISSION_UPDATE, - Database::PERMISSION_DELETE - ]; - break; - case 'collection': - case 'bucket': - $aggregates['write'] = [ - Database::PERMISSION_CREATE, - Database::PERMISSION_UPDATE, - Database::PERMISSION_DELETE - ]; - break; - } - - return $aggregates; - } -} diff --git a/src/Appwrite/Specification/Format/OpenAPI3.php b/src/Appwrite/Specification/Format/OpenAPI3.php index a9ac79e0c8..c7a9db0524 100644 --- a/src/Appwrite/Specification/Format/OpenAPI3.php +++ b/src/Appwrite/Specification/Format/OpenAPI3.php @@ -5,6 +5,8 @@ namespace Appwrite\Specification\Format; use Appwrite\Specification\Format; use Appwrite\Template\Template; use Appwrite\Utopia\Response\Model; +use Utopia\Database\Permission; +use Utopia\Database\Role; use Utopia\Validator; class OpenAPI3 extends Format @@ -338,6 +340,14 @@ class OpenAPI3 extends Format $node['schema']['items'] = [ 'type' => 'string', ]; + $node['schema']['x-example'] = '["' . Permission::read(Role::any()) . '"]'; + break; + case 'Utopia\Database\Validator\Roles': + $node['schema']['type'] = $validator->getType(); + $node['schema']['items'] = [ + 'type' => 'string', + ]; + $node['schema']['x-example'] = '["' . Role::any()->toString() . '"]'; break; case 'Appwrite\Auth\Validator\Password': $node['schema']['type'] = $validator->getType(); diff --git a/tests/e2e/Services/Databases/DatabasesBase.php b/tests/e2e/Services/Databases/DatabasesBase.php index 4df1c71760..7121af114a 100644 --- a/tests/e2e/Services/Databases/DatabasesBase.php +++ b/tests/e2e/Services/Databases/DatabasesBase.php @@ -48,13 +48,13 @@ trait DatabasesBase ]), [ 'collectionId' => ID::unique(), 'name' => 'Movies', + 'documentSecurity' => true, 'permissions' => [ Permission::read(Role::any()), Permission::create(Role::any()), Permission::update(Role::any()), Permission::delete(Role::any()), ], - 'documentSecurity' => true, ]); $this->assertEquals(201, $movies['headers']['status-code']); @@ -101,18 +101,20 @@ trait DatabasesBase ], ]); + $this->assertEquals(404, $responseCreateDocument['headers']['status-code']); + $responseListDocument = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $data['moviesId'] . '/documents', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); + $this->assertEquals(404, $responseListDocument['headers']['status-code']); + $responseGetDocument = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $data['moviesId'] . '/documents/someID', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); - $this->assertEquals(404, $responseCreateDocument['headers']['status-code']); - $this->assertEquals(404, $responseListDocument['headers']['status-code']); $this->assertEquals(404, $responseGetDocument['headers']['status-code']); } @@ -2222,7 +2224,7 @@ trait DatabasesBase $this->assertEquals([], $document['body']['$permissions']); } - // Updated and Inherit Permissions + // Updated Permissions $document = $this->client->call(Client::METHOD_PATCH, '/databases/' . $databaseId . '/collections/' . $data['moviesId'] . '/documents/' . $id, array_merge([ 'content-type' => 'application/json', @@ -2234,7 +2236,8 @@ trait DatabasesBase 'actors' => [], ], 'permissions' => [ - Permission::read(Role::any()), + Permission::read(Role::user($this->getUser()['$id'])), + Permission::update(Role::user($this->getUser()['$id'])) ], ]); @@ -2245,8 +2248,11 @@ trait DatabasesBase // This differs from the old permissions model because we don't inherit // existing document permissions on update, unless none were supplied, // so that specific types can be removed if wanted. - $this->assertCount(1, $document['body']['$permissions']); - $this->assertContains(Permission::read(Role::any()), $document['body']['$permissions']); + $this->assertCount(2, $document['body']['$permissions']); + $this->assertEquals([ + Permission::read(Role::user($this->getUser()['$id'])), + Permission::update(Role::user($this->getUser()['$id'])), + ], $document['body']['$permissions']); $document = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $data['moviesId'] . '/documents/' . $id, array_merge([ 'content-type' => 'application/json', @@ -2257,11 +2263,11 @@ trait DatabasesBase $this->assertEquals($document['body']['title'], 'Captain America 2'); $this->assertEquals($document['body']['releaseYear'], 1945); - // This differs from the old permissions model because we don't inherit - // existing document permissions on update, unless none were supplied, - // so that specific types can be removed if wanted. - $this->assertCount(1, $document['body']['$permissions']); - $this->assertContains(Permission::read(Role::any()), $document['body']['$permissions']); + $this->assertCount(2, $document['body']['$permissions']); + $this->assertEquals([ + Permission::read(Role::user($this->getUser()['$id'])), + Permission::update(Role::user($this->getUser()['$id'])), + ], $document['body']['$permissions']); // Reset Permissions @@ -2283,10 +2289,18 @@ trait DatabasesBase $this->assertCount(0, $document['body']['$permissions']); $this->assertEquals([], $document['body']['$permissions']); + // Check user can still read document due to collection permissions of read("any") + $document = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $data['moviesId'] . '/documents/' . $id, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(200, $document['headers']['status-code']); + return $data; } - public function testEnforceDocumentPermissions(): void + public function testEnforceCollectionAndDocumentPermissions(): void { $database = $this->client->call(Client::METHOD_POST, '/databases', array_merge([ 'content-type' => 'application/json', @@ -2294,10 +2308,10 @@ trait DatabasesBase 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ 'databaseId' => ID::unique(), - 'name' => 'EnforceCollectionPermissions', + 'name' => 'EnforceCollectionAndDocumentPermissions', ]); $this->assertEquals(201, $database['headers']['status-code']); - $this->assertEquals('EnforceCollectionPermissions', $database['body']['name']); + $this->assertEquals('EnforceCollectionAndDocumentPermissions', $database['body']['name']); $databaseId = $database['body']['$id']; $user = $this->getUser()['$id']; @@ -2307,7 +2321,7 @@ trait DatabasesBase 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ 'collectionId' => ID::unique(), - 'name' => 'enforceCollectionPermissions', + 'name' => 'enforceCollectionAndDocumentPermissions', 'documentSecurity' => true, 'permissions' => [ Permission::read(Role::user($user)), @@ -2318,7 +2332,7 @@ trait DatabasesBase ]); $this->assertEquals(201, $collection['headers']['status-code']); - $this->assertEquals($collection['body']['name'], 'enforceCollectionPermissions'); + $this->assertEquals($collection['body']['name'], 'enforceCollectionAndDocumentPermissions'); $this->assertEquals($collection['body']['documentSecurity'], true); $collectionId = $collection['body']['$id']; @@ -2412,22 +2426,16 @@ trait DatabasesBase 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); - switch ($this->getSide()) { - case 'client': - $this->assertEquals(2, $documentsUser1['body']['total']); - $this->assertCount(2, $documentsUser1['body']['documents']); - break; - case 'server': - $this->assertEquals(3, $documentsUser1['body']['total']); - $this->assertCount(3, $documentsUser1['body']['documents']); - break; - } + // Current user has read permission on the collection so can get any document + $this->assertEquals(3, $documentsUser1['body']['total']); + $this->assertCount(3, $documentsUser1['body']['documents']); $document3GetWithCollectionRead = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents/' . $document3['body']['$id'], array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); + // Current user has read permission on the collection so can get any document $this->assertEquals(200, $document3GetWithCollectionRead['headers']['status-code']); $email = uniqid() . 'user@localhost.test'; @@ -2460,6 +2468,7 @@ trait DatabasesBase 'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $session2, ]); + // Current user has no collection permissions but has read permission for this document $this->assertEquals(200, $document3GetWithDocumentRead['headers']['status-code']); $document2GetFailure = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents/' . $document2['body']['$id'], [ @@ -2469,7 +2478,8 @@ trait DatabasesBase 'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $session2, ]); - $this->assertEquals(401, $document2GetFailure['headers']['status-code']); + // Current user has no collection or document permissions for this document + $this->assertEquals(404, $document2GetFailure['headers']['status-code']); $documentsUser2 = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents', [ 'origin' => 'http://localhost', @@ -2478,6 +2488,210 @@ trait DatabasesBase 'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $session2, ]); + // Current user has no collection permissions but has read permission for one document + $this->assertEquals(1, $documentsUser2['body']['total']); + $this->assertCount(1, $documentsUser2['body']['documents']); + } + + public function testEnforceCollectionPermissions() + { + $database = $this->client->call(Client::METHOD_POST, '/databases', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'databaseId' => ID::unique(), + 'name' => 'EnforceCollectionPermissions', + ]); + $this->assertEquals(201, $database['headers']['status-code']); + $this->assertEquals('EnforceCollectionPermissions', $database['body']['name']); + + $databaseId = $database['body']['$id']; + $user = $this->getUser()['$id']; + $collection = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'collectionId' => ID::unique(), + 'name' => 'enforceCollectionPermissions', + 'permissions' => [ + Permission::read(Role::user($user)), + Permission::create(Role::user($user)), + Permission::update(Role::user($user)), + Permission::delete(Role::user($user)), + ], + ]); + + $this->assertEquals(201, $collection['headers']['status-code']); + $this->assertEquals($collection['body']['name'], 'enforceCollectionPermissions'); + $this->assertEquals($collection['body']['documentSecurity'], false); + + $collectionId = $collection['body']['$id']; + + sleep(2); + + $attribute = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes/string', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'attribute', + 'size' => 64, + 'required' => true, + ]); + + $this->assertEquals(202, $attribute['headers']['status-code'], 202); + $this->assertEquals('attribute', $attribute['body']['key']); + + // wait for db to add attribute + sleep(2); + + $index = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/indexes', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'key_attribute', + 'type' => 'key', + 'attributes' => [$attribute['body']['key']], + ]); + + $this->assertEquals(202, $index['headers']['status-code']); + $this->assertEquals('key_attribute', $index['body']['key']); + + // wait for db to add attribute + sleep(2); + + $document1 = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'documentId' => ID::unique(), + 'data' => [ + 'attribute' => 'one', + ], + 'permissions' => [ + Permission::read(Role::user($user)), + Permission::update(Role::user($user)), + Permission::delete(Role::user($user)), + ] + ]); + + $this->assertEquals(201, $document1['headers']['status-code']); + + $document2 = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'documentId' => ID::unique(), + 'data' => [ + 'attribute' => 'one', + ], + 'permissions' => [ + Permission::update(Role::user($user)), + Permission::delete(Role::user($user)), + ] + ]); + + $this->assertEquals(201, $document2['headers']['status-code']); + + $document3 = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ], [ + 'documentId' => ID::unique(), + 'data' => [ + 'attribute' => 'one', + ], + 'permissions' => [ + Permission::read(Role::user(ID::custom('other2'))), + Permission::update(Role::user(ID::custom('other2'))), + ], + ]); + + $this->assertEquals(201, $document3['headers']['status-code']); + + $documentsUser1 = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + // Current user has read permission on the collection so can get any document + $this->assertEquals(3, $documentsUser1['body']['total']); + $this->assertCount(3, $documentsUser1['body']['documents']); + + $document3GetWithCollectionRead = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents/' . $document3['body']['$id'], array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + // Current user has read permission on the collection so can get any document + $this->assertEquals(200, $document3GetWithCollectionRead['headers']['status-code']); + + $email = uniqid() . 'user2@localhost.test'; + $password = 'password'; + $name = 'User Name'; + $this->client->call(Client::METHOD_POST, '/account', [ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], [ + 'userId' => ID::custom('other2'), + 'email' => $email, + 'password' => $password, + 'name' => $name, + ]); + $session2 = $this->client->call(Client::METHOD_POST, '/account/sessions/email', [ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], [ + 'email' => $email, + 'password' => $password, + ]); + $session2 = $this->client->parseCookie((string)$session2['headers']['set-cookie'])['a_session_' . $this->getProject()['$id']]; + + $document3GetWithDocumentRead = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents/' . $document3['body']['$id'], [ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $session2, + ]); + + // Current user has no collection permissions and document permissions are disabled + $this->assertEquals(401, $document3GetWithDocumentRead['headers']['status-code']); + + $documentsUser2 = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents', [ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $session2, + ]); + + // Current user has no collection permissions and document permissions are disabled + $this->assertEquals(401, $documentsUser2['headers']['status-code']); + + + // Enable document permissions + $collection = $this->client->call(CLient::METHOD_PUT, '/databases/' . $databaseId . '/collections/' . $collectionId, [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ], [ + 'name' => $collection['body']['name'], + 'documentSecurity' => true, + ]); + + $documentsUser2 = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents', [ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $session2, + ]); + + // Current user has no collection permissions read access to one document $this->assertEquals(1, $documentsUser2['body']['total']); $this->assertCount(1, $documentsUser2['body']['documents']); } diff --git a/tests/e2e/Services/Databases/DatabasesPermissionsGuestTest.php b/tests/e2e/Services/Databases/DatabasesPermissionsGuestTest.php index 3a079c6d22..8d4a19bd30 100644 --- a/tests/e2e/Services/Databases/DatabasesPermissionsGuestTest.php +++ b/tests/e2e/Services/Databases/DatabasesPermissionsGuestTest.php @@ -9,6 +9,7 @@ use Tests\E2E\Scopes\SideClient; use Utopia\Database\ID; use Utopia\Database\Permission; use Utopia\Database\Role; +use Utopia\Database\Validator\Authorization; class DatabasesPermissionsGuestTest extends Scope { @@ -88,19 +89,19 @@ class DatabasesPermissionsGuestTest extends Scope $this->assertEquals(201, $response['headers']['status-code']); + $roles = Authorization::getRoles(); + Authorization::cleanRoles(); + $documents = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents', [ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ]); - foreach ($documents['body']['documents'] as $document) { - foreach ($document['$permissions'] as $permission) { - $permission = Permission::parse($permission); - if ($permission->getPermission() != 'read') { - continue; - } - $this->assertEquals($permission->getRole(), Role::any()->toString()); - } + $this->assertEquals(1, $documents['body']['total']); + $this->assertEquals($permissions, $documents['body']['documents'][0]['$permissions']); + + foreach ($roles as $role) { + Authorization::setRole($role); } } } diff --git a/tests/e2e/Services/Databases/DatabasesPermissionsMemberTest.php b/tests/e2e/Services/Databases/DatabasesPermissionsMemberTest.php index 29042178ce..aedc53854a 100644 --- a/tests/e2e/Services/Databases/DatabasesPermissionsMemberTest.php +++ b/tests/e2e/Services/Databases/DatabasesPermissionsMemberTest.php @@ -3,8 +3,8 @@ namespace Tests\E2E\Services\Databases; use Tests\E2E\Client; -use Tests\E2E\Scopes\Scope; use Tests\E2E\Scopes\ProjectCustom; +use Tests\E2E\Scopes\Scope; use Tests\E2E\Scopes\SideClient; use Utopia\Database\ID; use Utopia\Database\Permission; @@ -29,16 +29,72 @@ class DatabasesPermissionsMemberTest extends Scope public function permissionsProvider(): array { return [ - [[Permission::read(Role::any())]], - [[Permission::read(Role::users())]], - [[Permission::read(Role::user(ID::custom('random')))]], - [[Permission::read(Role::user(ID::custom('lorem'))), Permission::update(Role::user('lorem')), Permission::delete(Role::user('lorem'))]], - [[Permission::read(Role::user(ID::custom('dolor'))), Permission::update(Role::user('dolor')), Permission::delete(Role::user('dolor'))]], - [[Permission::read(Role::user(ID::custom('dolor'))), Permission::read(Role::user('lorem')), Permission::update(Role::user('dolor')), Permission::delete(Role::user('dolor'))]], - [[Permission::update(Role::any()), Permission::delete(Role::any())]], - [[Permission::read(Role::any()), Permission::update(Role::any()), Permission::delete(Role::any())]], - [[Permission::read(Role::users()), Permission::update(Role::users()), Permission::delete(Role::users())]], - [[Permission::read(Role::any()), Permission::update(Role::users()), Permission::delete(Role::users())]], + [ + 'permissions' => [Permission::read(Role::any())], + 'any' => 1, + 'users' => 1, + 'doconly' => 1, + ], + [ + 'permissions' => [Permission::read(Role::users())], + 'any' => 2, + 'users' => 2, + 'doconly' => 2, + ], + [ + 'permissions' => [Permission::read(Role::user(ID::custom('random')))], + 'any' => 3, + 'users' => 3, + 'doconly' => 2, + ], + [ + 'permissions' => [Permission::read(Role::user(ID::custom('lorem'))), Permission::update(Role::user('lorem')), Permission::delete(Role::user('lorem'))], + 'any' => 4, + 'users' => 4, + 'doconly' => 2, + ], + [ + 'permissions' => [Permission::read(Role::user(ID::custom('dolor'))), Permission::update(Role::user('dolor')), Permission::delete(Role::user('dolor'))], + 'any' => 5, + 'users' => 5, + 'doconly' => 2, + ], + [ + 'permissions' => [Permission::read(Role::user(ID::custom('dolor'))), Permission::read(Role::user('lorem')), Permission::update(Role::user('dolor')), Permission::delete(Role::user('dolor'))], + 'any' => 6, + 'users' => 6, + 'doconly' => 2, + ], + [ + 'permissions' => [Permission::update(Role::any()), Permission::delete(Role::any())], + 'any' => 7, + 'users' => 7, + 'doconly' => 2, + ], + [ + 'permissions' => [Permission::read(Role::any()), Permission::update(Role::any()), Permission::delete(Role::any())], + 'any' => 8, + 'users' => 8, + 'doconly' => 3, + ], + [ + 'permissions' => [Permission::read(Role::any()), Permission::update(Role::users()), Permission::delete(Role::users())], + 'any' => 9, + 'users' => 9, + 'doconly' => 4, + ], + [ + 'permissions' => [Permission::read(Role::user(ID::custom('user1')))], + 'any' => 10, + 'users' => 10, + 'doconly' => 5, + ], + [ + 'permissions' => [Permission::read(Role::user(ID::custom('user1'))), Permission::read(Role::user(ID::custom('user1')))], + 'any' => 11, + 'users' => 11, + 'doconly' => 6, + ], ]; } @@ -74,7 +130,6 @@ class DatabasesPermissionsMemberTest extends Scope 'documentSecurity' => true, ]); $this->assertEquals(201, $public['headers']['status-code']); - $this->collections = ['public' => $public['body']['$id']]; $response = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $this->collections['public'] . '/attributes/string', $this->getServerHeader(), [ @@ -96,10 +151,25 @@ class DatabasesPermissionsMemberTest extends Scope 'documentSecurity' => true, ]); $this->assertEquals(201, $private['headers']['status-code']); - $this->collections['private'] = $private['body']['$id']; - $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $this->collections['private'] . '/attributes/string', $this->getServerHeader(), [ + $response = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $this->collections['private'] . '/attributes/string', $this->getServerHeader(), [ + 'key' => 'title', + 'size' => 256, + 'required' => true, + ]); + $this->assertEquals(202, $response['headers']['status-code']); + + $doconly = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections', $this->getServerHeader(), [ + 'collectionId' => ID::unique(), + 'name' => 'Document Only Movies', + 'permissions' => [], + 'documentSecurity' => true, + ]); + $this->assertEquals(201, $private['headers']['status-code']); + $this->collections['doconly'] = $doconly['body']['$id']; + + $response = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $this->collections['doconly'] . '/attributes/string', $this->getServerHeader(), [ 'key' => 'title', 'size' => 256, 'required' => true, @@ -118,9 +188,9 @@ class DatabasesPermissionsMemberTest extends Scope /** * Data provider params are passed before test dependencies * @dataProvider permissionsProvider - * @depends testSetupDatabase + * @depends testSetupDatabase */ - public function testReadDocuments($permissions, $data) + public function testReadDocuments($permissions, $anyCount, $usersCount, $docOnlyCount, $data) { $users = $data['users']; $collections = $data['collections']; @@ -144,66 +214,52 @@ class DatabasesPermissionsMemberTest extends Scope ]); $this->assertEquals(201, $response['headers']['status-code']); + $response = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collections['doconly'] . '/documents', $this->getServerHeader(), [ + 'documentId' => ID::unique(), + 'data' => [ + 'title' => 'Lorem', + ], + 'permissions' => $permissions + ]); + $this->assertEquals(201, $response['headers']['status-code']); + /** - * Check "any" collection + * Check "any" permission collection */ - $documents = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $collections['public'] . '/documents', [ + $documents = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $collections['public'] . '/documents', [ 'origin' => 'http://localhost', 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $users['user1']['session'], ]); - foreach ($documents['body']['documents'] as $document) { - $hasPermissions = \array_reduce([ - Role::any()->toString(), - Role::users()->toString(), - Role::user($users['user1']['$id'])->toString(), - ], function (bool $carry, string $role) use ($document) { - if ($carry) { - return true; - } - foreach ($document['$permissions'] as $permission) { - $permission = Permission::parse($permission); - if ($permission->getPermission() == 'read' && $permission->getRole() == $role) { - return true; - } - } - return false; - }, false); - - $this->assertTrue($hasPermissions); - } + $this->assertEquals(200, $documents['headers']['status-code']); + $this->assertEquals($anyCount, $documents['body']['total']); /** - * Check role:member collection + * Check "users" permission collection */ - $documents = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $collections['private'] . '/documents', [ + $documents = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $collections['private'] . '/documents', [ 'origin' => 'http://localhost', 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $users['user1']['session'], ]); - foreach ($documents['body']['documents'] as $document) { - $hasPermissions = \array_reduce([ - Role::any()->toString(), - Role::users()->toString(), - Role::user($users['user1']['$id'])->toString(), - ], function (bool $carry, string $role) use ($document) { - if ($carry) { - return true; - } - foreach ($document['$permissions'] as $permission) { - $permission = Permission::parse($permission); - if ($permission->getPermission() == 'read' && $permission->getRole() == $role) { - return true; - } - } - return false; - }, false); + $this->assertEquals(200, $documents['headers']['status-code']); + $this->assertEquals($usersCount, $documents['body']['total']); - $this->assertTrue($hasPermissions); - } + /** + * Check "user:user1" document only permission collection + */ + $documents = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $collections['doconly'] . '/documents', [ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $users['user1']['session'], + ]); + + $this->assertEquals(200, $documents['headers']['status-code']); + $this->assertEquals($docOnlyCount, $documents['body']['total']); } }