diff --git a/app/controllers/api/databases.php b/app/controllers/api/databases.php index 3c4c718aa1..fd9cda854a 100644 --- a/app/controllers/api/databases.php +++ b/app/controllers/api/databases.php @@ -1,5 +1,6 @@ getId() : $collectionId; + $permissions = PermissionsProcessor::handleAggregates($permissions); + try { $dbForProject->createDocument('database_' . $database->getInternalId(), new Document([ '$id' => $collectionId, @@ -771,6 +774,7 @@ App::put('/v1/databases/:databaseId/collections/:collectionId') } $permissions ??= $collection->getPermissions() ?? []; + $permissions = PermissionsProcessor::handleAggregates($permissions); $enabled ??= $collection->getAttribute('enabled', true); try { @@ -1787,8 +1791,8 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId/indexes/:key') ->setParam('databaseId', $databaseId) ->setParam('collectionId', $collection->getId()) ->setParam('indexId', $index->getId()) - ->setContext('collection', $collection) - ->setContext('database', $db) + ->setContext('collection', $collection) + ->setContext('database', $db) ->setPayload($response->output($index, Response::MODEL_INDEX)) ; @@ -1855,35 +1859,20 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/documents') } } - // Check collection permissions when enforced - if (!$collection->getAttribute('documentSecurity', false)) { - $validator = new Authorization('create'); - if (!$validator->isValid($collection->getCreate())) { - throw new Exception('Unauthorized permissions', 401, Exception::USER_UNAUTHORIZED); - } + $permissions = PermissionsProcessor::addDefaultsIfNeeded($permissions, $user->getId()); + $permissions = PermissionsProcessor::handleAggregates($permissions); + + $validator = new Authorization('create'); + $valid = $validator->isValid($collection->getCreate()); + if ($collection->getAttribute('documentSecurity', false)) { + $valid |= $validator->isValid($permissions); + } + if (!$valid) { + throw new Exception('Unauthorized permissions', 401, Exception::USER_UNAUTHORIZED); } $data['$collection'] = $collection->getId(); // Adding this param to make API easier for developers $data['$id'] = $documentId == 'unique()' ? $dbForProject->getId() : $documentId; - - if (\is_null($permissions)) { - $permissions = []; - if (!$user->isEmpty()) { - $permissions = [ - 'read(user:' . $user->getId() . ') ', - 'write(user:' . $user->getId() . ') ', - ]; - } - } elseif (empty(\preg_grep('#^read\(.+\)$#', $permissions))) { - if (!$user->isEmpty()) { - $permissions[] = 'read(user:' . $user->getId() . ')'; - } - } elseif (empty(\preg_grep('#^write\(.+\)$#', $permissions))) { - if (!$user->isEmpty()) { - $permissions[] = 'write(user:' . $user->getId() . ')'; - } - } - $data['$permissions'] = $permissions; // Users can only add their roles to documents, API keys and Admin users can add any @@ -1892,24 +1881,20 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/documents') if (!Auth::isAppUser($roles) && !Auth::isPrivilegedUser($roles)) { foreach ($permissions as $permission) { $matches = []; - if (\preg_match('/^(create|write)\((.+)(?:,(.+))*\)$/', $permission, $matches)) { - \array_shift($matches); - foreach ($matches as $match) { - if (!Authorization::isRole($match)) { - throw new Exception('Permissions must be one of: (' . \implode(', ', $roles) . ')', 400, Exception::USER_UNAUTHORIZED); - } + if (!\preg_match('#^create\((.+)(?:,(.+))*\)$#', $permission, $matches)) { + continue; + } + \array_shift($matches); + foreach ($matches as $match) { + if (!Authorization::isRole($match)) { + throw new Exception('Permissions must be one of: (' . \implode(', ', $roles) . ')', 400, Exception::USER_UNAUTHORIZED); } } } } try { - if ($collection->getAttribute('permission') === 'collection') { - /** @var Document $document */ - $document = Authorization::skip(fn() => $dbForProject->createDocument('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), new Document($data))); - } else { - $document = $dbForProject->createDocument('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), new Document($data)); - } + $document = $dbForProject->createDocument('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), new Document($data)); $document->setAttribute('$collection', $collectionId); } catch (StructureException $exception) { throw new Exception($exception->getMessage(), 400, Exception::DOCUMENT_INVALID_STRUCTURE); @@ -1985,12 +1970,11 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents') } } - // Check collection permissions when enforced - if ($collection->getAttribute('permission') === 'collection') { - $validator = new Authorization('read'); - if (!$validator->isValid($collection->getRead())) { - throw new Exception('Unauthorized permissions', 401, Exception::USER_UNAUTHORIZED); - } + // Check collection permissions + $validator = new Authorization('create'); + $valid = $validator->isValid($collection->getCreate()); + if (!$valid) { + throw new Exception('Unauthorized permissions', 401, Exception::USER_UNAUTHORIZED); } $queries = \array_map(function ($query) { @@ -2019,23 +2003,15 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents') $cursorDocument = null; if (!empty($cursor)) { - $cursorDocument = $collection->getAttribute('permission') === 'collection' - ? Authorization::skip(fn () => $dbForProject->getDocument('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $cursor)) - : $dbForProject->getDocument('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $cursor); - + $cursorDocument = Authorization::skip(fn () => $dbForProject->getDocument('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $cursor)); if ($cursorDocument->isEmpty()) { throw new Exception("Document '{$cursor}' for the 'cursor' value not found.", 400, Exception::GENERAL_CURSOR_NOT_FOUND); } } - if ($collection->getAttribute('permission') === 'collection') { - /** @var Document[] $documents */ - $documents = Authorization::skip(fn() => $dbForProject->find('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $queries, $limit, $offset, $orderAttributes, $orderTypes, $cursorDocument ?? null, $cursorDirection)); - $total = Authorization::skip(fn() => $dbForProject->count('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $queries, APP_LIMIT_COUNT)); - } else { - $documents = $dbForProject->find('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $queries, $limit, $offset, $orderAttributes, $orderTypes, $cursorDocument ?? null, $cursorDirection); - $total = $dbForProject->count('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $queries, APP_LIMIT_COUNT); - } + /** @var Document[] $documents */ + $documents = Authorization::skip(fn() => $dbForProject->find('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $queries, $limit, $offset, $orderAttributes, $orderTypes, $cursorDocument ?? null, $cursorDirection)); + $total = Authorization::skip(fn() => $dbForProject->count('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $queries, APP_LIMIT_COUNT)); /** * Reset $collection attribute to remove prefix. @@ -2091,20 +2067,14 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents/:documen } } - // Check collection permissions when enforced - if ($collection->getAttribute('permission') === 'collection') { - $validator = new Authorization('read'); - if (!$validator->isValid($collection->getRead())) { - throw new Exception('Unauthorized permissions', 401, Exception::USER_UNAUTHORIZED); - } + // Check collection permissions + $validator = new Authorization('create'); + $valid = $validator->isValid($collection->getCreate()); + if (!$valid) { + throw new Exception('Unauthorized permissions', 401, Exception::USER_UNAUTHORIZED); } - if ($collection->getAttribute('permission') === 'collection') { - /** @var Document $document */ - $document = Authorization::skip(fn() => $dbForProject->getDocument('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $documentId)); - } else { - $document = $dbForProject->getDocument('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $documentId); - } + $document = Authorization::skip(fn() => $dbForProject->getDocument('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $documentId)); /** * Reset $collection attribute to remove prefix. @@ -2260,18 +2230,13 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/documents/:docum } } - // Check collection permissions when enforced - if ($collection->getAttribute('permission') === 'collection') { - $validator = new Authorization('write'); - if (!$validator->isValid($collection->getWrite())) { - throw new Exception('Unauthorized permissions', 401, Exception::USER_UNAUTHORIZED); - } - - $document = Authorization::skip(fn() => $dbForProject->getDocument('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $documentId)); - } else { - $document = $dbForProject->getDocument('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $documentId); + // Check collection permissions + $validator = new Authorization('write'); + if (!$validator->isValid($collection->getWrite())) { + throw new Exception('Unauthorized permissions', 401, Exception::USER_UNAUTHORIZED); } + $document = Authorization::skip(fn() => $dbForProject->getDocument('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $documentId)); if ($document->isEmpty()) { throw new Exception('Document not found', 404, Exception::DOCUMENT_NOT_FOUND); @@ -2397,30 +2362,19 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId/documents/:docu } } - // Check collection permissions when enforced - if ($collection->getAttribute('permission') === 'collection') { - $validator = new Authorization('write'); - if (!$validator->isValid($collection->getWrite())) { - throw new Exception('Unauthorized permissions', 401, Exception::USER_UNAUTHORIZED); - } + // Check collection permissions + $validator = new Authorization('write'); + if (!$validator->isValid($collection->getWrite())) { + throw new Exception('Unauthorized permissions', 401, Exception::USER_UNAUTHORIZED); } - if ($collection->getAttribute('permission') === 'collection') { - /** @var Document $document */ - $document = Authorization::skip(fn() => $dbForProject->getDocument('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $documentId)); - } else { - $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('No document found', 404, Exception::DOCUMENT_NOT_FOUND); } - if ($collection->getAttribute('permission') === 'collection') { - Authorization::skip(fn() => $dbForProject->deleteDocument('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $documentId)); - } else { - $dbForProject->deleteDocument('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $documentId); - } + Authorization::skip(fn() => $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 81e255a414..3561181735 100644 --- a/app/controllers/api/storage.php +++ b/app/controllers/api/storage.php @@ -5,6 +5,7 @@ use Appwrite\ClamAV\Network; use Appwrite\Event\Audit; 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; @@ -357,41 +358,21 @@ App::post('/v1/storage/buckets/:bucketId/files') ) { throw new Exception('Bucket not found', 404, Exception::STORAGE_BUCKET_NOT_FOUND); } - - // Check bucket permissions when enforced - $permissionBucket = $bucket->getAttribute('permission') === 'bucket'; - if ($permissionBucket) { - $validator = new Authorization('write'); - if (!$validator->isValid($bucket->getWrite())) { - throw new Exception('Unauthorized permissions', 401, Exception::USER_UNAUTHORIZED); - } + + $permissions = PermissionsProcessor::addDefaultsIfNeeded($permissions, $user->getId()); + + $validator = new Authorization('write'); + if (!$validator->isValid($bucket->getWrite())) { + throw new Exception('Unauthorized permissions', 401, Exception::USER_UNAUTHORIZED); } - - if (\is_null($permissions)) { - $permissions = []; - if (!$user->isEmpty()) { - $permissions = [ - 'read(user:' . $user->getId() . ') ', - 'write(user:' . $user->getId() . ') ', - ]; - } - } elseif (empty(\preg_grep('#^read\(.+\)$#', $permissions))) { - if (!$user->isEmpty()) { - $permissions[] = 'read(user:' . $user->getId() . ')'; - } - } elseif (empty(\preg_grep('#^write\(.+\)$#', $permissions))) { - if (!$user->isEmpty()) { - $permissions[] = 'write(user:' . $user->getId() . ')'; - } - } - + // Users can only add their roles to files, API keys and Admin users can add any $roles = Authorization::getRoles(); if (!Auth::isAppUser($roles) && !Auth::isPrivilegedUser($roles)) { foreach ($permissions as $permission) { $matches = []; - if (\preg_match('/^(create|write)\((.+)(?:,(.+))*\)$/', $permission, $matches)) { + if (\preg_match('/^create\((.+)(?:,(.+))*\)$/', $permission, $matches)) { \array_shift($matches); foreach ($matches as $match) { if (!Authorization::isRole($match)) { @@ -699,13 +680,10 @@ App::get('/v1/storage/buckets/:bucketId/files') ) { throw new Exception('Bucket not found', 404, Exception::STORAGE_BUCKET_NOT_FOUND); } - - // Check bucket permissions when enforced - if ($bucket->getAttribute('permission') === 'bucket') { - $validator = new Authorization('read'); - if (!$validator->isValid($bucket->getRead())) { - throw new Exception('Unauthorized permissions', 401, Exception::USER_UNAUTHORIZED); - } + + $validator = new Authorization('read'); + if (!$validator->isValid($bucket->getRead())) { + throw new Exception('Unauthorized permissions', 401, Exception::USER_UNAUTHORIZED); } $queries = [new Query('bucketId', Query::TYPE_EQUAL, [$bucketId])]; @@ -731,12 +709,8 @@ App::get('/v1/storage/buckets/:bucketId/files') if (!empty($search)) { $queries[] = new Query('search', Query::TYPE_SEARCH, [$search]); } - - if ($bucket->getAttribute('permission') === 'bucket') { - $files = Authorization::skip(fn () => $dbForProject->find('bucket_' . $bucket->getInternalId(), $queries, $limit, $offset, [], [$orderType], $cursorFile ?? null, $cursorDirection)); - } else { - $files = $dbForProject->find('bucket_' . $bucket->getInternalId(), $queries, $limit, $offset, [], [$orderType], $cursorFile ?? null, $cursorDirection); - } + + $files = Authorization::skip(fn () => $dbForProject->find('bucket_' . $bucket->getInternalId(), $queries, $limit, $offset, [], [$orderType], $cursorFile ?? null, $cursorDirection)); $usage ->setParam('storage.files.read', 1) @@ -785,12 +759,8 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId') throw new Exception('Unauthorized permissions', 401, Exception::USER_UNAUTHORIZED); } } - - if ($bucket->getAttribute('permission') === 'bucket') { - $file = Authorization::skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId)); - } else { - $file = $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId); - } + + $file = Authorization::skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId)); if ($file->isEmpty() || $file->getAttribute('bucketId') !== $bucketId) { throw new Exception('File not found', 404, Exception::STORAGE_FILE_NOT_FOUND); @@ -870,13 +840,8 @@ 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); - - if ($bucket->getAttribute('permission') === 'bucket') { - // skip authorization - $file = Authorization::skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId)); - } else { - $file = $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId); - } + + $file = Authorization::skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId)); if ($file->isEmpty() || $file->getAttribute('bucketId') !== $bucketId) { throw new Exception('File not found', 404, Exception::STORAGE_FILE_NOT_FOUND); @@ -1028,11 +993,7 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/download') } } - if ($bucket->getAttribute('permission') === 'bucket') { - $file = Authorization::skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId)); - } else { - $file = $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId); - } + $file = Authorization::skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId)); if ($file->isEmpty() || $file->getAttribute('bucketId') !== $bucketId) { throw new Exception('File not found', 404, Exception::STORAGE_FILE_NOT_FOUND);