1
0
Fork 0
mirror of synced 2024-05-20 04:32:37 +12:00

fix: collection level permissions

This commit is contained in:
Torsten Dittmann 2021-12-16 19:12:06 +01:00
parent 30ed41c21d
commit 166c906d51
7 changed files with 311 additions and 56 deletions

View file

@ -1589,13 +1589,15 @@ App::post('/v1/database/collections/:collectionId/documents')
->inject('user')
->inject('audits')
->inject('usage')
->action(function ($documentId, $collectionId, $data, $read, $write, $response, $dbForInternal, $dbForExternal, $user, $audits, $usage) {
->inject('events')
->action(function ($documentId, $collectionId, $data, $read, $write, $response, $dbForInternal, $dbForExternal, $user, $audits, $usage, $events) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForInternal */
/** @var Utopia\Database\Database $dbForExternal */
/** @var Utopia\Database\Document $user */
/** @var Appwrite\Event\Event $audits */
/** @var Appwrite\Stats\Stats $usage */
/** @var Appwrite\Event\Event $events */
$data = (\is_string($data)) ? \json_decode($data, true) : $data; // Cast to JSON array
@ -1607,7 +1609,10 @@ App::post('/v1/database/collections/:collectionId/documents')
throw new Exception('$id is not allowed for creating new documents, try update instead', 400);
}
$collection = $dbForInternal->getDocument('collections', $collectionId);
/**
* Skip Authorization to get the collection. Needed in case of empty permissions for document level permissions.
*/
$collection = Authorization::skip(fn() => $dbForInternal->getDocument('collections', $collectionId));
if ($collection->isEmpty()) {
throw new Exception('Collection not found', 404);
@ -1659,6 +1664,8 @@ App::post('/v1/database/collections/:collectionId/documents')
throw new Exception('Document already exists', 409);
}
$events->setParam('collection', $collection->getArrayCopy());
$usage
->setParam('database.documents.create', 1)
->setParam('collectionId', $collectionId)
@ -1703,7 +1710,10 @@ App::get('/v1/database/collections/:collectionId/documents')
/** @var Utopia\Database\Database $dbForExternal */
/** @var Appwrite\Stats\Stats $usage */
$collection = $dbForInternal->getDocument('collections', $collectionId);
/**
* Skip Authorization to get the collection. Needed in case of empty permissions for document level permissions.
*/
$collection = Authorization::skip(fn() => $dbForInternal->getDocument('collections', $collectionId));
if ($collection->isEmpty()) {
throw new Exception('Collection not found', 404);
@ -1739,11 +1749,11 @@ 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, $cursorDocument, $cursorDirection) {
return $dbForExternal->find($collectionId, $queries, $limit, $offset, $orderAttributes, $orderTypes, $cursorDocument ?? null, $cursorDirection);
});
$documents = Authorization::skip(fn() => $dbForExternal->find($collectionId, $queries, $limit, $offset, $orderAttributes, $orderTypes, $cursorDocument ?? null, $cursorDirection));
$sum = Authorization::skip(fn() => $dbForExternal->count($collectionId, $queries, APP_LIMIT_COUNT));
} else {
$documents = $dbForExternal->find($collectionId, $queries, $limit, $offset, $orderAttributes, $orderTypes, $cursorDocument ?? null, $cursorDirection);
$sum = $dbForExternal->count($collectionId, $queries, APP_LIMIT_COUNT);
}
$usage
@ -1752,7 +1762,7 @@ App::get('/v1/database/collections/:collectionId/documents')
;
$response->dynamic(new Document([
'sum' => $dbForExternal->count($collectionId, $queries, APP_LIMIT_COUNT),
'sum' => $sum,
'documents' => $documents,
]), Response::MODEL_DOCUMENT_LIST);
});
@ -1779,7 +1789,10 @@ App::get('/v1/database/collections/:collectionId/documents/:documentId')
/** @var Utopia\Database\Database $$dbForInternal */
/** @var Utopia\Database\Database $dbForExternal */
$collection = $dbForInternal->getDocument('collections', $collectionId);
/**
* Skip Authorization to get the collection. Needed in case of empty permissions for document level permissions.
*/
$collection = Authorization::skip(fn() => $dbForInternal->getDocument('collections', $collectionId));
if ($collection->isEmpty()) {
throw new Exception('Collection not found', 404);
@ -1930,14 +1943,19 @@ App::patch('/v1/database/collections/:collectionId/documents/:documentId')
->inject('dbForExternal')
->inject('audits')
->inject('usage')
->action(function ($collectionId, $documentId, $data, $read, $write, $response, $dbForInternal, $dbForExternal, $audits, $usage) {
->inject('events')
->action(function ($collectionId, $documentId, $data, $read, $write, $response, $dbForInternal, $dbForExternal, $audits, $usage, $events) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForInternal */
/** @var Utopia\Database\Database $dbForExternal */
/** @var Appwrite\Event\Event $audits */
/** @var Appwrite\Stats\Stats $usage */
/** @var Appwrite\Event\Event $events */
$collection = $dbForInternal->getDocument('collections', $collectionId);
/**
* Skip Authorization to get the collection. Needed in case of empty permissions for document level permissions.
*/
$collection = Authorization::skip(fn() => $dbForInternal->getDocument('collections', $collectionId));
if ($collection->isEmpty()) {
throw new Exception('Collection not found', 404);
@ -1949,9 +1967,12 @@ App::patch('/v1/database/collections/:collectionId/documents/:documentId')
if (!$validator->isValid($collection->getWrite())) {
throw new Exception('Unauthorized permissions', 401);
}
$document = Authorization::skip(fn() => $dbForExternal->getDocument($collectionId, $documentId));
} else {
$document = $dbForExternal->getDocument($collectionId, $documentId);
}
$document = $dbForExternal->getDocument($collectionId, $documentId);
if ($document->isEmpty()) {
throw new Exception('Document not found', 404);
@ -2009,7 +2030,9 @@ App::patch('/v1/database/collections/:collectionId/documents/:documentId')
catch (StructureException $exception) {
throw new Exception($exception->getMessage(), 400);
}
$events->setParam('collection', $collection->getArrayCopy());
$usage
->setParam('database.documents.update', 1)
->setParam('collectionId', $collectionId)
@ -2050,7 +2073,10 @@ App::delete('/v1/database/collections/:collectionId/documents/:documentId')
/** @var Appwrite\Event\Event $audits */
/** @var Appwrite\Stats\Stats $usage */
$collection = $dbForInternal->getDocument('collections', $collectionId);
/**
* Skip Authorization to get the collection. Needed in case of empty permissions for document level permissions.
*/
$collection = Authorization::skip(fn() => $dbForInternal->getDocument('collections', $collectionId));
if ($collection->isEmpty()) {
throw new Exception('Collection not found', 404);
@ -2077,7 +2103,12 @@ App::delete('/v1/database/collections/:collectionId/documents/:documentId')
throw new Exception('No document found', 404);
}
$dbForExternal->deleteDocument($collectionId, $documentId);
if ($collection->getAttribute('permission') === 'collection') {
Authorization::skip(fn() => $dbForExternal->deleteDocument($collectionId, $documentId));
} else {
$dbForExternal->deleteDocument($collectionId, $documentId);
}
$dbForExternal->deleteCachedDocument($collectionId, $documentId);
$usage
@ -2087,6 +2118,7 @@ App::delete('/v1/database/collections/:collectionId/documents/:documentId')
$events
->setParam('eventData', $response->output($document, Response::MODEL_DOCUMENT))
->setParam('collection', $collection->getArrayCopy());
;
$audits

View file

@ -207,7 +207,14 @@ App::shutdown(function ($utopia, $request, $response, $project, $events, $audits
if ($project->getId() !== 'console') {
$payload = new Document($response->getPayload());
$target = Realtime::fromPayload($events->getParam('event'), $payload, $project);
$collection = new Document($events->getParam('collection'));
$target = Realtime::fromPayload(
event: $events->getParam('event'),
payload: $payload,
project: $project,
collection: $collection
);
Realtime::send(
$target['projectId'] ?? $project->getId(),

View file

@ -240,7 +240,7 @@ class Realtime extends Adapter
* @param Document|null $project
* @return array
*/
public static function fromPayload(string $event, Document $payload, Document $project = null): array
public static function fromPayload(string $event, Document $payload, Document $project = null, Document $collection = null): array
{
$channels = [];
$roles = [];
@ -275,12 +275,6 @@ class Realtime extends Adapter
$channels[] = 'teams.' . $payload->getId();
$roles = ['team:' . $payload->getId()];
break;
case strpos($event, 'database.collections.') === 0:
$channels[] = 'collections';
$channels[] = 'collections.' . $payload->getId();
$roles = $payload->getRead();
break;
case strpos($event, 'database.attributes.') === 0:
case strpos($event, 'database.indexes.') === 0:
@ -290,10 +284,15 @@ class Realtime extends Adapter
break;
case strpos($event, 'database.documents.') === 0:
if ($collection->isEmpty()) {
throw new \Exception('Collection need to be passed to to Realtime for Document events in the Database.');
}
$channels[] = 'documents';
$channels[] = 'collections.' . $payload->getAttribute('$collection') . '.documents';
$channels[] = 'documents.' . $payload->getId();
$roles = $payload->getRead();
$roles = ($collection->getAttribute('permission') === 'collection') ? $collection->getRead() : $payload->getRead();
break;
case strpos($event, 'storage.') === 0:

View file

@ -19,8 +19,8 @@ trait DatabaseBase
]), [
'collectionId' => 'unique()',
'name' => 'Movies',
'read' => ['role:all'],
'write' => ['role:all'],
'read' => [],
'write' => [],
'permission' => 'document',
]);
@ -113,8 +113,8 @@ trait DatabaseBase
]), [
'collectionId' => 'unique()',
'name' => 'Response Models',
'read' => ['role:all'],
'write' => ['role:all'],
'read' => [],
'write' => [],
'permission' => 'document',
]);
@ -1229,8 +1229,8 @@ trait DatabaseBase
]), [
'collectionId' => 'unique()',
'name' => 'invalidDocumentStructure',
'read' => ['role:all'],
'write' => ['role:all'],
'read' => [],
'write' => [],
'permission' => 'document',
]);
@ -1829,13 +1829,41 @@ trait DatabaseBase
$this->assertEquals(201, $document1['headers']['status-code']);
$document2 = $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' => [],
'write' => [$user],
]);
$this->assertEquals(201, $document2['headers']['status-code']);
$document3 = $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' => [],
'write' => [],
]);
$this->assertEquals(201, $document3['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']);
$this->assertEquals(3, $documents['body']['sum']);
$this->assertCount(3, $documents['body']['documents']);
/*
* Test for Failure
@ -1894,7 +1922,7 @@ trait DatabaseBase
'x-appwrite-project' => $this->getProject()['$id'],
]));
$this->assertEquals(404, $documents['headers']['status-code']);
$this->assertEquals(401, $documents['headers']['status-code']);
}
/**

View file

@ -160,7 +160,7 @@ class DatabasePermissionsTeamTest extends Scope
if ($success) {
$this->assertCount(1, $documents['body']['documents']);
} else {
$this->assertEquals(404, $documents['headers']['status-code']);
$this->assertEquals(401, $documents['headers']['status-code']);
}
}

View file

@ -77,9 +77,7 @@ class RealtimeCustomClientTest extends Scope
'files',
'files.1',
'collections',
'collections.1',
'collections.1.documents',
'collections.2',
'collections.2.documents',
'documents',
'documents.1',
@ -93,15 +91,13 @@ class RealtimeCustomClientTest extends Scope
$this->assertEquals('connected', $response['type']);
$this->assertNotEmpty($response['data']);
$this->assertNotEmpty($response['data']['user']);
$this->assertCount(12, $response['data']['channels']);
$this->assertCount(10, $response['data']['channels']);
$this->assertContains('account', $response['data']['channels']);
$this->assertContains('account.' . $userId, $response['data']['channels']);
$this->assertContains('files', $response['data']['channels']);
$this->assertContains('files.1', $response['data']['channels']);
$this->assertContains('collections', $response['data']['channels']);
$this->assertContains('collections.1', $response['data']['channels']);
$this->assertContains('collections.1.documents', $response['data']['channels']);
$this->assertContains('collections.2', $response['data']['channels']);
$this->assertContains('collections.2.documents', $response['data']['channels']);
$this->assertContains('documents', $response['data']['channels']);
$this->assertContains('documents.1', $response['data']['channels']);
@ -561,24 +557,11 @@ class RealtimeCustomClientTest extends Scope
]), [
'collectionId' => 'unique()',
'name' => 'Actors',
'read' => ['role:all'],
'write' => ['role:all'],
'permission' => 'collection'
'read' => [],
'write' => [],
'permission' => 'document'
]);
$response = json_decode($client->receive(), true);
$this->assertArrayHasKey('type', $response);
$this->assertArrayHasKey('data', $response);
$this->assertEquals('event', $response['type']);
$this->assertNotEmpty($response['data']);
$this->assertArrayHasKey('timestamp', $response['data']);
$this->assertCount(2, $response['data']['channels']);
$this->assertContains('collections', $response['data']['channels']);
$this->assertContains('collections.' . $actors['body']['$id'], $response['data']['channels']);
$this->assertEquals('database.collections.create', $response['data']['event']);
$this->assertNotEmpty($response['data']['payload']);
$data = ['actorsId' => $actors['body']['$id']];
$name = $this->client->call(Client::METHOD_POST, '/database/collections/' . $data['actorsId'] . '/attributes/string', array_merge([
@ -662,7 +645,6 @@ class RealtimeCustomClientTest extends Scope
$this->assertEquals($response['data']['payload']['name'], 'Chris Evans 2');
/**
* Test Document Delete
*/
@ -703,6 +685,166 @@ class RealtimeCustomClientTest extends Scope
$client->close();
}
public function testChannelDatabaseCollectionPermissions()
{
$user = $this->getUser();
$session = $user['session'] ?? '';
$projectId = $this->getProject()['$id'];
$client = $this->getWebsocket(['documents', 'collections'], [
'origin' => 'http://localhost',
'cookie' => 'a_session_'.$projectId.'=' . $session
]);
$response = json_decode($client->receive(), true);
$this->assertArrayHasKey('type', $response);
$this->assertArrayHasKey('data', $response);
$this->assertEquals('connected', $response['type']);
$this->assertNotEmpty($response['data']);
$this->assertCount(2, $response['data']['channels']);
$this->assertContains('documents', $response['data']['channels']);
$this->assertContains('collections', $response['data']['channels']);
$this->assertNotEmpty($response['data']['user']);
$this->assertEquals($user['$id'], $response['data']['user']['$id']);
/**
* Test Collection Create
*/
$actors = $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' => 'Actors',
'read' => ['role:all'],
'write' => ['role:all'],
'permission' => 'collection'
]);
$data = ['actorsId' => $actors['body']['$id']];
$name = $this->client->call(Client::METHOD_POST, '/database/collections/' . $data['actorsId'] . '/attributes/string', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'attributeId' => 'name',
'size' => 256,
'required' => true,
]);
$this->assertEquals($name['headers']['status-code'], 201);
$this->assertEquals($name['body']['key'], 'name');
$this->assertEquals($name['body']['type'], 'string');
$this->assertEquals($name['body']['size'], 256);
$this->assertEquals($name['body']['required'], true);
sleep(2);
/**
* Test Document Create
*/
$document = $this->client->call(Client::METHOD_POST, '/database/collections/' . $data['actorsId'] . '/documents', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'documentId' => 'unique()',
'data' => [
'name' => 'Chris Evans'
],
'read' => [],
'write' => [],
]);
$response = json_decode($client->receive(), true);
$this->assertArrayHasKey('type', $response);
$this->assertArrayHasKey('data', $response);
$this->assertEquals('event', $response['type']);
$this->assertNotEmpty($response['data']);
$this->assertArrayHasKey('timestamp', $response['data']);
$this->assertCount(3, $response['data']['channels']);
$this->assertContains('documents', $response['data']['channels']);
$this->assertContains('documents.' . $document['body']['$id'], $response['data']['channels']);
$this->assertContains('collections.' . $actors['body']['$id'] . '.documents', $response['data']['channels']);
$this->assertEquals('database.documents.create', $response['data']['event']);
$this->assertNotEmpty($response['data']['payload']);
$this->assertEquals($response['data']['payload']['name'], 'Chris Evans');
$data['documentId'] = $document['body']['$id'];
/**
* Test Document Update
*/
$document = $this->client->call(Client::METHOD_PATCH, '/database/collections/' . $data['actorsId'] . '/documents/' . $data['documentId'], array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'data' => [
'name' => 'Chris Evans 2'
],
'read' => [],
'write' => [],
]);
$response = json_decode($client->receive(), true);
$this->assertArrayHasKey('type', $response);
$this->assertArrayHasKey('data', $response);
$this->assertEquals('event', $response['type']);
$this->assertNotEmpty($response['data']);
$this->assertArrayHasKey('timestamp', $response['data']);
$this->assertCount(3, $response['data']['channels']);
$this->assertContains('documents', $response['data']['channels']);
$this->assertContains('documents.' . $data['documentId'], $response['data']['channels']);
$this->assertContains('collections.' . $data['actorsId'] . '.documents', $response['data']['channels']);
$this->assertEquals('database.documents.update', $response['data']['event']);
$this->assertNotEmpty($response['data']['payload']);
$this->assertEquals($response['data']['payload']['name'], 'Chris Evans 2');
/**
* Test Document Delete
*/
$document = $this->client->call(Client::METHOD_POST, '/database/collections/' . $data['actorsId'] . '/documents', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'documentId' => 'unique()',
'data' => [
'name' => 'Bradley Cooper'
],
'read' => [],
'write' => [],
]);
$client->receive();
$this->client->call(Client::METHOD_DELETE, '/database/collections/' . $data['actorsId'] . '/documents/' . $document['body']['$id'], array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$response = json_decode($client->receive(), true);
$this->assertArrayHasKey('type', $response);
$this->assertArrayHasKey('data', $response);
$this->assertEquals('event', $response['type']);
$this->assertNotEmpty($response['data']);
$this->assertArrayHasKey('timestamp', $response['data']);
$this->assertCount(3, $response['data']['channels']);
$this->assertContains('documents', $response['data']['channels']);
$this->assertContains('documents.' . $document['body']['$id'], $response['data']['channels']);
$this->assertContains('collections.' . $data['actorsId'] . '.documents', $response['data']['channels']);
$this->assertEquals('database.documents.delete', $response['data']['event']);
$this->assertNotEmpty($response['data']['payload']);
$this->assertEquals($response['data']['payload']['name'], 'Bradley Cooper');
$client->close();
}
public function testChannelFiles()
{
$user = $this->getUser();

View file

@ -2,7 +2,7 @@
namespace Appwrite\Tests;
use Appwrite\Database\Document;
use Utopia\Database\Document;
use Appwrite\Messaging\Adapter\Realtime;
use PHPUnit\Framework\TestCase;
@ -195,4 +195,51 @@ class MessagingTest extends TestCase
$this->assertArrayHasKey('account', $channels);
$this->assertArrayNotHasKey('account.456', $channels);
}
public function testFromPayloadCollectionLevelPermissions(): void
{
/**
* Test Collection Level Permissions
*/
$result = Realtime::fromPayload(
event: 'database.documents.create',
payload: new Document([
'$id' => 'test',
'$collection' => 'collection',
'$read' => ['role:admin'],
'$write' => ['role:admin']
]),
collection: new Document([
'$id' => 'collection',
'$read' => ['role:all'],
'$write' => ['role:all'],
'permission' => 'collection'
])
);
$this->assertContains('role:all', $result['roles']);
$this->assertNotContains('role:admin', $result['roles']);
/**
* Test Document Level Permissions
*/
$result = Realtime::fromPayload(
event: 'database.documents.create',
payload: new Document([
'$id' => 'test',
'$collection' => 'collection',
'$read' => ['role:all'],
'$write' => ['role:all']
]),
collection: new Document([
'$id' => 'collection',
'$read' => ['role:admin'],
'$write' => ['role:admin'],
'permission' => 'document'
])
);
$this->assertContains('role:all', $result['roles']);
$this->assertNotContains('role:admin', $result['roles']);
}
}