Merge pull request #2506 from appwrite/fix-database-collection-level-permissions
fix: collection level permissions
This commit is contained in:
commit
0e84545829
|
@ -1589,13 +1589,15 @@ App::post('/v1/database/collections/:collectionId/documents')
|
||||||
->inject('user')
|
->inject('user')
|
||||||
->inject('audits')
|
->inject('audits')
|
||||||
->inject('usage')
|
->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 Appwrite\Utopia\Response $response */
|
||||||
/** @var Utopia\Database\Database $dbForInternal */
|
/** @var Utopia\Database\Database $dbForInternal */
|
||||||
/** @var Utopia\Database\Database $dbForExternal */
|
/** @var Utopia\Database\Database $dbForExternal */
|
||||||
/** @var Utopia\Database\Document $user */
|
/** @var Utopia\Database\Document $user */
|
||||||
/** @var Appwrite\Event\Event $audits */
|
/** @var Appwrite\Event\Event $audits */
|
||||||
/** @var Appwrite\Stats\Stats $usage */
|
/** @var Appwrite\Stats\Stats $usage */
|
||||||
|
/** @var Appwrite\Event\Event $events */
|
||||||
|
|
||||||
$data = (\is_string($data)) ? \json_decode($data, true) : $data; // Cast to JSON array
|
$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);
|
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()) {
|
if ($collection->isEmpty()) {
|
||||||
throw new Exception('Collection not found', 404);
|
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);
|
throw new Exception('Document already exists', 409);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$events->setParam('collection', $collection->getArrayCopy());
|
||||||
|
|
||||||
$usage
|
$usage
|
||||||
->setParam('database.documents.create', 1)
|
->setParam('database.documents.create', 1)
|
||||||
->setParam('collectionId', $collectionId)
|
->setParam('collectionId', $collectionId)
|
||||||
|
@ -1703,7 +1710,10 @@ App::get('/v1/database/collections/:collectionId/documents')
|
||||||
/** @var Utopia\Database\Database $dbForExternal */
|
/** @var Utopia\Database\Database $dbForExternal */
|
||||||
/** @var Appwrite\Stats\Stats $usage */
|
/** @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()) {
|
if ($collection->isEmpty()) {
|
||||||
throw new Exception('Collection not found', 404);
|
throw new Exception('Collection not found', 404);
|
||||||
|
@ -1739,11 +1749,11 @@ App::get('/v1/database/collections/:collectionId/documents')
|
||||||
|
|
||||||
if ($collection->getAttribute('permission') === 'collection') {
|
if ($collection->getAttribute('permission') === 'collection') {
|
||||||
/** @var Document[] $documents */
|
/** @var Document[] $documents */
|
||||||
$documents = Authorization::skip(function() use ($dbForExternal, $collectionId, $queries, $limit, $offset, $orderAttributes, $orderTypes, $cursorDocument, $cursorDirection) {
|
$documents = Authorization::skip(fn() => $dbForExternal->find($collectionId, $queries, $limit, $offset, $orderAttributes, $orderTypes, $cursorDocument ?? null, $cursorDirection));
|
||||||
return $dbForExternal->find($collectionId, $queries, $limit, $offset, $orderAttributes, $orderTypes, $cursorDocument ?? null, $cursorDirection);
|
$sum = Authorization::skip(fn() => $dbForExternal->count($collectionId, $queries, APP_LIMIT_COUNT));
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
$documents = $dbForExternal->find($collectionId, $queries, $limit, $offset, $orderAttributes, $orderTypes, $cursorDocument ?? null, $cursorDirection);
|
$documents = $dbForExternal->find($collectionId, $queries, $limit, $offset, $orderAttributes, $orderTypes, $cursorDocument ?? null, $cursorDirection);
|
||||||
|
$sum = $dbForExternal->count($collectionId, $queries, APP_LIMIT_COUNT);
|
||||||
}
|
}
|
||||||
|
|
||||||
$usage
|
$usage
|
||||||
|
@ -1752,7 +1762,7 @@ App::get('/v1/database/collections/:collectionId/documents')
|
||||||
;
|
;
|
||||||
|
|
||||||
$response->dynamic(new Document([
|
$response->dynamic(new Document([
|
||||||
'sum' => $dbForExternal->count($collectionId, $queries, APP_LIMIT_COUNT),
|
'sum' => $sum,
|
||||||
'documents' => $documents,
|
'documents' => $documents,
|
||||||
]), Response::MODEL_DOCUMENT_LIST);
|
]), 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 $$dbForInternal */
|
||||||
/** @var Utopia\Database\Database $dbForExternal */
|
/** @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()) {
|
if ($collection->isEmpty()) {
|
||||||
throw new Exception('Collection not found', 404);
|
throw new Exception('Collection not found', 404);
|
||||||
|
@ -1930,14 +1943,19 @@ App::patch('/v1/database/collections/:collectionId/documents/:documentId')
|
||||||
->inject('dbForExternal')
|
->inject('dbForExternal')
|
||||||
->inject('audits')
|
->inject('audits')
|
||||||
->inject('usage')
|
->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 Appwrite\Utopia\Response $response */
|
||||||
/** @var Utopia\Database\Database $dbForInternal */
|
/** @var Utopia\Database\Database $dbForInternal */
|
||||||
/** @var Utopia\Database\Database $dbForExternal */
|
/** @var Utopia\Database\Database $dbForExternal */
|
||||||
/** @var Appwrite\Event\Event $audits */
|
/** @var Appwrite\Event\Event $audits */
|
||||||
/** @var Appwrite\Stats\Stats $usage */
|
/** @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()) {
|
if ($collection->isEmpty()) {
|
||||||
throw new Exception('Collection not found', 404);
|
throw new Exception('Collection not found', 404);
|
||||||
|
@ -1949,9 +1967,12 @@ App::patch('/v1/database/collections/:collectionId/documents/:documentId')
|
||||||
if (!$validator->isValid($collection->getWrite())) {
|
if (!$validator->isValid($collection->getWrite())) {
|
||||||
throw new Exception('Unauthorized permissions', 401);
|
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()) {
|
if ($document->isEmpty()) {
|
||||||
throw new Exception('Document not found', 404);
|
throw new Exception('Document not found', 404);
|
||||||
|
@ -2010,6 +2031,8 @@ App::patch('/v1/database/collections/:collectionId/documents/:documentId')
|
||||||
throw new Exception($exception->getMessage(), 400);
|
throw new Exception($exception->getMessage(), 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$events->setParam('collection', $collection->getArrayCopy());
|
||||||
|
|
||||||
$usage
|
$usage
|
||||||
->setParam('database.documents.update', 1)
|
->setParam('database.documents.update', 1)
|
||||||
->setParam('collectionId', $collectionId)
|
->setParam('collectionId', $collectionId)
|
||||||
|
@ -2050,7 +2073,10 @@ App::delete('/v1/database/collections/:collectionId/documents/:documentId')
|
||||||
/** @var Appwrite\Event\Event $audits */
|
/** @var Appwrite\Event\Event $audits */
|
||||||
/** @var Appwrite\Stats\Stats $usage */
|
/** @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()) {
|
if ($collection->isEmpty()) {
|
||||||
throw new Exception('Collection not found', 404);
|
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);
|
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);
|
$dbForExternal->deleteCachedDocument($collectionId, $documentId);
|
||||||
|
|
||||||
$usage
|
$usage
|
||||||
|
@ -2087,6 +2118,7 @@ App::delete('/v1/database/collections/:collectionId/documents/:documentId')
|
||||||
|
|
||||||
$events
|
$events
|
||||||
->setParam('eventData', $response->output($document, Response::MODEL_DOCUMENT))
|
->setParam('eventData', $response->output($document, Response::MODEL_DOCUMENT))
|
||||||
|
->setParam('collection', $collection->getArrayCopy());
|
||||||
;
|
;
|
||||||
|
|
||||||
$audits
|
$audits
|
||||||
|
|
|
@ -207,7 +207,14 @@ App::shutdown(function ($utopia, $request, $response, $project, $events, $audits
|
||||||
|
|
||||||
if ($project->getId() !== 'console') {
|
if ($project->getId() !== 'console') {
|
||||||
$payload = new Document($response->getPayload());
|
$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(
|
Realtime::send(
|
||||||
$target['projectId'] ?? $project->getId(),
|
$target['projectId'] ?? $project->getId(),
|
||||||
|
|
|
@ -240,7 +240,7 @@ class Realtime extends Adapter
|
||||||
* @param Document|null $project
|
* @param Document|null $project
|
||||||
* @return array
|
* @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 = [];
|
$channels = [];
|
||||||
$roles = [];
|
$roles = [];
|
||||||
|
@ -275,12 +275,6 @@ class Realtime extends Adapter
|
||||||
$channels[] = 'teams.' . $payload->getId();
|
$channels[] = 'teams.' . $payload->getId();
|
||||||
$roles = ['team:' . $payload->getId()];
|
$roles = ['team:' . $payload->getId()];
|
||||||
|
|
||||||
break;
|
|
||||||
case strpos($event, 'database.collections.') === 0:
|
|
||||||
$channels[] = 'collections';
|
|
||||||
$channels[] = 'collections.' . $payload->getId();
|
|
||||||
$roles = $payload->getRead();
|
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case strpos($event, 'database.attributes.') === 0:
|
case strpos($event, 'database.attributes.') === 0:
|
||||||
case strpos($event, 'database.indexes.') === 0:
|
case strpos($event, 'database.indexes.') === 0:
|
||||||
|
@ -290,10 +284,15 @@ class Realtime extends Adapter
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case strpos($event, 'database.documents.') === 0:
|
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[] = 'documents';
|
||||||
$channels[] = 'collections.' . $payload->getAttribute('$collection') . '.documents';
|
$channels[] = 'collections.' . $payload->getAttribute('$collection') . '.documents';
|
||||||
$channels[] = 'documents.' . $payload->getId();
|
$channels[] = 'documents.' . $payload->getId();
|
||||||
$roles = $payload->getRead();
|
|
||||||
|
$roles = ($collection->getAttribute('permission') === 'collection') ? $collection->getRead() : $payload->getRead();
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case strpos($event, 'storage.') === 0:
|
case strpos($event, 'storage.') === 0:
|
||||||
|
|
|
@ -19,8 +19,8 @@ trait DatabaseBase
|
||||||
]), [
|
]), [
|
||||||
'collectionId' => 'unique()',
|
'collectionId' => 'unique()',
|
||||||
'name' => 'Movies',
|
'name' => 'Movies',
|
||||||
'read' => ['role:all'],
|
'read' => [],
|
||||||
'write' => ['role:all'],
|
'write' => [],
|
||||||
'permission' => 'document',
|
'permission' => 'document',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
@ -113,8 +113,8 @@ trait DatabaseBase
|
||||||
]), [
|
]), [
|
||||||
'collectionId' => 'unique()',
|
'collectionId' => 'unique()',
|
||||||
'name' => 'Response Models',
|
'name' => 'Response Models',
|
||||||
'read' => ['role:all'],
|
'read' => [],
|
||||||
'write' => ['role:all'],
|
'write' => [],
|
||||||
'permission' => 'document',
|
'permission' => 'document',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
@ -1229,8 +1229,8 @@ trait DatabaseBase
|
||||||
]), [
|
]), [
|
||||||
'collectionId' => 'unique()',
|
'collectionId' => 'unique()',
|
||||||
'name' => 'invalidDocumentStructure',
|
'name' => 'invalidDocumentStructure',
|
||||||
'read' => ['role:all'],
|
'read' => [],
|
||||||
'write' => ['role:all'],
|
'write' => [],
|
||||||
'permission' => 'document',
|
'permission' => 'document',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
@ -1829,13 +1829,41 @@ trait DatabaseBase
|
||||||
|
|
||||||
$this->assertEquals(201, $document1['headers']['status-code']);
|
$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([
|
$documents = $this->client->call(Client::METHOD_GET, '/database/collections/' . $collectionId . '/documents', array_merge([
|
||||||
'content-type' => 'application/json',
|
'content-type' => 'application/json',
|
||||||
'x-appwrite-project' => $this->getProject()['$id'],
|
'x-appwrite-project' => $this->getProject()['$id'],
|
||||||
], $this->getHeaders()));
|
], $this->getHeaders()));
|
||||||
|
|
||||||
$this->assertEquals(1, $documents['body']['sum']);
|
$this->assertEquals(3, $documents['body']['sum']);
|
||||||
$this->assertCount(1, $documents['body']['documents']);
|
$this->assertCount(3, $documents['body']['documents']);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Test for Failure
|
* Test for Failure
|
||||||
|
@ -1894,7 +1922,7 @@ trait DatabaseBase
|
||||||
'x-appwrite-project' => $this->getProject()['$id'],
|
'x-appwrite-project' => $this->getProject()['$id'],
|
||||||
]));
|
]));
|
||||||
|
|
||||||
$this->assertEquals(404, $documents['headers']['status-code']);
|
$this->assertEquals(401, $documents['headers']['status-code']);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -160,7 +160,7 @@ class DatabasePermissionsTeamTest extends Scope
|
||||||
if ($success) {
|
if ($success) {
|
||||||
$this->assertCount(1, $documents['body']['documents']);
|
$this->assertCount(1, $documents['body']['documents']);
|
||||||
} else {
|
} else {
|
||||||
$this->assertEquals(404, $documents['headers']['status-code']);
|
$this->assertEquals(401, $documents['headers']['status-code']);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -77,9 +77,7 @@ class RealtimeCustomClientTest extends Scope
|
||||||
'files',
|
'files',
|
||||||
'files.1',
|
'files.1',
|
||||||
'collections',
|
'collections',
|
||||||
'collections.1',
|
|
||||||
'collections.1.documents',
|
'collections.1.documents',
|
||||||
'collections.2',
|
|
||||||
'collections.2.documents',
|
'collections.2.documents',
|
||||||
'documents',
|
'documents',
|
||||||
'documents.1',
|
'documents.1',
|
||||||
|
@ -93,15 +91,13 @@ class RealtimeCustomClientTest extends Scope
|
||||||
$this->assertEquals('connected', $response['type']);
|
$this->assertEquals('connected', $response['type']);
|
||||||
$this->assertNotEmpty($response['data']);
|
$this->assertNotEmpty($response['data']);
|
||||||
$this->assertNotEmpty($response['data']['user']);
|
$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', $response['data']['channels']);
|
||||||
$this->assertContains('account.' . $userId, $response['data']['channels']);
|
$this->assertContains('account.' . $userId, $response['data']['channels']);
|
||||||
$this->assertContains('files', $response['data']['channels']);
|
$this->assertContains('files', $response['data']['channels']);
|
||||||
$this->assertContains('files.1', $response['data']['channels']);
|
$this->assertContains('files.1', $response['data']['channels']);
|
||||||
$this->assertContains('collections', $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.1.documents', $response['data']['channels']);
|
||||||
$this->assertContains('collections.2', $response['data']['channels']);
|
|
||||||
$this->assertContains('collections.2.documents', $response['data']['channels']);
|
$this->assertContains('collections.2.documents', $response['data']['channels']);
|
||||||
$this->assertContains('documents', $response['data']['channels']);
|
$this->assertContains('documents', $response['data']['channels']);
|
||||||
$this->assertContains('documents.1', $response['data']['channels']);
|
$this->assertContains('documents.1', $response['data']['channels']);
|
||||||
|
@ -561,24 +557,11 @@ class RealtimeCustomClientTest extends Scope
|
||||||
]), [
|
]), [
|
||||||
'collectionId' => 'unique()',
|
'collectionId' => 'unique()',
|
||||||
'name' => 'Actors',
|
'name' => 'Actors',
|
||||||
'read' => ['role:all'],
|
'read' => [],
|
||||||
'write' => ['role:all'],
|
'write' => [],
|
||||||
'permission' => 'collection'
|
'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']];
|
$data = ['actorsId' => $actors['body']['$id']];
|
||||||
|
|
||||||
$name = $this->client->call(Client::METHOD_POST, '/database/collections/' . $data['actorsId'] . '/attributes/string', array_merge([
|
$name = $this->client->call(Client::METHOD_POST, '/database/collections/' . $data['actorsId'] . '/attributes/string', array_merge([
|
||||||
|
@ -586,7 +569,7 @@ class RealtimeCustomClientTest extends Scope
|
||||||
'x-appwrite-project' => $this->getProject()['$id'],
|
'x-appwrite-project' => $this->getProject()['$id'],
|
||||||
'x-appwrite-key' => $this->getProject()['apiKey']
|
'x-appwrite-key' => $this->getProject()['apiKey']
|
||||||
]), [
|
]), [
|
||||||
'key' => 'name',
|
'attributeId' => 'name',
|
||||||
'size' => 256,
|
'size' => 256,
|
||||||
'required' => true,
|
'required' => true,
|
||||||
]);
|
]);
|
||||||
|
@ -662,7 +645,6 @@ class RealtimeCustomClientTest extends Scope
|
||||||
|
|
||||||
$this->assertEquals($response['data']['payload']['name'], 'Chris Evans 2');
|
$this->assertEquals($response['data']['payload']['name'], 'Chris Evans 2');
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test Document Delete
|
* Test Document Delete
|
||||||
*/
|
*/
|
||||||
|
@ -703,6 +685,166 @@ class RealtimeCustomClientTest extends Scope
|
||||||
$client->close();
|
$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']
|
||||||
|
]), [
|
||||||
|
'key' => '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()
|
public function testChannelFiles()
|
||||||
{
|
{
|
||||||
$user = $this->getUser();
|
$user = $this->getUser();
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
namespace Appwrite\Tests;
|
namespace Appwrite\Tests;
|
||||||
|
|
||||||
use Appwrite\Database\Document;
|
use Utopia\Database\Document;
|
||||||
use Appwrite\Messaging\Adapter\Realtime;
|
use Appwrite\Messaging\Adapter\Realtime;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
@ -195,4 +195,51 @@ class MessagingTest extends TestCase
|
||||||
$this->assertArrayHasKey('account', $channels);
|
$this->assertArrayHasKey('account', $channels);
|
||||||
$this->assertArrayNotHasKey('account.456', $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']);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue