1
0
Fork 0
mirror of synced 2024-06-29 19:50:26 +12:00

Merge branch 'refactor-permissions-inc-console-fix' into feat-list-users-queries

This commit is contained in:
Steven 2022-08-24 18:52:39 +00:00
commit fc38771d69
12 changed files with 515 additions and 366 deletions

View file

@ -508,11 +508,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([
@ -528,9 +525,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);
}
@ -777,10 +774,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);
@ -1878,7 +1873,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);
@ -1892,25 +1887,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())) {
@ -1918,19 +1909,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)) {
@ -2025,9 +2005,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);
}
@ -2076,11 +2056,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.");
}
@ -2151,25 +2130,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.
*/
@ -2325,25 +2301,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)) {
@ -2365,11 +2334,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() ?? [];
@ -2382,14 +2352,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) {
@ -2452,27 +2424,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);
/**

View file

@ -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(

View file

@ -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) {

View file

@ -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()),

View file

@ -134,7 +134,7 @@ $cli
(new Delete())
->setType(DELETE_TYPE_CACHE_BY_TIMESTAMP)
->setTimestamp(time() - $interval)
->setDatetime(DateTime::addSeconds(new \DateTime(), -1 * $interval))
->trigger();
}

View file

@ -51,7 +51,7 @@
"utopia-php/cache": "0.6.*",
"utopia-php/cli": "0.13.*",
"utopia-php/config": "0.2.*",
"utopia-php/database": "0.23.*",
"utopia-php/database": "dev-refactor-permissions as 0.23.0",
"utopia-php/locale": "0.4.*",
"utopia-php/registry": "0.5.*",
"utopia-php/preloader": "0.2.*",

23
composer.lock generated
View file

@ -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": "0e850206a924d2a48861ecd290f59bc0",
"content-hash": "64351ec59c6d50023ef9f6195777709b",
"packages": [
{
"name": "adhocore/jwt",
@ -2052,16 +2052,16 @@
},
{
"name": "utopia-php/database",
"version": "0.23.0",
"version": "dev-refactor-permissions",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/database.git",
"reference": "e4a23f8e26a3d96f292e549cebe0ff6937ec5d71"
"reference": "44dda6914c7be148eb59ce11847386ce39f7b106"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/database/zipball/e4a23f8e26a3d96f292e549cebe0ff6937ec5d71",
"reference": "e4a23f8e26a3d96f292e549cebe0ff6937ec5d71",
"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.23.0"
"source": "https://github.com/utopia-php/database/tree/refactor-permissions"
},
"time": "2022-08-18T19:08:33+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.23.0",
"alias_normalized": "0.23.0.0"
}
],
"minimum-stability": "stable",
"stability-flags": {
"utopia-php/database": 20,
"appwrite/sdk-generator": 20
},
"prefer-stable": false,
@ -5383,5 +5390,5 @@
"platform-overrides": {
"php": "8.0"
},
"plugin-api-version": "2.3.0"
"plugin-api-version": "2.2.0"
}

View file

@ -1,62 +0,0 @@
<?php
namespace Appwrite\Permissions;
use Utopia\Database\Database;
use Utopia\Database\Permission;
class PermissionsProcessor
{
public static function aggregate(?array $permissions, string $resource): ?array
{
if (\is_null($permissions)) {
return null;
}
$aggregates = self::getAggregates($resource);
foreach ($permissions as $i => $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;
}
}

View file

@ -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();

View file

@ -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']);
}

View file

@ -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);
}
}
}

View file

@ -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']);
}
}