Merge pull request #1480 from appwrite/feat-enforce-document-collection-permissions

feat(refactor-db) enforce either document or collection permissions
This commit is contained in:
Torsten Dittmann
2021-10-05 20:33:23 +00:00
committed by GitHub
2 changed files with 222 additions and 7 deletions
+83 -6
View File
@@ -15,6 +15,7 @@ use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Query;
use Utopia\Database\Adapter\MariaDB;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\Permissions;
use Utopia\Database\Validator\QueryValidator;
@@ -31,7 +32,6 @@ use Appwrite\Network\Validator\IP;
use Appwrite\Network\Validator\URL;
use Appwrite\Utopia\Response;
use DeviceDetector\DeviceDetector;
use Utopia\Database\Validator\Authorization;
/**
* Create attribute of varying type
@@ -1459,13 +1459,28 @@ App::post('/v1/database/collections/:collectionId/documents')
throw new Exception('Collection not found', 404);
}
// 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);
}
}
$data['$collection'] = $collection->getId(); // Adding this param to make API easier for developers
$data['$id'] = $documentId == 'unique()' ? $dbForExternal->getId() : $documentId;
$data['$read'] = (is_null($read) && !$user->isEmpty()) ? ['user:'.$user->getId()] : $read ?? []; // By default set read permissions for user
$data['$write'] = (is_null($write) && !$user->isEmpty()) ? ['user:'.$user->getId()] : $write ?? []; // By default set write permissions for user
try {
$document = $dbForExternal->createDocument($collectionId, new Document($data));
if ($collection->getAttribute('permission') === 'collection') {
/** @var Document $document */
$document = Authorization::skip(function() use ($dbForExternal, $collectionId, $data) {
return $dbForExternal->createDocument($collectionId, new Document($data));
});
} else {
$document = $dbForExternal->createDocument($collectionId, new Document($data));
}
}
catch (StructureException $exception) {
throw new Exception($exception->getMessage(), 400);
@@ -1523,6 +1538,14 @@ App::get('/v1/database/collections/:collectionId/documents')
throw new Exception('Collection not found', 404);
}
// 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);
}
}
$queries = \array_map(function ($query) {
return Query::parse($query);
}, $queries);
@@ -1542,6 +1565,15 @@ App::get('/v1/database/collections/:collectionId/documents')
}
}
if ($collection->getAttribute('permission') === 'collection') {
/** @var Document[] $documents */
$documents = Authorization::skip(function() use ($dbForExternal, $collectionId, $queries, $limit, $offset, $orderAttributes, $orderTypes, $afterDocument) {
return $dbForExternal->find($collectionId, $queries, $limit, $offset, $orderAttributes, $orderTypes, $afterDocument ?? null);
});
} else {
$documents = $dbForExternal->find($collectionId, $queries, $limit, $offset, $orderAttributes, $orderTypes, $afterDocument ?? null);
}
$usage
->setParam('database.documents.read', 1)
->setParam('collectionId', $collectionId)
@@ -1549,7 +1581,7 @@ App::get('/v1/database/collections/:collectionId/documents')
$response->dynamic(new Document([
'sum' => $dbForExternal->count($collectionId, $queries, APP_LIMIT_COUNT),
'documents' => $dbForExternal->find($collectionId, $queries, $limit, $offset, $orderAttributes, $orderTypes, $afterDocument ?? null),
'documents' => $documents,
]), Response::MODEL_DOCUMENT_LIST);
});
@@ -1581,7 +1613,22 @@ App::get('/v1/database/collections/:collectionId/documents/:documentId')
throw new Exception('Collection not found', 404);
}
$document = $dbForExternal->getDocument($collectionId, $documentId);
// 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);
}
}
if ($collection->getAttribute('permission') === 'collection') {
/** @var Document $document */
$document = Authorization::skip(function() use ($dbForExternal, $collectionId, $documentId) {
return $dbForExternal->getDocument($collectionId, $documentId);
});
} else {
$document = $dbForExternal->getDocument($collectionId, $documentId);
}
if ($document->isEmpty()) {
throw new Exception('No document found', 404);
@@ -1630,6 +1677,14 @@ App::patch('/v1/database/collections/:collectionId/documents/:documentId')
throw new Exception('Collection not found', 404);
}
// 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);
}
}
$document = $dbForExternal->getDocument($collectionId, $documentId);
if ($document->isEmpty()) {
@@ -1654,7 +1709,14 @@ App::patch('/v1/database/collections/:collectionId/documents/:documentId')
$data['$write'] = (is_null($write)) ? ($document->getWrite() ?? []) : $write; // By default inherit write permissions
try {
$document = $dbForExternal->updateDocument($collection->getId(), $document->getId(), new Document($data));
if ($collection->getAttribute('permission') === 'collection') {
/** @var Document $document */
$document = Authorization::skip(function() use ($dbForExternal, $collection, $document, $data) {
return $dbForExternal->updateDocument($collection->getId(), $document->getId(), new Document($data));
});
} else {
$document = $dbForExternal->updateDocument($collection->getId(), $document->getId(), new Document($data));
}
}
catch (AuthorizationException $exception) {
throw new Exception('Unauthorized permissions', 401);
@@ -1712,7 +1774,22 @@ App::delete('/v1/database/collections/:collectionId/documents/:documentId')
throw new Exception('Collection not found', 404);
}
$document = $dbForExternal->getDocument($collectionId, $documentId);
// 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);
}
}
if ($collection->getAttribute('permission') === 'collection') {
/** @var Document $document */
$document = Authorization::skip(function() use ($dbForExternal, $collectionId, $documentId) {
return $dbForExternal->getDocument($collectionId, $documentId);
});
} else {
$document = $dbForExternal->getDocument($collectionId, $documentId);
}
if ($document->isEmpty()) {
throw new Exception('No document found', 404);
+139 -1
View File
@@ -1167,7 +1167,7 @@ trait DatabaseBase
// $this->assertEquals('Minimum value must be lesser than maximum value', $invalidRange['body']['message']);
// wait for worker to add attributes
sleep(2);
sleep(3);
$collection = $this->client->call(Client::METHOD_GET, '/database/collections/' . $collectionId, array_merge([
'content-type' => 'application/json',
@@ -1503,6 +1503,144 @@ trait DatabaseBase
return $data;
}
public function testEnforceCollectionPermissions()
{
$user = 'user:' . $this->getUser()['$id'];
$collection = $this->client->call(Client::METHOD_POST, '/database/collections', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'collectionId' => 'unique()',
'name' => 'enforceCollectionPermissions',
'permission' => 'collection',
'read' => [$user],
'write' => [$user]
]);
$this->assertEquals($collection['headers']['status-code'], 201);
$this->assertEquals($collection['body']['name'], 'enforceCollectionPermissions');
$this->assertEquals($collection['body']['permission'], 'collection');
$collectionId = $collection['body']['$id'];
sleep(2);
$attribute = $this->client->call(Client::METHOD_POST, '/database/collections/' . $collectionId . '/attributes/string', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'attributeId' => 'attribute',
'size' => 64,
'required' => true,
]);
$this->assertEquals(201, $attribute['headers']['status-code'], 201);
$this->assertEquals('attribute', $attribute['body']['key']);
// wait for db to add attribute
sleep(2);
$index = $this->client->call(Client::METHOD_POST, '/database/collections/' . $collectionId . '/indexes', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'indexId' => 'key_attribute',
'type' => 'key',
'attributes' => [$attribute['body']['key']],
]);
$this->assertEquals(201, $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, '/database/collections/' . $collectionId . '/documents', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'documentId' => 'unique()',
'data' => [
'attribute' => 'one',
],
'read' => [$user],
'write' => [$user],
]);
$this->assertEquals(201, $document1['headers']['status-code']);
$documents = $this->client->call(Client::METHOD_GET, '/database/collections/' . $collectionId . '/documents', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(1, $documents['body']['sum']);
$this->assertCount(1, $documents['body']['documents']);
/*
* Test for Failure
*/
// Remove write permission
$collection = $this->client->call(Client::METHOD_PUT, '/database/collections/' . $collectionId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'name' => 'enforceCollectionPermissions',
'permission' => 'collection',
'read' => [$user],
'write' => []
]);
$this->assertEquals(200, $collection['headers']['status-code']);
$badDocument = $this->client->call(Client::METHOD_POST, '/database/collections/' . $collectionId . '/documents', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'documentId' => 'unique()',
'data' => [
'attribute' => 'bad',
],
'read' => [$user],
'write' => [$user],
]);
if($this->getSide() == 'client') {
$this->assertEquals(401, $badDocument['headers']['status-code']);
}
if($this->getSide() == 'server') {
$this->assertEquals(201, $badDocument['headers']['status-code']);
}
// Remove read permission
$collection = $this->client->call(Client::METHOD_PUT, '/database/collections/' . $collectionId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'name' => 'enforceCollectionPermissions',
'permission' => 'collection',
'read' => [],
'write' => []
]);
$this->assertEquals(200, $collection['headers']['status-code']);
$documents = $this->client->call(Client::METHOD_GET, '/database/collections/' . $collectionId . '/documents', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
]));
$this->assertEquals(404, $documents['headers']['status-code']);
}
/**
* @depends testDefaultPermissions
*/