mirror of
https://github.com/appwrite/appwrite.git
synced 2026-05-26 13:51:13 +00:00
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:
@@ -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);
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user