From 9a182d615a1ee94afdab72cc3c447b3eec55491d Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Tue, 2 Aug 2022 21:18:49 +1200 Subject: [PATCH 001/107] Refactor databases to use new permissions --- app/controllers/api/databases.php | 109 ++++++++------- composer.json | 2 +- .../Utopia/Response/Model/Collection.php | 21 +-- .../Utopia/Response/Model/Document.php | 11 +- .../e2e/Services/Databases/DatabasesBase.php | 126 ++++++++---------- .../Databases/DatabasesCustomServerTest.php | 12 +- .../DatabasesPermissionsGuestTest.php | 7 +- .../DatabasesPermissionsMemberTest.php | 10 +- 8 files changed, 142 insertions(+), 156 deletions(-) diff --git a/app/controllers/api/databases.php b/app/controllers/api/databases.php index a14e57d6be..f395ce49d5 100644 --- a/app/controllers/api/databases.php +++ b/app/controllers/api/databases.php @@ -497,15 +497,14 @@ App::post('/v1/databases/:databaseId/collections') ->param('databaseId', '', new UID(), 'Database ID.') ->param('collectionId', '', new CustomId(), 'Unique Id. Choose your own unique ID or pass the string "unique()" to auto generate it. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.') ->param('name', '', new Text(128), 'Collection name. Max length: 128 chars.') - ->param('permission', null, new WhiteList(['document', 'collection']), 'Specifies the permissions model used in this collection, which accepts either \'collection\' or \'document\'. For \'collection\' level permission, the permissions specified in read and write params are applied to all documents in the collection. For \'document\' level permissions, read and write permissions are specified in each document. [learn more about permissions](https://appwrite.io/docs/permissions) and get a full list of available permissions.') - ->param('read', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE), 'An array of strings with read permissions. By default no user is granted with any read permissions. [learn more about permissions](https://appwrite.io/docs/permissions) and get a full list of available permissions.') - ->param('write', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE), 'An array of strings with write permissions. By default no user is granted with any write permissions. [learn more about permissions](https://appwrite.io/docs/permissions) and get a full list of available permissions.') + ->param('permissions', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE), 'An array of strings with permissions. By default no user is granted with any read permissions. [learn more about permissions](https://appwrite.io/docs/permissions) and get a full list of available permissions.') + ->param('documentSecurity', false, new Boolean(), 'Specifies the permissions model used in this collection, which accepts either \'collection\' or \'document\'. For \'collection\' level permission, the permissions specified in read and write params are applied to all documents in the collection. For \'document\' level permissions, read and write permissions are specified in each document. [learn more about permissions](https://appwrite.io/docs/permissions) and get a full list of available permissions.') ->inject('response') ->inject('dbForProject') ->inject('audits') ->inject('usage') ->inject('events') - ->action(function (string $databaseId, string $collectionId, string $name, ?string $permission, ?array $read, ?array $write, Response $response, Database $dbForProject, EventAudit $audits, Stats $usage, Event $events) { + ->action(function (string $databaseId, string $collectionId, string $name, ?array $permissions, bool $documentSecurity, Response $response, Database $dbForProject, EventAudit $audits, Stats $usage, Event $events) { $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); @@ -520,9 +519,8 @@ App::post('/v1/databases/:databaseId/collections') '$id' => $collectionId, 'databaseInternalId' => $database->getInternalId(), 'databaseId' => $databaseId, - '$read' => $read ?? [], // Collection permissions for collection documents (based on permission model) - '$write' => $write ?? [], // Collection permissions for collection documents (based on permission model) - 'permission' => $permission, // Permissions model type (document vs collection) + '$permissions' => $permissions ?? [], + 'documentSecurity' => $documentSecurity, 'enabled' => true, 'name' => $name, 'search' => implode(' ', [$collectionId, $name]), @@ -751,16 +749,15 @@ App::put('/v1/databases/:databaseId/collections/:collectionId') ->param('databaseId', '', new UID(), 'Database ID.') ->param('collectionId', '', new UID(), 'Collection ID.') ->param('name', null, new Text(128), 'Collection name. Max length: 128 chars.') - ->param('permission', null, new WhiteList(['document', 'collection']), 'Permissions type model to use for reading documents in this collection. You can use collection-level permission set once on the collection using the `read` and `write` params, or you can set document-level permission where each document read and write params will decide who has access to read and write to each document individually. [learn more about permissions](https://appwrite.io/docs/permissions) and get a full list of available permissions.') - ->param('read', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE), 'An array of strings with read permissions. By default inherits the existing read permissions. [learn more about permissions](https://appwrite.io/docs/permissions) and get a full list of available permissions.', true) - ->param('write', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE), 'An array of strings with write permissions. By default inherits the existing write permissions. [learn more about permissions](https://appwrite.io/docs/permissions) and get a full list of available permissions.', true) + ->param('permissions', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE), 'An array of strings with permissions. By default no user is granted with any permissions. [learn more about permissions](/docs/permissions) and get a full list of available permissions.', true) + ->param('documentSecurity', false, new Boolean(), 'Whether to enable document-level permission where each document\'s permissions parameter will decide who has access to each file individually. [learn more about permissions](/docs/permissions) and get a full list of available permissions.') ->param('enabled', true, new Boolean(), 'Is collection enabled?', true) ->inject('response') ->inject('dbForProject') ->inject('audits') ->inject('usage') ->inject('events') - ->action(function (string $databaseId, string $collectionId, string $name, string $permission, ?array $read, ?array $write, bool $enabled, Response $response, Database $dbForProject, EventAudit $audits, Stats $usage, Event $events) { + ->action(function (string $databaseId, string $collectionId, string $name, ?array $permissions, bool $documentSecurity, bool $enabled, Response $response, Database $dbForProject, EventAudit $audits, Stats $usage, Event $events) { $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); @@ -779,10 +776,9 @@ App::put('/v1/databases/:databaseId/collections/:collectionId') try { $collection = $dbForProject->updateDocument('database_' . $database->getInternalId(), $collectionId, $collection - ->setAttribute('$write', $write) - ->setAttribute('$read', $read) + ->setAttribute('$permissions', $permissions) ->setAttribute('name', $name) - ->setAttribute('permission', $permission) + ->setAttribute('documentSecurity', $documentSecurity) ->setAttribute('enabled', $enabled) ->setAttribute('search', implode(' ', [$collectionId, $name]))); } catch (AuthorizationException $exception) { @@ -1822,8 +1818,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/documents') ->param('documentId', '', new CustomId(), 'Document ID. Choose your own unique ID or pass the string "unique()" to auto generate it. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.') ->param('collectionId', null, new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection). Make sure to define attributes before creating documents.') ->param('data', [], new JSON(), 'Document data as JSON object.') - ->param('read', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE), 'An array of strings with read permissions. By default only the current user is granted with read permissions. [learn more about permissions](https://appwrite.io/docs/permissions) and get a full list of available permissions.', true) - ->param('write', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE), 'An array of strings with write permissions. By default only the current user is granted with write permissions. [learn more about permissions](https://appwrite.io/docs/permissions) and get a full list of available permissions.', true) + ->param('permissions', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE), 'An array of strings with permissions. By default no user is granted with any permissions. [learn more about permissions](/docs/permissions) and get a full list of available permissions.', true) ->inject('response') ->inject('dbForProject') ->inject('user') @@ -1831,7 +1826,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/documents') ->inject('usage') ->inject('events') ->inject('mode') - ->action(function (string $databaseId, string $documentId, string $collectionId, string|array $data, ?array $read, ?array $write, Response $response, Database $dbForProject, Document $user, EventAudit $audits, Stats $usage, Event $events, string $mode) { + ->action(function (string $databaseId, string $documentId, string $collectionId, string|array $data, ?array $permissions, Response $response, Database $dbForProject, Document $user, EventAudit $audits, Stats $usage, Event $events, string $mode) { $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); @@ -1862,31 +1857,49 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/documents') } // Check collection permissions when enforced - if ($collection->getAttribute('permission') === 'collection') { - $validator = new Authorization('write'); - if (!$validator->isValid($collection->getWrite())) { + if (!$collection->getAttribute('documentSecurity', false)) { + $validator = new Authorization('create'); + if (!$validator->isValid($collection->getCreate())) { 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; - $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 + + 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 $roles = Authorization::getRoles(); if (!Auth::isAppUser($roles) && !Auth::isPrivilegedUser($roles)) { - foreach ($data['$read'] as $read) { - if (!Authorization::isRole($read)) { - // TODO: Isn't this a 401: Unauthorized Error ? - throw new Exception('Read permissions must be one of: (' . \implode(', ', $roles) . ')', 400, Exception::USER_UNAUTHORIZED); - } - } - foreach ($data['$write'] as $write) { - if (!Authorization::isRole($write)) { - throw new Exception('Write permissions must be one of: (' . \implode(', ', $roles) . ')', 400, Exception::USER_UNAUTHORIZED); + 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); + } + } } } } @@ -2223,15 +2236,14 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/documents/:docum ->param('collectionId', null, new UID(), 'Collection ID.') ->param('documentId', null, new UID(), 'Document ID.') ->param('data', [], new JSON(), 'Document data as JSON object. Include only attribute and value pairs to be updated.', true) - ->param('read', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE), 'An array of strings with read permissions. By default inherits the existing read permissions. [learn more about permissions](https://appwrite.io/docs/permissions) and get a full list of available permissions.', true) - ->param('write', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE), 'An array of strings with write permissions. By default inherits the existing write permissions. [learn more about permissions](https://appwrite.io/docs/permissions) and get a full list of available permissions.', true) + ->param('permissions', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE), 'An array of strings with permissions. By default no user is granted with any permissions. [learn more about permissions](/docs/permissions) and get a full list of available permissions.', true) ->inject('response') ->inject('dbForProject') ->inject('audits') ->inject('usage') ->inject('events') ->inject('mode') - ->action(function (string $databaseId, string $collectionId, string $documentId, string|array $data, ?array $read, ?array $write, Response $response, Database $dbForProject, EventAudit $audits, Stats $usage, Event $events, string $mode) { + ->action(function (string $databaseId, string $collectionId, string $documentId, string|array $data, ?array $permissions, Response $response, Database $dbForProject, EventAudit $audits, Stats $usage, Event $events, string $mode) { $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); @@ -2281,31 +2293,32 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/documents/:docum $data['$collection'] = $collection->getId(); // Make sure user don't switch collectionID $data['$createdAt'] = $document->getCreatedAt(); // Make sure user don't switch createdAt $data['$id'] = $document->getId(); // Make sure user don't switch document unique ID - $data['$read'] = (is_null($read)) ? ($document->getRead() ?? []) : $read; // By default inherit read permissions - $data['$write'] = (is_null($write)) ? ($document->getWrite() ?? []) : $write; // By default inherit write permissions + + if (\is_null($permissions)) { + $permissions = $document->getPermissions() ?? []; + } + + $data['$permissions'] = $permissions; // Users can only add their roles to documents, API keys and Admin users can add any $roles = Authorization::getRoles(); if (!Auth::isAppUser($roles) && !Auth::isPrivilegedUser($roles)) { - if (!is_null($read)) { - foreach ($data['$read'] as $read) { - if (!Authorization::isRole($read)) { - throw new Exception('Read permissions must be one of: (' . \implode(', ', $roles) . ')', 400, Exception::USER_UNAUTHORIZED); - } - } - } - if (!is_null($write)) { - foreach ($data['$write'] as $write) { - if (!Authorization::isRole($write)) { - throw new Exception('Write permissions must be one of: (' . \implode(', ', $roles) . ')', 400, Exception::USER_UNAUTHORIZED); + foreach ($data['$permissions'] as $permission) { + $matches = []; + if (\preg_match('/^(update|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); + } } } } } try { - if ($collection->getAttribute('permission') === 'collection') { + if (!$collection->getAttribute('documentSecurity', false)) { /** @var Document $document */ $document = Authorization::skip(fn() => $dbForProject->updateDocument('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $document->getId(), new Document($data))); } else { diff --git a/composer.json b/composer.json index c95dbab819..a04d46e4e8 100644 --- a/composer.json +++ b/composer.json @@ -50,7 +50,7 @@ "utopia-php/cache": "0.6.*", "utopia-php/cli": "0.13.*", "utopia-php/config": "0.2.*", - "utopia-php/database": "0.18.*", + "utopia-php/database": "dev-refactor-permissions as 0.18.9", "utopia-php/locale": "0.4.*", "utopia-php/registry": "0.5.*", "utopia-php/preloader": "0.2.*", diff --git a/src/Appwrite/Utopia/Response/Model/Collection.php b/src/Appwrite/Utopia/Response/Model/Collection.php index 2340cc5297..cd75da2ce6 100644 --- a/src/Appwrite/Utopia/Response/Model/Collection.php +++ b/src/Appwrite/Utopia/Response/Model/Collection.php @@ -28,18 +28,11 @@ class Collection extends Model 'default' => 0, 'example' => 1592981250, ]) - ->addRule('$read', [ + ->addRule('$permissions', [ 'type' => self::TYPE_STRING, - 'description' => 'Collection read permissions.', + 'description' => 'Collection permissions.', 'default' => '', - 'example' => 'role:all', - 'array' => true - ]) - ->addRule('$write', [ - 'type' => self::TYPE_STRING, - 'description' => 'Collection write permissions.', - 'default' => '', - 'example' => 'user:608f9da25e7e1', + 'example' => 'read(any)', 'array' => true ]) ->addRule('databaseId', [ @@ -60,11 +53,11 @@ class Collection extends Model 'default' => true, 'example' => false, ]) - ->addRule('permission', [ - 'type' => self::TYPE_STRING, - 'description' => 'Collection permission model. Possible values: `document` or `collection`', + ->addRule('documentSecurity', [ + 'type' => self::TYPE_BOOLEAN, + 'description' => 'Whether document-level permissions are enabled.', 'default' => '', - 'example' => 'document', + 'example' => true, ]) ->addRule('attributes', [ 'type' => [ diff --git a/src/Appwrite/Utopia/Response/Model/Document.php b/src/Appwrite/Utopia/Response/Model/Document.php index 3106e813b1..1384a67d44 100644 --- a/src/Appwrite/Utopia/Response/Model/Document.php +++ b/src/Appwrite/Utopia/Response/Model/Document.php @@ -54,18 +54,11 @@ class Document extends Any 'default' => 0, 'example' => 1592981250, ]) - ->addRule('$read', [ - 'type' => self::TYPE_STRING, - 'description' => 'Document read permissions.', - 'default' => '', - 'example' => 'role:all', - 'array' => true, - ]) - ->addRule('$write', [ + ->addRule('$permissions', [ 'type' => self::TYPE_STRING, 'description' => 'Document write permissions.', 'default' => '', - 'example' => 'user:608f9da25e7e1', + 'example' => 'read(user:608f9da25e7e1)', 'array' => true, ]) ; diff --git a/tests/e2e/Services/Databases/DatabasesBase.php b/tests/e2e/Services/Databases/DatabasesBase.php index 34a4160fde..04680ce4d3 100644 --- a/tests/e2e/Services/Databases/DatabasesBase.php +++ b/tests/e2e/Services/Databases/DatabasesBase.php @@ -840,10 +840,8 @@ trait DatabasesBase $this->assertEquals($document1['headers']['status-code'], 201); $this->assertEquals($document1['body']['title'], 'Captain America'); $this->assertEquals($document1['body']['releaseYear'], 1944); - $this->assertIsArray($document1['body']['$read']); - $this->assertIsArray($document1['body']['$write']); - $this->assertCount(1, $document1['body']['$read']); - $this->assertCount(1, $document1['body']['$write']); + $this->assertIsArray($document1['body']['$permissions']); + $this->assertCount(2, $document1['body']['$permissions']); $this->assertCount(2, $document1['body']['actors']); $this->assertEquals($document1['body']['actors'][0], 'Chris Evans'); $this->assertEquals($document1['body']['actors'][1], 'Samuel Jackson'); @@ -852,10 +850,8 @@ trait DatabasesBase $this->assertEquals($document2['body']['title'], 'Spider-Man: Far From Home'); $this->assertEquals($document2['body']['releaseYear'], 2019); $this->assertEquals($document2['body']['duration'], null); - $this->assertIsArray($document2['body']['$read']); - $this->assertIsArray($document2['body']['$write']); - $this->assertCount(1, $document2['body']['$read']); - $this->assertCount(1, $document2['body']['$write']); + $this->assertIsArray($document2['body']['$permissions']); + $this->assertCount(2, $document2['body']['$permissions']); $this->assertCount(3, $document2['body']['actors']); $this->assertEquals($document2['body']['actors'][0], 'Tom Holland'); $this->assertEquals($document2['body']['actors'][1], 'Zendaya Maree Stoermer'); @@ -865,10 +861,8 @@ trait DatabasesBase $this->assertEquals($document3['body']['title'], 'Spider-Man: Homecoming'); $this->assertEquals($document3['body']['releaseYear'], 2017); $this->assertEquals($document3['body']['duration'], 0); - $this->assertIsArray($document3['body']['$read']); - $this->assertIsArray($document3['body']['$write']); - $this->assertCount(1, $document3['body']['$read']); - $this->assertCount(1, $document3['body']['$write']); + $this->assertIsArray($document3['body']['$permissions']); + $this->assertCount(2, $document3['body']['$permisisons']); $this->assertCount(2, $document3['body']['actors']); $this->assertEquals($document3['body']['actors'][0], 'Tom Holland'); $this->assertEquals($document3['body']['actors'][1], 'Zendaya Maree Stoermer'); @@ -996,8 +990,7 @@ trait DatabasesBase $this->assertEquals($response['body']['$collection'], $document['$collection']); $this->assertEquals($response['body']['title'], $document['title']); $this->assertEquals($response['body']['releaseYear'], $document['releaseYear']); - $this->assertEquals($response['body']['$read'], $document['$read']); - $this->assertEquals($response['body']['$write'], $document['$write']); + $this->assertEquals($response['body']['$permissions'], $document['$permissions']); $this->assertFalse(array_key_exists('$internalId', $response['body'])); } } @@ -1417,8 +1410,10 @@ trait DatabasesBase 'actors' => [], '$createdAt' => 5 // Should be ignored ], - 'read' => ['user:' . $this->getUser()['$id']], - 'write' => ['user:' . $this->getUser()['$id']], + 'permissions' => [ + 'read(user: ' . $this->getUser()['$id'] . ')', + 'write(user: ' . $this->getUser()['$id'] . ')', + ], ]); $id = $document['body']['$id']; @@ -1427,8 +1422,8 @@ trait DatabasesBase $this->assertEquals($document['body']['title'], 'Thor: Ragnaroc'); $this->assertEquals($document['body']['releaseYear'], 2017); $this->assertNotEquals($document['body']['$createdAt'], 5); - $this->assertEquals('user:' . $this->getUser()['$id'], $document['body']['$read'][0]); - $this->assertEquals('user:' . $this->getUser()['$id'], $document['body']['$write'][0]); + $this->assertEquals('read(user:' . $this->getUser()['$id'] . ')', $document['body']['$permissions'][0]); + $this->assertEquals('write(user:' . $this->getUser()['$id'] . ')', $document['body']['$permissions'][1]); $document = $this->client->call(Client::METHOD_PATCH, '/databases/' . $databaseId . '/collections/' . $data['moviesId'] . '/documents/' . $id, array_merge([ 'content-type' => 'application/json', @@ -1437,8 +1432,10 @@ trait DatabasesBase 'data' => [ 'title' => 'Thor: Ragnarok', ], - 'read' => ['role:member'], - 'write' => ['role:member'], + 'permissions' => [ + 'read(users)', + 'write(users)', + ], ]); $this->assertEquals($document['headers']['status-code'], 200); @@ -1446,8 +1443,8 @@ trait DatabasesBase $this->assertEquals($document['body']['$collection'], $data['moviesId']); $this->assertEquals($document['body']['title'], 'Thor: Ragnarok'); $this->assertEquals($document['body']['releaseYear'], 2017); - $this->assertEquals('role:member', $document['body']['$read'][0]); - $this->assertEquals('role:member', $document['body']['$write'][0]); + $this->assertEquals('read(users)', $document['body']['$permissions'][0]); + $this->assertEquals('write(users)', $document['body']['$permissions'][1]); $document = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $data['moviesId'] . '/documents/' . $id, array_merge([ 'content-type' => 'application/json', @@ -1994,21 +1991,17 @@ trait DatabasesBase $this->assertEquals($document['headers']['status-code'], 201); $this->assertEquals($document['body']['title'], 'Captain America'); $this->assertEquals($document['body']['releaseYear'], 1944); - $this->assertIsArray($document['body']['$read']); - $this->assertIsArray($document['body']['$write']); + $this->assertIsArray($document['body']['$permissions']); if ($this->getSide() == 'client') { - $this->assertCount(1, $document['body']['$read']); - $this->assertCount(1, $document['body']['$write']); - $this->assertEquals(['user:' . $this->getUser()['$id']], $document['body']['$read']); - $this->assertEquals(['user:' . $this->getUser()['$id']], $document['body']['$write']); + $this->assertCount(2, $document['body']['$permissions']); + $this->assertEquals(['read(user:' . $this->getUser()['$id'] . ')'], $document['body']['$permissions']); + $this->assertEquals(['write(user:' . $this->getUser()['$id'] . ')'], $document['body']['$permissions']); } if ($this->getSide() == 'server') { - $this->assertCount(0, $document['body']['$read']); - $this->assertCount(0, $document['body']['$write']); - $this->assertEquals([], $document['body']['$read']); - $this->assertEquals([], $document['body']['$write']); + $this->assertCount(0, $document['body']['$permissions']); + $this->assertEquals([], $document['body']['$permissions']); } // Updated and Inherit Permissions @@ -2022,7 +2015,9 @@ trait DatabasesBase 'releaseYear' => 1945, 'actors' => [], ], - 'read' => ['role:all'], + 'permissions' => [ + 'read(any)', + ], ]); $this->assertEquals($document['headers']['status-code'], 200); @@ -2030,17 +2025,14 @@ trait DatabasesBase $this->assertEquals($document['body']['releaseYear'], 1945); if ($this->getSide() == 'client') { - $this->assertCount(1, $document['body']['$read']); - $this->assertCount(1, $document['body']['$write']); - $this->assertEquals(['role:all'], $document['body']['$read']); - $this->assertEquals(['user:' . $this->getUser()['$id']], $document['body']['$write']); + $this->assertCount(2, $document['body']['$permissions']); + $this->assertContains('read(any)', $document['body']['$permissions']); + $this->assertContains('write(user:' . $this->getUser()['$id'] . ')', $document['body']['$permissions']); } if ($this->getSide() == 'server') { - $this->assertCount(1, $document['body']['$read']); - $this->assertCount(0, $document['body']['$write']); - $this->assertEquals(['role:all'], $document['body']['$read']); - $this->assertEquals([], $document['body']['$write']); + $this->assertCount(1, $document['body']['$permissions']); + $this->assertEquals(['read(any)'], $document['body']['$permissions']); } $document = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $data['moviesId'] . '/documents/' . $id, array_merge([ @@ -2053,17 +2045,14 @@ trait DatabasesBase $this->assertEquals($document['body']['releaseYear'], 1945); if ($this->getSide() == 'client') { - $this->assertCount(1, $document['body']['$read']); - $this->assertCount(1, $document['body']['$write']); - $this->assertEquals(['role:all'], $document['body']['$read']); + $this->assertCount(2, $document['body']['$permissions']); + $this->assertEquals(['read(any)'], $document['body']['$permissions']); $this->assertEquals(['user:' . $this->getUser()['$id']], $document['body']['$write']); } if ($this->getSide() == 'server') { - $this->assertCount(1, $document['body']['$read']); - $this->assertCount(0, $document['body']['$write']); - $this->assertEquals(['role:all'], $document['body']['$read']); - $this->assertEquals([], $document['body']['$write']); + $this->assertCount(1, $document['body']['$permissions']); + $this->assertEquals(['read(any)'], $document['body']['$permissions']); } // Reset Permissions @@ -2089,10 +2078,8 @@ trait DatabasesBase $this->assertEquals($document['headers']['status-code'], 200); $this->assertEquals($document['body']['title'], 'Captain America 3'); $this->assertEquals($document['body']['releaseYear'], 1946); - $this->assertCount(0, $document['body']['$read']); - $this->assertCount(0, $document['body']['$write']); - $this->assertEquals([], $document['body']['$read']); - $this->assertEquals([], $document['body']['$write']); + $this->assertCount(0, $document['body']['$permissions']); + $this->assertEquals([], $document['body']['$permissions']); } return $data; @@ -2478,24 +2465,27 @@ trait DatabasesBase 'data' => [ 'title' => 'Captain America', ], - 'read' => ['role:all'], - 'write' => ['role:all'], + 'permissions' => [ + 'read(any)', + 'write(any)', + ], ]); $id = $document['body']['$id']; $this->assertEquals($document['headers']['status-code'], 201); - $this->assertCount(1, $document['body']['$read']); - $this->assertCount(1, $document['body']['$write']); - $this->assertEquals(['role:all'], $document['body']['$read']); - $this->assertEquals(['role:all'], $document['body']['$write']); + $this->assertCount(2, $document['body']['$permissions']); + $this->assertContains('read(any)', $document['body']['$permissions']); + $this->assertContains('write(any)', $document['body']['$permissions']); // Send only read permission $document = $this->client->call(Client::METHOD_PATCH, '/databases/' . $databaseId . '/collections/' . $moviesId . '/documents/' . $id, array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ - 'read' => ['user:' . $this->getUser()['$id']], + 'permissions' => [ + 'read(user: ' . $this->getUser()['$id'] . ')', + ] ]); if ($this->getSide() == 'client') { @@ -2504,10 +2494,9 @@ trait DatabasesBase if ($this->getSide() == 'server') { $this->assertEquals($document['headers']['status-code'], 200); - $this->assertCount(1, $document['body']['$read']); - $this->assertCount(1, $document['body']['$write']); - $this->assertEquals(['user:' . $this->getUser()['$id']], $document['body']['$read']); - $this->assertEquals(['role:all'], $document['body']['$write']); + $this->assertCount(2, $document['body']['$permissions']); + $this->assertContains('read(user:' . $this->getUser()['$id'] . ')', $document['body']['$permissions']); + $this->assertContains('write(any)', $document['body']['$permissions']); } // send only write permission @@ -2515,15 +2504,16 @@ trait DatabasesBase 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ - 'write' => ['user:' . $this->getUser()['$id']], + 'permissions' => [ + 'write(user: ' . $this->getUser()['$id'] . ')', + ], ]); if ($this->getSide() == 'server') { $this->assertEquals($document['headers']['status-code'], 200); - $this->assertCount(1, $document['body']['$read']); - $this->assertCount(1, $document['body']['$write']); - $this->assertEquals(['user:' . $this->getUser()['$id']], $document['body']['$read']); - $this->assertEquals(['user:' . $this->getUser()['$id']], $document['body']['$write']); + $this->assertCount(2, $document['body']['$permissions']); + $this->assertContains('read(user:' . $this->getUser()['$id'] . ')', $document['body']['$permissions']); + $this->assertContains('write(user:' . $this->getUser()['$id'] . ')', $document['body']['$permissions']); } // remove collection diff --git a/tests/e2e/Services/Databases/DatabasesCustomServerTest.php b/tests/e2e/Services/Databases/DatabasesCustomServerTest.php index cc75a13a80..f88feea3e2 100644 --- a/tests/e2e/Services/Databases/DatabasesCustomServerTest.php +++ b/tests/e2e/Services/Databases/DatabasesCustomServerTest.php @@ -849,18 +849,14 @@ class DatabasesCustomServerTest extends Scope ]); $this->assertEquals($document1['headers']['status-code'], 201); - $this->assertIsArray($document1['body']['$read']); - $this->assertIsArray($document1['body']['$write']); - $this->assertCount(1, $document1['body']['$read']); - $this->assertCount(1, $document1['body']['$write']); + $this->assertIsArray($document1['body']['$permissions']); + $this->assertCount(2, $document1['body']['$permissions']); $this->assertEquals($document1['body']['firstName'], 'Tom'); $this->assertEquals($document1['body']['lastName'], 'Holland'); $this->assertEquals($document2['headers']['status-code'], 201); - $this->assertIsArray($document2['body']['$read']); - $this->assertIsArray($document2['body']['$write']); - $this->assertCount(1, $document2['body']['$read']); - $this->assertCount(1, $document2['body']['$write']); + $this->assertIsArray($document2['body']['$permissions']); + $this->assertCount(1, $document2['body']['$permissions']); $this->assertEquals($document2['body']['firstName'], 'Samuel'); $this->assertEquals($document2['body']['lastName'], 'Jackson'); diff --git a/tests/e2e/Services/Databases/DatabasesPermissionsGuestTest.php b/tests/e2e/Services/Databases/DatabasesPermissionsGuestTest.php index 7b1ce8f3e6..b5d6a8a397 100644 --- a/tests/e2e/Services/Databases/DatabasesPermissionsGuestTest.php +++ b/tests/e2e/Services/Databases/DatabasesPermissionsGuestTest.php @@ -66,7 +66,7 @@ class DatabasesPermissionsGuestTest extends Scope /** * @dataProvider readDocumentsProvider */ - public function testReadDocuments($read, $write) + public function testReadDocuments($permissions) { $data = $this->createCollection(); $collectionId = $data['collectionId']; @@ -76,8 +76,7 @@ class DatabasesPermissionsGuestTest extends Scope 'data' => [ 'title' => 'Lorem', ], - 'read' => $read, - 'write' => $write, + 'permissions' => $permissions, ]); $this->assertEquals(201, $response['headers']['status-code']); @@ -87,7 +86,7 @@ class DatabasesPermissionsGuestTest extends Scope ]); foreach ($documents['body']['documents'] as $document) { - $this->assertContains('role:all', $document['$read']); + $this->assertContains('role:all', $document['$permissions']); } } } diff --git a/tests/e2e/Services/Databases/DatabasesPermissionsMemberTest.php b/tests/e2e/Services/Databases/DatabasesPermissionsMemberTest.php index 0c3f3a08a3..6c857b172a 100644 --- a/tests/e2e/Services/Databases/DatabasesPermissionsMemberTest.php +++ b/tests/e2e/Services/Databases/DatabasesPermissionsMemberTest.php @@ -148,8 +148,9 @@ class DatabasesPermissionsMemberTest extends Scope ]); foreach ($documents['body']['documents'] as $document) { - $hasPermissions = \array_reduce(['role:all', 'role:member', 'user:' . $users['user1']['$id']], function ($carry, $item) use ($document) { - return $carry ? true : \in_array($item, $document['$read']); + $hasPermissions = \array_reduce(['any', 'users', 'user:' . $users['user1']['$id']], function ($carry, $item) use ($document) { + // TODO: Fix this + return $carry ? true : \in_array($item, $document['$permissions']); }, false); $this->assertTrue($hasPermissions); } @@ -165,8 +166,9 @@ class DatabasesPermissionsMemberTest extends Scope ]); foreach ($documents['body']['documents'] as $document) { - $hasPermissions = \array_reduce(['role:all', 'role:member', 'user:' . $users['user1']['$id']], function ($carry, $item) use ($document) { - return $carry ? true : \in_array($item, $document['$read']); + $hasPermissions = \array_reduce(['any', 'users', 'user:' . $users['user1']['$id']], function ($carry, $item) use ($document) { + // TODO: Fix this + return $carry ? true : \in_array($item, $document['$permissions']); }, false); $this->assertTrue($hasPermissions); } From 4520114780002f9dcb30c4f4707dff9dd4a5f0ce Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Tue, 2 Aug 2022 21:19:15 +1200 Subject: [PATCH 002/107] Refactor storage to use new permissions --- app/controllers/api/storage.php | 124 ++++++++++-------- app/http.php | 6 +- app/realtime.php | 3 +- app/views/console/storage/bucket.phtml | 10 +- src/Appwrite/Utopia/Response/Model/Bucket.php | 19 +-- src/Appwrite/Utopia/Response/Model/File.php | 13 +- tests/e2e/Services/Storage/StorageBase.php | 12 +- .../Storage/StorageCustomClientTest.php | 11 +- .../Storage/StorageCustomServerTest.php | 7 +- 9 files changed, 104 insertions(+), 101 deletions(-) diff --git a/app/controllers/api/storage.php b/app/controllers/api/storage.php index b62aa750db..205815ae45 100644 --- a/app/controllers/api/storage.php +++ b/app/controllers/api/storage.php @@ -55,9 +55,8 @@ App::post('/v1/storage/buckets') ->label('sdk.response.model', Response::MODEL_BUCKET) ->param('bucketId', '', new CustomId(), 'Unique Id. Choose your own unique ID or pass the string `unique()` to auto generate it. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.') ->param('name', '', new Text(128), 'Bucket name') - ->param('permission', null, new WhiteList(['file', 'bucket']), 'Permissions type model to use for reading files in this bucket. You can use bucket-level permission set once on the bucket using the `read` and `write` params, or you can set file-level permission where each file read and write params will decide who has access to read and write to each file individually. [learn more about permissions](/docs/permissions) and get a full list of available permissions.') - ->param('read', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE), 'An array of strings with read permissions. By default no user is granted with any read permissions. [learn more about permissions](/docs/permissions) and get a full list of available permissions.', true) - ->param('write', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE), 'An array of strings with write permissions. By default no user is granted with any write permissions. [learn more about permissions](/docs/permissions) and get a full list of available permissions.', true) + ->param('fileSecurity', false, new Boolean(), 'Whether to enable file-level permission where each files permissions parameter will decide who has access to each file individually. [learn more about permissions](/docs/permissions) and get a full list of available permissions.') + ->param('permissions', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE), 'An array of strings with permissions. By default no user is granted with any permissions. [learn more about permissions](/docs/permissions) and get a full list of available permissions.', true) ->param('enabled', true, new Boolean(true), 'Is bucket enabled?', true) ->param('maximumFileSize', (int) App::getEnv('_APP_STORAGE_LIMIT', 0), new Range(1, (int) App::getEnv('_APP_STORAGE_LIMIT', 0)), 'Maximum file size allowed in bytes. Maximum allowed value is ' . Storage::human(App::getEnv('_APP_STORAGE_LIMIT', 0), 0) . '. For self-hosted setups you can change the max limit by changing the `_APP_STORAGE_LIMIT` environment variable. [Learn more about storage environment variables](docs/environment-variables#storage)', true) ->param('allowedFileExtensions', [], new ArrayList(new Text(64), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Allowed file extensions. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' extensions are allowed, each 64 characters long.', true) @@ -68,7 +67,7 @@ App::post('/v1/storage/buckets') ->inject('audits') ->inject('usage') ->inject('events') - ->action(function (string $bucketId, string $name, string $permission, ?array $read, ?array $write, bool $enabled, int $maximumFileSize, array $allowedFileExtensions, bool $encryption, bool $antivirus, Response $response, Database $dbForProject, Audit $audits, Stats $usage, Event $events) { + ->action(function (string $bucketId, string $name, string $fileSecurity, ?array $permissions, bool $enabled, int $maximumFileSize, array $allowedFileExtensions, bool $encryption, bool $antivirus, Response $response, Database $dbForProject, Audit $audits, Stats $usage, Event $events) { $bucketId = $bucketId === 'unique()' ? $dbForProject->getId() : $bucketId; try { @@ -108,14 +107,13 @@ App::post('/v1/storage/buckets') '$id' => $bucketId, '$collection' => 'buckets', 'name' => $name, - 'permission' => $permission, + 'documentSecurity' => $fileSecurity, 'maximumFileSize' => $maximumFileSize, 'allowedFileExtensions' => $allowedFileExtensions, 'enabled' => (bool) filter_var($enabled, FILTER_VALIDATE_BOOLEAN), 'encryption' => (bool) filter_var($encryption, FILTER_VALIDATE_BOOLEAN), 'antivirus' => (bool) filter_var($antivirus, FILTER_VALIDATE_BOOLEAN), - '$read' => $read ?? [], - '$write' => $write ?? [], + '$permissions' => $permissions, 'search' => implode(' ', [$bucketId, $name]), ])); @@ -223,9 +221,8 @@ App::put('/v1/storage/buckets/:bucketId') ->label('sdk.response.model', Response::MODEL_BUCKET) ->param('bucketId', '', new UID(), 'Bucket unique ID.') ->param('name', null, new Text(128), 'Bucket name', false) - ->param('permission', null, new WhiteList(['file', 'bucket']), 'Permissions type model to use for reading files in this bucket. You can use bucket-level permission set once on the bucket using the `read` and `write` params, or you can set file-level permission where each file read and write params will decide who has access to read and write to each file individually. [learn more about permissions](/docs/permissions) and get a full list of available permissions.') - ->param('read', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE), 'An array of strings with read permissions. By default inherits the existing read permissions. [learn more about permissions](/docs/permissions) and get a full list of available permissions.', true) - ->param('write', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE), 'An array of strings with write permissions. By default inherits the existing write permissions. [learn more about permissions](/docs/permissions) and get a full list of available permissions.', true) + ->param('fileSecurity', false, new Boolean(), 'Whether to enable file-level permission where each files permissions parameter will decide who has access to each file individually. [learn more about permissions](/docs/permissions) and get a full list of available permissions.') + ->param('permissions', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE), 'An array of strings with permissions. By default no user is granted with any permissions. [learn more about permissions](/docs/permissions) and get a full list of available permissions.', true) ->param('enabled', true, new Boolean(true), 'Is bucket enabled?', true) ->param('maximumFileSize', null, new Range(1, (int) App::getEnv('_APP_STORAGE_LIMIT', 0)), 'Maximum file size allowed in bytes. Maximum allowed value is ' . Storage::human((int)App::getEnv('_APP_STORAGE_LIMIT', 0), 0) . '. For self hosted version you can change the limit by changing _APP_STORAGE_LIMIT environment variable. [Learn more about storage environment variables](docs/environment-variables#storage)', true) ->param('allowedFileExtensions', [], new ArrayList(new Text(64), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Allowed file extensions. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' extensions are allowed, each 64 characters long.', true) @@ -236,15 +233,14 @@ App::put('/v1/storage/buckets/:bucketId') ->inject('audits') ->inject('usage') ->inject('events') - ->action(function (string $bucketId, string $name, string $permission, ?array $read, ?array $write, bool $enabled, ?int $maximumFileSize, array $allowedFileExtensions, bool $encryption, bool $antivirus, Response $response, Database $dbForProject, Audit $audits, Stats $usage, Event $events) { + ->action(function (string $bucketId, string $name, string $fileSecurity, ?array $permissions, bool $enabled, ?int $maximumFileSize, array $allowedFileExtensions, bool $encryption, bool $antivirus, Response $response, Database $dbForProject, Audit $audits, Stats $usage, Event $events) { $bucket = $dbForProject->getDocument('buckets', $bucketId); if ($bucket->isEmpty()) { throw new Exception('Bucket not found', 404, Exception::STORAGE_BUCKET_NOT_FOUND); } - $read ??= $bucket->getAttribute('$read', []); // By default inherit read permissions - $write ??= $bucket->getAttribute('$write', []); // By default inherit write permissions + $permissions ??= $bucket->getPermissions(); $maximumFileSize ??= $bucket->getAttribute('maximumFileSize', (int) App::getEnv('_APP_STORAGE_LIMIT', 0)); $allowedFileExtensions ??= $bucket->getAttribute('allowedFileExtensions', []); $enabled ??= $bucket->getAttribute('enabled', true); @@ -253,13 +249,12 @@ App::put('/v1/storage/buckets/:bucketId') $bucket = $dbForProject->updateDocument('buckets', $bucket->getId(), $bucket ->setAttribute('name', $name) - ->setAttribute('$read', $read) - ->setAttribute('$write', $write) + ->setAttribute('$permissions', $permissions) ->setAttribute('maximumFileSize', $maximumFileSize) ->setAttribute('allowedFileExtensions', $allowedFileExtensions) ->setAttribute('enabled', (bool) filter_var($enabled, FILTER_VALIDATE_BOOLEAN)) ->setAttribute('encryption', (bool) filter_var($encryption, FILTER_VALIDATE_BOOLEAN)) - ->setAttribute('permission', $permission) + ->setAttribute('documentSecurity', $fileSecurity) ->setAttribute('antivirus', (bool) filter_var($antivirus, FILTER_VALIDATE_BOOLEAN))); $audits @@ -342,8 +337,7 @@ App::post('/v1/storage/buckets/:bucketId/files') ->param('bucketId', null, new UID(), 'Storage bucket unique ID. You can create a new storage bucket using the Storage service [server integration](/docs/server/storage#createBucket).') ->param('fileId', '', new CustomId(), 'File ID. Choose your own unique ID or pass the string "unique()" to auto generate it. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.') ->param('file', [], new File(), 'Binary file.', false) - ->param('read', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE), 'An array of strings with read permissions. By default only the current user is granted with read permissions. [learn more about permissions](https://appwrite.io/docs/permissions) and get a full list of available permissions.', true) - ->param('write', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE), 'An array of strings with write permissions. By default only the current user is granted with write permissions. [learn more about permissions](https://appwrite.io/docs/permissions) and get a full list of available permissions.', true) + ->param('permissions', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE), 'An array of strings with permissions. By default no user is granted with any permissions. [learn more about permissions](/docs/permissions) and get a full list of available permissions.', true) ->inject('request') ->inject('response') ->inject('dbForProject') @@ -354,7 +348,7 @@ App::post('/v1/storage/buckets/:bucketId/files') ->inject('mode') ->inject('deviceFiles') ->inject('deviceLocal') - ->action(function (string $bucketId, string $fileId, mixed $file, ?array $read, ?array $write, Request $request, Response $response, Database $dbForProject, Document $user, Audit $audits, Stats $usage, Event $events, string $mode, Device $deviceFiles, Device $deviceLocal) { + ->action(function (string $bucketId, string $fileId, mixed $file, ?array $permissions, Request $request, Response $response, Database $dbForProject, Document $user, Audit $audits, Stats $usage, Event $events, string $mode, Device $deviceFiles, Device $deviceLocal) { $bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); if ( @@ -373,21 +367,37 @@ App::post('/v1/storage/buckets/:bucketId/files') } } - $read = (is_null($read) && !$user->isEmpty()) ? ['user:' . $user->getId()] : $read ?? []; // By default set read permissions for user - $write = (is_null($write) && !$user->isEmpty()) ? ['user:' . $user->getId()] : $write ?? []; + 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 ($read as $role) { - if (!Authorization::isRole($role)) { - throw new Exception('Read permissions must be one of: (' . \implode(', ', $roles) . ')', 400, Exception::USER_UNAUTHORIZED); - } - } - foreach ($write as $role) { - if (!Authorization::isRole($role)) { - throw new Exception('Write permissions must be one of: (' . \implode(', ', $roles) . ')', 400, Exception::USER_UNAUTHORIZED); + 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); + } + } } } } @@ -542,8 +552,7 @@ App::post('/v1/storage/buckets/:bucketId/files') if ($file->isEmpty()) { $doc = new Document([ '$id' => $fileId, - '$read' => $read, - '$write' => $write, + '$permissions' => $permissions, 'bucketId' => $bucket->getId(), 'name' => $fileName, 'path' => $path, @@ -569,8 +578,7 @@ App::post('/v1/storage/buckets/:bucketId/files') } } else { $file = $file - ->setAttribute('$read', $read) - ->setAttribute('$write', $write) + ->setAttribute('$permissions', $permissions) ->setAttribute('signature', $fileHash) ->setAttribute('mimeType', $mimeType) ->setAttribute('sizeActual', $sizeActual) @@ -608,8 +616,7 @@ App::post('/v1/storage/buckets/:bucketId/files') if ($file->isEmpty()) { $doc = new Document([ '$id' => $fileId, - '$read' => $read, - '$write' => $write, + '$permissions' => $permissions, 'bucketId' => $bucket->getId(), 'name' => $fileName, 'path' => $path, @@ -1288,8 +1295,7 @@ App::put('/v1/storage/buckets/:bucketId/files/:fileId') ->label('sdk.response.model', Response::MODEL_FILE) ->param('bucketId', null, new UID(), 'Storage bucket unique ID. You can create a new storage bucket using the Storage service [server integration](/docs/server/storage#createBucket).') ->param('fileId', '', new UID(), 'File unique ID.') - ->param('read', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE), 'An array of strings with read permissions. By default no user is granted with any read permissions. [learn more about permissions](https://appwrite.io/docs/permissions) and get a full list of available permissions.', true) - ->param('write', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE), 'An array of strings with write permissions. By default no user is granted with any write permissions. [learn more about permissions](https://appwrite.io/docs/permissions) and get a full list of available permissions.', true) + ->param('permissions', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE), 'An array of strings with permissions. By default no user is granted with any permissions. [learn more about permissions](/docs/permissions) and get a full list of available permissions.', true) ->inject('response') ->inject('dbForProject') ->inject('user') @@ -1297,23 +1303,40 @@ App::put('/v1/storage/buckets/:bucketId/files/:fileId') ->inject('usage') ->inject('mode') ->inject('events') - ->action(function (string $bucketId, string $fileId, ?array $read, ?array $write, Response $response, Database $dbForProject, Document $user, Audit $audits, Stats $usage, string $mode, Event $events) { + ->action(function (string $bucketId, string $fileId, ?array $permissions, Response $response, Database $dbForProject, Document $user, Audit $audits, Stats $usage, string $mode, Event $events) { $bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); - $read = (is_null($read) && !$user->isEmpty()) ? ['user:' . $user->getId()] : $read ?? []; // By default set read permissions for user - $write = (is_null($write) && !$user->isEmpty()) ? ['user:' . $user->getId()] : $write ?? []; + + 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 ($read as $role) { - if (!Authorization::isRole($role)) { - throw new Exception('Read permissions must be one of: (' . \implode(', ', $roles) . ')', 400, Exception::USER_UNAUTHORIZED); - } - } - foreach ($write as $role) { - if (!Authorization::isRole($role)) { - throw new Exception('Write permissions must be one of: (' . \implode(', ', $roles) . ')', 400, Exception::USER_UNAUTHORIZED); + foreach ($permissions as $permission) { + $matches = []; + if (\preg_match('/^(update|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); + } + } } } } @@ -1343,10 +1366,7 @@ App::put('/v1/storage/buckets/:bucketId/files/:fileId') throw new Exception('File not found', 404, Exception::STORAGE_FILE_NOT_FOUND); } - $file - ->setAttribute('$read', $read) - ->setAttribute('$write', $write) - ; + $file->setAttribute('$permissions', $permissions); if ($bucket->getAttribute('permission') === 'bucket') { $file = Authorization::skip(fn () => $dbForProject->updateDocument('bucket_' . $bucket->getInternalId(), $fileId, $file)); diff --git a/app/http.php b/app/http.php index 72aa8183ef..e7944e4f63 100644 --- a/app/http.php +++ b/app/http.php @@ -169,8 +169,10 @@ $http->on('start', function (Server $http) use ($payloadSize, $register) { 'enabled' => true, 'encryption' => true, 'antivirus' => true, - '$read' => ['role:all'], - '$write' => ['role:all'], + '$permissions' => [ + 'read(any)', + 'write(any)', + ], 'search' => 'buckets Default', ])); diff --git a/app/realtime.php b/app/realtime.php index 09e419dcce..36620f51b7 100644 --- a/app/realtime.php +++ b/app/realtime.php @@ -148,8 +148,7 @@ $server->onStart(function () use ($stats, $register, $containerId, &$statsDocume $document = new Document([ '$id' => $database->getId(), '$collection' => 'realtime', - '$read' => [], - '$write' => [], + '$permissions' => [], 'container' => $containerId, 'timestamp' => time(), 'value' => '{}' diff --git a/app/views/console/storage/bucket.phtml b/app/views/console/storage/bucket.phtml index 97b7ffab6d..81af5c81ba 100644 --- a/app/views/console/storage/bucket.phtml +++ b/app/views/console/storage/bucket.phtml @@ -132,11 +132,11 @@ $fileLimitHuman = $this->getParam('fileLimitHuman', 0); - +
Add 'role:all' for wildcard access
- +
Add 'role:all' for wildcard access
@@ -439,18 +439,18 @@ $fileLimitHuman = $this->getParam('fileLimitHuman', 0);
- +
Add 'role:all' for wildcard access
- +
Add 'role:all' for wildcard access
-
+
File Level

With File Level permissions, you have granular access control over every file. Users will only be able to access files for which they have explicit permissions.

diff --git a/src/Appwrite/Utopia/Response/Model/Bucket.php b/src/Appwrite/Utopia/Response/Model/Bucket.php index 64cbd2c24e..c68df9b817 100644 --- a/src/Appwrite/Utopia/Response/Model/Bucket.php +++ b/src/Appwrite/Utopia/Response/Model/Bucket.php @@ -28,25 +28,18 @@ class Bucket extends Model 'default' => 0, 'example' => 1592981250, ]) - ->addRule('$read', [ + ->addRule('$permissions', [ 'type' => self::TYPE_STRING, - 'description' => 'File read permissions.', + 'description' => 'File permissions.', 'default' => [], - 'example' => ['role:all'], + 'example' => ['read(any)'], 'array' => true, ]) - ->addRule('$write', [ + ->addRule('fileSecurity', [ 'type' => self::TYPE_STRING, - 'description' => 'File write permissions.', - 'default' => [], - 'example' => ['user:608f9da25e7e1'], - 'array' => true, - ]) - ->addRule('permission', [ - 'type' => self::TYPE_STRING, - 'description' => 'Bucket permission model. Possible values: `bucket` or `file`', + 'description' => 'Whether file-level security is enabled.', 'default' => '', - 'example' => 'file', + 'example' => true, ]) ->addRule('name', [ 'type' => self::TYPE_STRING, diff --git a/src/Appwrite/Utopia/Response/Model/File.php b/src/Appwrite/Utopia/Response/Model/File.php index bf1d749abe..b64297922a 100644 --- a/src/Appwrite/Utopia/Response/Model/File.php +++ b/src/Appwrite/Utopia/Response/Model/File.php @@ -34,18 +34,11 @@ class File extends Model 'default' => 0, 'example' => 1592981250, ]) - ->addRule('$read', [ + ->addRule('$permissions', [ 'type' => self::TYPE_STRING, - 'description' => 'File read permissions.', + 'description' => 'File permissions.', 'default' => [], - 'example' => 'role:all', - 'array' => true, - ]) - ->addRule('$write', [ - 'type' => self::TYPE_STRING, - 'description' => 'File write permissions.', - 'default' => [], - 'example' => 'user:608f9da25e7e1', + 'example' => 'read(role:all)', 'array' => true, ]) ->addRule('name', [ diff --git a/tests/e2e/Services/Storage/StorageBase.php b/tests/e2e/Services/Storage/StorageBase.php index edcaa0f8a6..8ade451753 100644 --- a/tests/e2e/Services/Storage/StorageBase.php +++ b/tests/e2e/Services/Storage/StorageBase.php @@ -263,10 +263,8 @@ trait StorageBase //$this->assertEquals('aes-128-gcm', $file1['body']['fileOpenSSLCipher']); //$this->assertNotEmpty($file1['body']['fileOpenSSLTag']); //$this->assertNotEmpty($file1['body']['fileOpenSSLIV']); - $this->assertIsArray($file1['body']['$read']); - $this->assertIsArray($file1['body']['$write']); - $this->assertCount(1, $file1['body']['$read']); - $this->assertCount(1, $file1['body']['$write']); + $this->assertIsArray($file1['body']['$permissions']); + $this->assertCount(2, $file1['body']['$permissions']); $file2 = $this->client->call(Client::METHOD_GET, '/storage/buckets/' . $bucketId . '/files/' . $data['fileId'] . '/preview', array_merge([ 'content-type' => 'application/json', @@ -557,10 +555,8 @@ trait StorageBase //$this->assertEquals('aes-128-gcm', $file['body']['fileOpenSSLCipher']); //$this->assertNotEmpty($file['body']['fileOpenSSLTag']); //$this->assertNotEmpty($file['body']['fileOpenSSLIV']); - $this->assertIsArray($file['body']['$read']); - $this->assertIsArray($file['body']['$write']); - $this->assertCount(1, $file['body']['$read']); - $this->assertCount(1, $file['body']['$write']); + $this->assertIsArray($file['body']['$permissions']); + $this->assertCount(2, $file['body']['$permissions']); /** * Test for FAILURE unknown Bucket diff --git a/tests/e2e/Services/Storage/StorageCustomClientTest.php b/tests/e2e/Services/Storage/StorageCustomClientTest.php index fa30f6e762..6859436b7f 100644 --- a/tests/e2e/Services/Storage/StorageCustomClientTest.php +++ b/tests/e2e/Services/Storage/StorageCustomClientTest.php @@ -119,9 +119,11 @@ class StorageCustomClientTest extends Scope ], [ 'bucketId' => 'unique()', 'name' => 'Test Bucket', - 'permission' => 'file', - 'read' => ['role:all'], - 'write' => ['role:all'], + 'fileSecurity' => true, + 'permissions' => [ + 'read(any)', + 'write(any)', + ], ]); $this->assertEquals(201, $bucket['headers']['status-code']); $this->assertNotEmpty($bucket['body']['$id']); @@ -136,8 +138,7 @@ class StorageCustomClientTest extends Scope $this->assertEquals($file['headers']['status-code'], 201); $this->assertNotEmpty($file['body']['$id']); - $this->assertContains('user:' . $this->getUser()['$id'], $file['body']['$read']); - $this->assertContains('user:' . $this->getUser()['$id'], $file['body']['$write']); + $this->assertContains('user:' . $this->getUser()['$id'], $file['body']['$permissions']); $this->assertIsInt($file['body']['$createdAt']); $this->assertEquals('permissions.png', $file['body']['name']); $this->assertEquals('image/png', $file['body']['mimeType']); diff --git a/tests/e2e/Services/Storage/StorageCustomServerTest.php b/tests/e2e/Services/Storage/StorageCustomServerTest.php index eae59e9f9b..e53d456db9 100644 --- a/tests/e2e/Services/Storage/StorageCustomServerTest.php +++ b/tests/e2e/Services/Storage/StorageCustomServerTest.php @@ -29,8 +29,7 @@ class StorageCustomServerTest extends Scope $this->assertEquals(201, $bucket['headers']['status-code']); $this->assertNotEmpty($bucket['body']['$id']); $this->assertIsInt($bucket['body']['$createdAt']); - $this->assertIsArray($bucket['body']['$read']); - $this->assertIsArray($bucket['body']['$write']); + $this->assertIsArray($bucket['body']['$permissions']); $this->assertIsArray($bucket['body']['allowedFileExtensions']); $this->assertEquals('Test Bucket', $bucket['body']['name']); $this->assertEquals(true, $bucket['body']['enabled']); @@ -188,8 +187,8 @@ class StorageCustomServerTest extends Scope $this->assertEquals(200, $bucket['headers']['status-code']); $this->assertNotEmpty($bucket['body']['$id']); $this->assertIsInt($bucket['body']['$createdAt']); - $this->assertIsArray($bucket['body']['$read']); - $this->assertIsArray($bucket['body']['$write']); + $this->assertIsArray($bucket['body']['$permissions']); + $this->assertIsArray($bucket['body']['allowedFileExtensions']); $this->assertEquals('Test Bucket Updated', $bucket['body']['name']); $this->assertEquals(false, $bucket['body']['enabled']); From ce38e74ba20ec41ac82333396cc0e5a3311d9b14 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Tue, 2 Aug 2022 21:21:53 +1200 Subject: [PATCH 003/107] Update remaining services to new permissions --- app/controllers/api/account.php | 92 ++++++++++++------- app/controllers/api/functions.php | 15 +-- app/controllers/api/projects.php | 30 ++++-- app/controllers/api/teams.php | 30 ++++-- app/controllers/api/users.php | 6 +- app/workers/builds.php | 3 +- app/workers/functions.php | 3 +- .../Utopia/Response/Model/Execution.php | 4 +- tests/e2e/Services/Webhooks/WebhooksBase.php | 42 +++------ .../Webhooks/WebhooksCustomServerTest.php | 12 +-- tests/unit/Messaging/MessagingTest.php | 54 +++++++---- 11 files changed, 169 insertions(+), 122 deletions(-) diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index 6c90441afb..f8f782b6a7 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -96,8 +96,10 @@ App::post('/v1/account') $userId = $userId == 'unique()' ? $dbForProject->getId() : $userId; $user = Authorization::skip(fn() => $dbForProject->createDocument('users', new Document([ '$id' => $userId, - '$read' => ['role:all'], - '$write' => ['user:' . $userId], + '$permissions' => [ + 'read(any)', + 'write(user:' . $userId . ')', + ], 'email' => $email, 'emailVerification' => false, 'status' => true, @@ -198,9 +200,10 @@ App::post('/v1/account/sessions/email') Authorization::setRole('user:' . $profile->getId()); - $session = $dbForProject->createDocument('sessions', $session - ->setAttribute('$read', ['user:' . $profile->getId()]) - ->setAttribute('$write', ['user:' . $profile->getId()])); + $session = $dbForProject->createDocument('sessions', $session->setAttribute('$permissions', [ + 'read(user:' . $profile->getId() . ')', + 'write(user:' . $profile->getId() . ')', + ])); $dbForProject->deleteCachedDocument('users', $profile->getId()); @@ -478,8 +481,10 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect') $userId = $dbForProject->getId(); $user = Authorization::skip(fn() => $dbForProject->createDocument('users', new Document([ '$id' => $userId, - '$read' => ['role:all'], - '$write' => ['user:' . $userId], + '$permissions' => [ + 'read(any)', + 'write(user:' . $userId . ')', + ], 'email' => $email, 'emailVerification' => true, 'status' => true, // Email should already be authenticated by OAuth2 provider @@ -542,9 +547,10 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect') $dbForProject->updateDocument('users', $user->getId(), $user); - $session = $dbForProject->createDocument('sessions', $session - ->setAttribute('$read', ['user:' . $user->getId()]) - ->setAttribute('$write', ['user:' . $user->getId()])); + $session = $dbForProject->createDocument('sessions', $session->setAttribute('$permissions', [ + 'read(user:' . $user->getId() . ')', + 'write(user:' . $user->getId() . ')', + ])); $dbForProject->deleteCachedDocument('users', $user->getId()); @@ -643,8 +649,10 @@ App::post('/v1/account/sessions/magic-url') $user = Authorization::skip(fn () => $dbForProject->createDocument('users', new Document([ '$id' => $userId, - '$read' => ['role:all'], - '$write' => ['user:' . $userId], + '$permissions' => [ + 'read(any)', + 'write(user: ' . $userId . ')', + ], 'email' => $email, 'emailVerification' => false, 'status' => true, @@ -678,8 +686,10 @@ App::post('/v1/account/sessions/magic-url') Authorization::setRole('user:' . $user->getId()); $token = $dbForProject->createDocument('tokens', $token - ->setAttribute('$read', ['user:' . $user->getId()]) - ->setAttribute('$write', ['user:' . $user->getId()])); + ->setAttribute('$permissions', [ + 'read(user: ' . $user->getId() . ')', + 'write(user:' . $user->getId() . ')', + ])); $dbForProject->deleteCachedDocument('users', $user->getId()); @@ -783,8 +793,10 @@ App::put('/v1/account/sessions/magic-url') Authorization::setRole('user:' . $user->getId()); $session = $dbForProject->createDocument('sessions', $session - ->setAttribute('$read', ['user:' . $user->getId()]) - ->setAttribute('$write', ['user:' . $user->getId()])); + ->setAttribute('$permissions', [ + 'read(user: ' . $user->getId() . ')', + 'write(user:' . $user->getId() . ')', + ])); $dbForProject->deleteCachedDocument('users', $user->getId()); @@ -884,8 +896,10 @@ App::post('/v1/account/sessions/phone') $user = Authorization::skip(fn () => $dbForProject->createDocument('users', new Document([ '$id' => $userId, - '$read' => ['role:all'], - '$write' => ['user:' . $userId], + '$permissions' => [ + 'read(any)', + 'write(user:' . $userId . ')' + ], 'email' => null, 'phone' => $number, 'emailVerification' => false, @@ -921,8 +935,10 @@ App::post('/v1/account/sessions/phone') Authorization::setRole('user:' . $user->getId()); $token = $dbForProject->createDocument('tokens', $token - ->setAttribute('$read', ['user:' . $user->getId()]) - ->setAttribute('$write', ['user:' . $user->getId()])); + ->setAttribute('$permissions', [ + 'read(user: ' . $user->getId() . ')', + 'write(user:' . $user->getId() . ')' + ])); $dbForProject->deleteCachedDocument('users', $user->getId()); @@ -1013,8 +1029,10 @@ App::put('/v1/account/sessions/phone') Authorization::setRole('user:' . $user->getId()); $session = $dbForProject->createDocument('sessions', $session - ->setAttribute('$read', ['user:' . $user->getId()]) - ->setAttribute('$write', ['user:' . $user->getId()])); + ->setAttribute('$permissions', [ + 'read(user: ' . $user->getId() . ')', + 'write(user:' . $user->getId() . ')' + ])); $dbForProject->deleteCachedDocument('users', $user->getId()); @@ -1112,8 +1130,10 @@ App::post('/v1/account/sessions/anonymous') $userId = $dbForProject->getId(); $user = Authorization::skip(fn() => $dbForProject->createDocument('users', new Document([ '$id' => $userId, - '$read' => ['role:all'], - '$write' => ['user:' . $userId], + '$permissions' => [ + 'read(any)', + 'write(user:' . $userId . ')' + ], 'email' => null, 'emailVerification' => false, 'status' => true, @@ -1155,8 +1175,10 @@ App::post('/v1/account/sessions/anonymous') Authorization::setRole('user:' . $user->getId()); $session = $dbForProject->createDocument('sessions', $session - ->setAttribute('$read', ['user:' . $user->getId()]) - ->setAttribute('$write', ['user:' . $user->getId()])); + -->setAttribute('$permissions', [ + 'read(user: ' . $user->getId() . ')', + 'write(user:' . $user->getId() . ')' + ])); $dbForProject->deleteCachedDocument('users', $user->getId()); @@ -1979,8 +2001,10 @@ App::post('/v1/account/recovery') Authorization::setRole('user:' . $profile->getId()); $recovery = $dbForProject->createDocument('tokens', $recovery - ->setAttribute('$read', ['user:' . $profile->getId()]) - ->setAttribute('$write', ['user:' . $profile->getId()])); + ->setAttribute('$permissions', [ + 'read(user: ' . $profile->getId() . ')', + 'write(user: ' . $profile->getId() . ')', + ])); $dbForProject->deleteCachedDocument('users', $profile->getId()); @@ -2140,8 +2164,10 @@ App::post('/v1/account/verification') Authorization::setRole('user:' . $user->getId()); $verification = $dbForProject->createDocument('tokens', $verification - ->setAttribute('$read', ['user:' . $user->getId()]) - ->setAttribute('$write', ['user:' . $user->getId()])); + ->setAttribute('$permissions', [ + 'read(user: ' . $user->getId() . ')', + 'write(user: ' . $user->getId() . ')', + ])); $dbForProject->deleteCachedDocument('users', $user->getId()); @@ -2295,8 +2321,10 @@ App::post('/v1/account/verification/phone') Authorization::setRole('user:' . $user->getId()); $verification = $dbForProject->createDocument('tokens', $verification - ->setAttribute('$read', ['user:' . $user->getId()]) - ->setAttribute('$write', ['user:' . $user->getId()])); + ->setAttribute('$permissions', [ + 'read(user: ' . $user->getId() . ')', + 'write(user: ' . $user->getId() . ')', + ])); $dbForProject->deleteCachedDocument('users', $user->getId()); diff --git a/app/controllers/api/functions.php b/app/controllers/api/functions.php index 21b827042d..0b7e0c57bc 100644 --- a/app/controllers/api/functions.php +++ b/app/controllers/api/functions.php @@ -569,8 +569,10 @@ App::post('/v1/functions/:functionId/deployments') if ($deployment->isEmpty()) { $deployment = $dbForProject->createDocument('deployments', new Document([ '$id' => $deploymentId, - '$read' => ['role:all'], - '$write' => ['role:all'], + '$permissions' => [ + 'read(any)', + 'write(any)' + ], 'resourceId' => $function->getId(), 'resourceType' => 'functions', 'entrypoint' => $entrypoint, @@ -598,8 +600,10 @@ App::post('/v1/functions/:functionId/deployments') if ($deployment->isEmpty()) { $deployment = $dbForProject->createDocument('deployments', new Document([ '$id' => $deploymentId, - '$read' => ['role:all'], - '$write' => ['role:all'], + '$permissions' => [ + 'read(any)', + 'write(any)' + ], 'resourceId' => $function->getId(), 'resourceType' => 'functions', 'entrypoint' => $entrypoint, @@ -854,8 +858,7 @@ App::post('/v1/functions/:functionId/executions') /** @var Document $execution */ $execution = Authorization::skip(fn () => $dbForProject->createDocument('executions', new Document([ '$id' => $executionId, - '$read' => (!$user->isEmpty()) ? ['user:' . $user->getId()] : [], - '$write' => [], + '$permissions' => !$user->isEmpty() ? ['read(user:' . $user->getId() . ')'] : [], 'functionId' => $function->getId(), 'deploymentId' => $deployment->getId(), 'trigger' => 'http', // http / schedule / event diff --git a/app/controllers/api/projects.php b/app/controllers/api/projects.php index 86996f4a58..89e606de47 100644 --- a/app/controllers/api/projects.php +++ b/app/controllers/api/projects.php @@ -85,8 +85,10 @@ App::post('/v1/projects') $project = $dbForConsole->createDocument('projects', new Document([ '$id' => $projectId, - '$read' => ['team:' . $teamId], - '$write' => ['team:' . $teamId . '/owner', 'team:' . $teamId . '/developer'], + '$permissions' => [ + 'read(team:' . $teamId . ')', + 'write(team:' . $teamId . '/owner, team:' . $teamId . '/developer)', + ], 'name' => $name, 'teamInternalId' => $team->getInternalId(), 'teamId' => $team->getId(), @@ -588,8 +590,10 @@ App::post('/v1/projects/:projectId/webhooks') $webhook = new Document([ '$id' => $dbForConsole->getId(), - '$read' => ['role:all'], - '$write' => ['role:all'], + '$permissions' => [ + 'read(any)', + 'write(any)', + ], 'projectInternalId' => $project->getInternalId(), 'projectId' => $project->getId(), 'name' => $name, @@ -832,8 +836,10 @@ App::post('/v1/projects/:projectId/keys') $key = new Document([ '$id' => $dbForConsole->getId(), - '$read' => ['role:all'], - '$write' => ['role:all'], + '$permissions' => [ + 'read(any)', + 'write(any)', + ], 'projectInternalId' => $project->getInternalId(), 'projectId' => $project->getId(), 'name' => $name, @@ -1028,8 +1034,10 @@ App::post('/v1/projects/:projectId/platforms') $platform = new Document([ '$id' => $dbForConsole->getId(), - '$read' => ['role:all'], - '$write' => ['role:all'], + '$permissions' => [ + 'read(any)', + 'write(any)', + ], 'projectInternalId' => $project->getInternalId(), 'projectId' => $project->getId(), 'type' => $type, @@ -1240,8 +1248,10 @@ App::post('/v1/projects/:projectId/domains') $domain = new Document([ '$id' => $dbForConsole->getId(), - '$read' => ['role:all'], - '$write' => ['role:all'], + '$permissions' => [ + 'read(any)', + 'write(any)', + ], 'projectInternalId' => $project->getInternalId(), 'projectId' => $project->getId(), 'updated' => \time(), diff --git a/app/controllers/api/teams.php b/app/controllers/api/teams.php index 383bcac149..f0699b31d4 100644 --- a/app/controllers/api/teams.php +++ b/app/controllers/api/teams.php @@ -59,8 +59,10 @@ App::post('/v1/teams') $teamId = $teamId == 'unique()' ? $dbForProject->getId() : $teamId; $team = Authorization::skip(fn() => $dbForProject->createDocument('teams', new Document([ '$id' => $teamId , - '$read' => ['team:' . $teamId], - '$write' => ['team:' . $teamId . '/owner'], + '$permissions' => [ + "read(team:{$teamId}", + "write(team:{$teamId}/owner)", + ], 'name' => $name, 'total' => ($isPrivilegedUser || $isAppUser) ? 0 : 1, 'search' => implode(' ', [$teamId, $name]), @@ -70,8 +72,10 @@ App::post('/v1/teams') $membershipId = $dbForProject->getId(); $membership = new Document([ '$id' => $membershipId, - '$read' => ['user:' . $user->getId(), 'team:' . $team->getId()], - '$write' => ['user:' . $user->getId(), 'team:' . $team->getId() . '/owner'], + '$permissions' => [ + "read(user:{$user->getId()}, team:{$team->getId()})", + "write(user:{$user->getId()}, team:{$team->getId()}/owner)", + ], 'userId' => $user->getId(), 'userInternalId' => $user->getInternalId(), 'teamId' => $team->getId(), @@ -328,8 +332,10 @@ App::post('/v1/teams/:teamId/memberships') $userId = $dbForProject->getId(); $invitee = Authorization::skip(fn() => $dbForProject->createDocument('users', new Document([ '$id' => $userId, - '$read' => ['user:' . $userId, 'role:all'], - '$write' => ['user:' . $userId], + '$permissions' => [ + 'read(any, user:' . $userId . ')', + 'write(user:' . $userId . ')', + ], 'email' => $email, 'emailVerification' => false, 'status' => true, @@ -365,8 +371,10 @@ App::post('/v1/teams/:teamId/memberships') $membershipId = $dbForProject->getId(); $membership = new Document([ '$id' => $membershipId, - '$read' => ['role:all'], - '$write' => ['user:' . $invitee->getId(), 'team:' . $team->getId() . '/owner'], + '$permissions' => [ + 'read(any)', + 'write(user: ' . $invitee->getId() . ', team:' . $team->getId() . '/owner)', + ], 'userId' => $invitee->getId(), 'userInternalId' => $invitee->getInternalId(), 'teamId' => $team->getId(), @@ -716,8 +724,10 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status') ], $detector->getOS(), $detector->getClient(), $detector->getDevice())); $session = $dbForProject->createDocument('sessions', $session - ->setAttribute('$read', ['user:' . $user->getId()]) - ->setAttribute('$write', ['user:' . $user->getId()])); + ->setAttribute('$permissions', [ + 'read(user: ' . $user->getId() . ')', + 'write(user: ' . $user->getId() . ')', + ])); $dbForProject->deleteCachedDocument('users', $user->getId()); diff --git a/app/controllers/api/users.php b/app/controllers/api/users.php index 8845db4810..65be0bdd48 100644 --- a/app/controllers/api/users.php +++ b/app/controllers/api/users.php @@ -57,8 +57,10 @@ App::post('/v1/users') $userId = $userId == 'unique()' ? $dbForProject->getId() : $userId; $user = $dbForProject->createDocument('users', new Document([ '$id' => $userId, - '$read' => ['role:all'], - '$write' => ['user:' . $userId], + '$permissions' => [ + 'read(any)', + 'write(user:' . $userId . ')', + ], 'email' => $email, 'emailVerification' => false, 'status' => true, diff --git a/app/workers/builds.php b/app/workers/builds.php index 2d2f66bb72..271e2967d4 100644 --- a/app/workers/builds.php +++ b/app/workers/builds.php @@ -81,8 +81,7 @@ class BuildsV1 extends Worker $buildId = $dbForProject->getId(); $build = $dbForProject->createDocument('builds', new Document([ '$id' => $buildId, - '$read' => [], - '$write' => [], + '$permissions' => [], 'startTime' => $startTime, 'deploymentId' => $deployment->getId(), 'status' => 'processing', diff --git a/app/workers/functions.php b/app/workers/functions.php index 55d71ef368..112c47ca0a 100644 --- a/app/workers/functions.php +++ b/app/workers/functions.php @@ -237,8 +237,7 @@ class FunctionsV1 extends Worker $executionId = $dbForProject->getId(); $execution = $dbForProject->createDocument('executions', new Document([ '$id' => $executionId, - '$read' => $user->isEmpty() ? [] : ['user:' . $user->getId()], - '$write' => [], + '$permissions' => $user->isEmpty() ? [] : ['read(user:' . $user->getId() . ')'], 'functionId' => $functionId, 'deploymentId' => $deploymentId, 'trigger' => $trigger, diff --git a/src/Appwrite/Utopia/Response/Model/Execution.php b/src/Appwrite/Utopia/Response/Model/Execution.php index e4e4be696a..cb4ce9c200 100644 --- a/src/Appwrite/Utopia/Response/Model/Execution.php +++ b/src/Appwrite/Utopia/Response/Model/Execution.php @@ -28,9 +28,9 @@ class Execution extends Model 'default' => 0, 'example' => 1592981250, ]) - ->addRule('$read', [ + ->addRule('$permissions', [ 'type' => self::TYPE_STRING, - 'description' => 'Execution read permissions.', + 'description' => 'Execution permissions.', 'default' => '', 'example' => 'role:all', 'array' => true, diff --git a/tests/e2e/Services/Webhooks/WebhooksBase.php b/tests/e2e/Services/Webhooks/WebhooksBase.php index c1647b6e87..bb958791c9 100644 --- a/tests/e2e/Services/Webhooks/WebhooksBase.php +++ b/tests/e2e/Services/Webhooks/WebhooksBase.php @@ -67,10 +67,8 @@ trait WebhooksBase $this->assertEquals(empty($webhook['headers']['X-Appwrite-Webhook-User-Id'] ?? ''), true); $this->assertNotEmpty($webhook['data']['$id']); $this->assertEquals($webhook['data']['name'], 'Actors'); - $this->assertIsArray($webhook['data']['$read']); - $this->assertIsArray($webhook['data']['$write']); - $this->assertCount(1, $webhook['data']['$read']); - $this->assertCount(1, $webhook['data']['$write']); + $this->assertIsArray($webhook['data']['$permissions']); + $this->assertCount(2, $webhook['data']['$permissions']); return array_merge(['actorsId' => $actorsId, 'databaseId' => $databaseId]); } @@ -224,10 +222,8 @@ trait WebhooksBase $this->assertNotEmpty($webhook['data']['$id']); $this->assertEquals($webhook['data']['firstName'], 'Chris'); $this->assertEquals($webhook['data']['lastName'], 'Evans'); - $this->assertIsArray($webhook['data']['$read']); - $this->assertIsArray($webhook['data']['$write']); - $this->assertCount(1, $webhook['data']['$read']); - $this->assertCount(1, $webhook['data']['$write']); + $this->assertIsArray($webhook['data']['$permissions']); + $this->assertCount(2, $webhook['data']['$permissions']); $data['documentId'] = $document['body']['$id']; @@ -285,10 +281,8 @@ trait WebhooksBase $this->assertNotEmpty($webhook['data']['$id']); $this->assertEquals($webhook['data']['firstName'], 'Chris1'); $this->assertEquals($webhook['data']['lastName'], 'Evans2'); - $this->assertIsArray($webhook['data']['$read']); - $this->assertIsArray($webhook['data']['$write']); - $this->assertCount(1, $webhook['data']['$read']); - $this->assertCount(1, $webhook['data']['$write']); + $this->assertIsArray($webhook['data']['$permissions']); + $this->assertCount(2, $webhook['data']['$permissions']); return $data; } @@ -353,10 +347,8 @@ trait WebhooksBase $this->assertNotEmpty($webhook['data']['$id']); $this->assertEquals($webhook['data']['firstName'], 'Bradly'); $this->assertEquals($webhook['data']['lastName'], 'Cooper'); - $this->assertIsArray($webhook['data']['$read']); - $this->assertIsArray($webhook['data']['$write']); - $this->assertCount(1, $webhook['data']['$read']); - $this->assertCount(1, $webhook['data']['$write']); + $this->assertIsArray($webhook['data']['$permissions']); + $this->assertCount(2, $webhook['data']['$permissions']); return $data; } @@ -401,8 +393,7 @@ trait WebhooksBase $this->assertNotEmpty($webhook['data']['$id']); $this->assertEquals('Test Bucket', $webhook['data']['name']); $this->assertEquals(true, $webhook['data']['enabled']); - $this->assertIsArray($webhook['data']['$read']); - $this->assertIsArray($webhook['data']['$write']); + $this->assertIsArray($webhook['data']['$permissions']); return array_merge(['bucketId' => $bucketId]); } @@ -447,8 +438,7 @@ trait WebhooksBase $this->assertNotEmpty($webhook['data']['$id']); $this->assertEquals('Test Bucket Updated', $webhook['data']['name']); $this->assertEquals(false, $webhook['data']['enabled']); - $this->assertIsArray($webhook['data']['$read']); - $this->assertIsArray($webhook['data']['$write']); + $this->assertIsArray($webhook['data']['$permissions']); return array_merge(['bucketId' => $bucket['body']['$id']]); } @@ -512,8 +502,7 @@ trait WebhooksBase $this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Project-Id'] ?? '', $this->getProject()['$id']); $this->assertEquals(empty($webhook['headers']['X-Appwrite-Webhook-User-Id'] ?? ''), ('server' === $this->getSide())); $this->assertNotEmpty($webhook['data']['$id']); - $this->assertIsArray($webhook['data']['$read']); - $this->assertIsArray($webhook['data']['$write']); + $this->assertIsArray($webhook['data']['$permissions']); $this->assertEquals($webhook['data']['name'], 'logo.png'); $this->assertIsInt($webhook['data']['$createdAt']); $this->assertNotEmpty($webhook['data']['signature']); @@ -568,8 +557,7 @@ trait WebhooksBase $this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Project-Id'] ?? '', $this->getProject()['$id']); $this->assertEquals(empty($webhook['headers']['X-Appwrite-Webhook-User-Id'] ?? ''), ('server' === $this->getSide())); $this->assertNotEmpty($webhook['data']['$id']); - $this->assertIsArray($webhook['data']['$read']); - $this->assertIsArray($webhook['data']['$write']); + $this->assertIsArray($webhook['data']['$permissions']); $this->assertEquals($webhook['data']['name'], 'logo.png'); $this->assertIsInt($webhook['data']['$createdAt']); $this->assertNotEmpty($webhook['data']['signature']); @@ -619,8 +607,7 @@ trait WebhooksBase $this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Project-Id'] ?? '', $this->getProject()['$id']); $this->assertEquals(empty($webhook['headers']['X-Appwrite-Webhook-User-Id'] ?? ''), ('server' === $this->getSide())); $this->assertNotEmpty($webhook['data']['$id']); - $this->assertIsArray($webhook['data']['$read']); - $this->assertIsArray($webhook['data']['$write']); + $this->assertIsArray($webhook['data']['$permissions']); $this->assertEquals($webhook['data']['name'], 'logo.png'); $this->assertIsInt($webhook['data']['$createdAt']); $this->assertNotEmpty($webhook['data']['signature']); @@ -665,8 +652,7 @@ trait WebhooksBase $this->assertNotEmpty($webhook['data']['$id']); $this->assertEquals('Test Bucket Updated', $webhook['data']['name']); $this->assertEquals(true, $webhook['data']['enabled']); - $this->assertIsArray($webhook['data']['$read']); - $this->assertIsArray($webhook['data']['$write']); + $this->assertIsArray($webhook['data']['$permissions']); } public function testCreateTeam(): array diff --git a/tests/e2e/Services/Webhooks/WebhooksCustomServerTest.php b/tests/e2e/Services/Webhooks/WebhooksCustomServerTest.php index c60e57f6af..503e216f87 100644 --- a/tests/e2e/Services/Webhooks/WebhooksCustomServerTest.php +++ b/tests/e2e/Services/Webhooks/WebhooksCustomServerTest.php @@ -54,10 +54,8 @@ class WebhooksCustomServerTest extends Scope $this->assertEquals(empty($webhook['headers']['X-Appwrite-Webhook-User-Id'] ?? ''), true); $this->assertNotEmpty($webhook['data']['$id']); $this->assertEquals($webhook['data']['name'], 'Actors1'); - $this->assertIsArray($webhook['data']['$read']); - $this->assertIsArray($webhook['data']['$write']); - $this->assertCount(1, $webhook['data']['$read']); - $this->assertCount(1, $webhook['data']['$write']); + $this->assertIsArray($webhook['data']['$permissions']); + $this->assertCount(2, $webhook['data']['$permissions']); return array_merge(['actorsId' => $actors['body']['$id']]); } @@ -193,10 +191,8 @@ class WebhooksCustomServerTest extends Scope $this->assertEquals(empty($webhook['headers']['X-Appwrite-Webhook-User-Id'] ?? ''), true); $this->assertNotEmpty($webhook['data']['$id']); $this->assertEquals($webhook['data']['name'], 'Demo'); - $this->assertIsArray($webhook['data']['$read']); - $this->assertIsArray($webhook['data']['$write']); - $this->assertCount(1, $webhook['data']['$read']); - $this->assertCount(1, $webhook['data']['$write']); + $this->assertIsArray($webhook['data']['$permissions']); + $this->assertCount(2, $webhook['data']['$permissions']); return []; } diff --git a/tests/unit/Messaging/MessagingTest.php b/tests/unit/Messaging/MessagingTest.php index dba684b323..18be4bb1fd 100644 --- a/tests/unit/Messaging/MessagingTest.php +++ b/tests/unit/Messaging/MessagingTest.php @@ -206,14 +206,17 @@ class MessagingTest extends TestCase payload: new Document([ '$id' => 'test', '$collection' => 'collection', - '$read' => ['role:admin'], - '$write' => ['role:admin'] + '$permissions' => [ + 'read(admin)', + 'write(admin)', + ], ]), collection: new Document([ '$id' => 'collection', - '$read' => ['role:all'], - '$write' => ['role:all'], - 'permission' => 'collection' + '$permissions' => [ + 'read(any)', + 'write(any)', + ], ]), database: new Document([ '$id' => 'database', @@ -231,14 +234,18 @@ class MessagingTest extends TestCase payload: new Document([ '$id' => 'test', '$collection' => 'collection', - '$read' => ['role:all'], - '$write' => ['role:all'] + '$permissions' => [ + 'read(any)', + 'write(any)', + ], ]), collection: new Document([ '$id' => 'collection', - '$read' => ['role:admin'], - '$write' => ['role:admin'], - 'permission' => 'document' + '$permissions' => [ + 'read(admin)', + 'write(admin)', + ], + 'documentSecurity' => true, ]), database: new Document([ '$id' => 'database', @@ -259,14 +266,17 @@ class MessagingTest extends TestCase payload: new Document([ '$id' => 'test', '$collection' => 'bucket', - '$read' => ['role:admin'], - '$write' => ['role:admin'] + '$permissions' => [ + 'read(admin)', + 'write(admin)', + ], ]), bucket: new Document([ '$id' => 'bucket', - '$read' => ['role:all'], - '$write' => ['role:all'], - 'permission' => 'bucket' + '$permissions' => [ + 'read(any)', + 'write(any)', + ], ]) ); @@ -281,14 +291,18 @@ class MessagingTest extends TestCase payload: new Document([ '$id' => 'test', '$collection' => 'bucket', - '$read' => ['role:all'], - '$write' => ['role:all'] + '$permissions' => [ + 'read(any)', + 'write(any)', + ], ]), bucket: new Document([ '$id' => 'bucket', - '$read' => ['role:admin'], - '$write' => ['role:admin'], - 'permission' => 'file' + '$permissions' => [ + 'read(admin)', + 'write(admin)', + ], + 'documentSecurity' => 'true' ]) ); From 1ab86c9331668850a7d7838d4c67bf7169f424a6 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Wed, 3 Aug 2022 16:17:49 +1200 Subject: [PATCH 004/107] Role reference updates --- app/controllers/api/account.php | 5 +- app/controllers/api/databases.php | 2 +- app/http.php | 3 +- app/realtime.php | 6 +- app/views/console/databases/collection.phtml | 4 +- app/views/console/databases/document.phtml | 4 +- app/views/console/functions/function.phtml | 2 +- app/views/console/settings/index.phtml | 2 +- app/views/console/storage/bucket.phtml | 14 +- docs/specs/authentication.drawio.svg | 4 +- src/Appwrite/Messaging/Adapter/Realtime.php | 6 +- .../Specification/Format/Swagger2.php | 2 +- .../Utopia/Response/Model/Execution.php | 2 +- src/Appwrite/Utopia/Response/Model/File.php | 2 +- src/Appwrite/Utopia/Response/Model/Func.php | 2 +- .../e2e/Services/Databases/DatabasesBase.php | 231 +++++++++++------- .../Databases/DatabasesConsoleClientTest.php | 8 +- .../Databases/DatabasesCustomClientTest.php | 11 +- .../Databases/DatabasesCustomServerTest.php | 80 +++--- .../DatabasesPermissionsGuestTest.php | 22 +- .../DatabasesPermissionsMemberTest.php | 64 +++-- .../DatabasesPermissionsTeamTest.php | 14 +- .../Realtime/RealtimeConsoleClientTest.php | 7 +- .../Realtime/RealtimeCustomClientTest.php | 60 +++-- tests/e2e/Services/Storage/StorageBase.php | 84 ++++--- .../Storage/StorageCustomClientTest.php | 51 ++-- .../Storage/StorageCustomServerTest.php | 10 +- tests/e2e/Services/Webhooks/WebhooksBase.php | 49 ++-- .../Webhooks/WebhooksCustomServerTest.php | 10 +- tests/extensions/TestHook.php | 4 +- tests/unit/Auth/AuthTest.php | 8 +- .../unit/Messaging/MessagingChannelsTest.php | 6 +- tests/unit/Messaging/MessagingGuestTest.php | 6 +- tests/unit/Messaging/MessagingTest.php | 20 +- 34 files changed, 484 insertions(+), 321 deletions(-) diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index f8f782b6a7..2f74113ab8 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -897,7 +897,7 @@ App::post('/v1/account/sessions/phone') $user = Authorization::skip(fn () => $dbForProject->createDocument('users', new Document([ '$id' => $userId, '$permissions' => [ - 'read(any)', + 'read(any)', 'write(user:' . $userId . ')' ], 'email' => null, @@ -1174,8 +1174,7 @@ App::post('/v1/account/sessions/anonymous') Authorization::setRole('user:' . $user->getId()); - $session = $dbForProject->createDocument('sessions', $session - -->setAttribute('$permissions', [ + $session = $dbForProject->createDocument('sessions', $session-- > setAttribute('$permissions', [ 'read(user: ' . $user->getId() . ')', 'write(user:' . $user->getId() . ')' ])); diff --git a/app/controllers/api/databases.php b/app/controllers/api/databases.php index f395ce49d5..84f70cd777 100644 --- a/app/controllers/api/databases.php +++ b/app/controllers/api/databases.php @@ -757,7 +757,7 @@ App::put('/v1/databases/:databaseId/collections/:collectionId') ->inject('audits') ->inject('usage') ->inject('events') - ->action(function (string $databaseId, string $collectionId, string $name, ?array $permissions, bool $documentSecurity, bool $enabled, Response $response, Database $dbForProject, EventAudit $audits, Stats $usage, Event $events) { + ->action(function (string $databaseId, string $collectionId, string $name, ?array $permissions, bool $documentSecurity, bool $enabled, Response $response, Database $dbForProject, EventAudit $audits, Stats $usage, Event $events) { $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); diff --git a/app/http.php b/app/http.php index e7944e4f63..68bf3959d8 100644 --- a/app/http.php +++ b/app/http.php @@ -163,7 +163,6 @@ $http->on('start', function (Server $http) use ($payloadSize, $register) { '$id' => 'default', '$collection' => 'buckets', 'name' => 'Default', - 'permission' => 'file', 'maximumFileSize' => (int) App::getEnv('_APP_STORAGE_LIMIT', 0), // 10MB 'allowedFileExtensions' => [], 'enabled' => true, @@ -254,7 +253,7 @@ $http->on('request', function (SwooleRequest $swooleRequest, SwooleResponse $swo try { Authorization::cleanRoles(); - Authorization::setRole('role:all'); + Authorization::setRole('any'); $app->run($request, $response); } catch (\Throwable $th) { diff --git a/app/realtime.php b/app/realtime.php index 36620f51b7..920d96dc00 100644 --- a/app/realtime.php +++ b/app/realtime.php @@ -202,7 +202,7 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats, /** * Sending current connections to project channels on the console project every 5 seconds. */ - if ($realtime->hasSubscriber('console', 'role:member', 'project')) { + if ($realtime->hasSubscriber('console', 'users', 'project')) { [$database, $returnDatabase] = getDatabase($register, '_console'); $payload = []; @@ -253,12 +253,12 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats, /** * Sending test message for SDK E2E tests every 5 seconds. */ - if ($realtime->hasSubscriber('console', 'role:guest', 'tests')) { + if ($realtime->hasSubscriber('console', 'guests', 'tests')) { $payload = ['response' => 'WS:/v1/realtime:passed']; $event = [ 'project' => 'console', - 'roles' => ['role:guest'], + 'roles' => ['guests'], 'data' => [ 'events' => ['test.event'], 'channels' => ['tests'], diff --git a/app/views/console/databases/collection.phtml b/app/views/console/databases/collection.phtml index 2428e30661..a77979c584 100644 --- a/app/views/console/databases/collection.phtml +++ b/app/views/console/databases/collection.phtml @@ -553,11 +553,11 @@ $logs = $this->getParam('logs', null);
-
Add 'role:all' for wildcard access
+
Add 'any' for wildcard access
-
Add 'role:all' for wildcard access
+
Add 'any' for wildcard access
diff --git a/app/views/console/databases/document.phtml b/app/views/console/databases/document.phtml index 17dc35f229..e6f71a1e1d 100644 --- a/app/views/console/databases/document.phtml +++ b/app/views/console/databases/document.phtml @@ -321,11 +321,11 @@ $logs = $this->getParam('logs', null); -
Add 'role:all' for wildcard access
+
Add 'any' for wildcard access
-
Add 'role:all' for wildcard access
+
Add 'any' for wildcard access
diff --git a/app/views/console/functions/function.phtml b/app/views/console/functions/function.phtml index 5d5cc20b9f..ca32ad75a3 100644 --- a/app/views/console/functions/function.phtml +++ b/app/views/console/functions/function.phtml @@ -537,7 +537,7 @@ sort($patterns); -
Add 'role:all' for wildcard access
+
Add 'any' for wildcard access
diff --git a/app/views/console/settings/index.phtml b/app/views/console/settings/index.phtml index aed016b17b..9b355a7e60 100644 --- a/app/views/console/settings/index.phtml +++ b/app/views/console/settings/index.phtml @@ -57,7 +57,7 @@ $smtpEnabled = $this->getParam('smtpEnabled', false);
- +

diff --git a/app/views/console/storage/bucket.phtml b/app/views/console/storage/bucket.phtml index 81af5c81ba..232e69c934 100644 --- a/app/views/console/storage/bucket.phtml +++ b/app/views/console/storage/bucket.phtml @@ -133,11 +133,11 @@ $fileLimitHuman = $this->getParam('fileLimitHuman', 0); -
Add 'role:all' for wildcard access
+
Add 'any' for wildcard access
-
Add 'role:all' for wildcard access
+
Add 'any' for wildcard access
getParam('fileLimitHuman', 0);
(Max file size allowed: )
- -
Add 'role:all' for wildcard access
+ +
Add 'any' for wildcard access
-
Add 'role:all' for wildcard access
+
Add 'any' for wildcard access