diff --git a/app/config/events.php b/app/config/events.php index 18edf4ba4a..6cec0ebba7 100644 --- a/app/config/events.php +++ b/app/config/events.php @@ -157,6 +157,16 @@ return [ 'model' => Response::MODEL_BUCKET, 'note' => '', ], + 'storage.buckets.update' => [ + 'description' => 'This event triggers when a storage bucket is updated.', + 'model' => Response::MODEL_BUCKET, + 'note' => '', + ], + 'storage.buckets.delete' => [ + 'description' => 'This event triggers when a storage bucket is deleted.', + 'model' => Response::MODEL_BUCKET, + 'note' => '', + ], 'users.create' => [ 'description' => 'This event triggers when a user is created from the users API.', 'model' => Response::MODEL_USER, diff --git a/app/controllers/api/storage.php b/app/controllers/api/storage.php index 2191342102..ed7034d805 100644 --- a/app/controllers/api/storage.php +++ b/app/controllers/api/storage.php @@ -48,13 +48,10 @@ App::post('/v1/storage/buckets') ->param('antiVirus', true, new Boolean(), 'Is virus scanning enabled?', true) ->inject('response') ->inject('dbForInternal') - ->inject('user') ->inject('audits') - ->action(function ($name, $read, $write, $maximumFileSize, $allowedFileExtensions, $enabled, $adapter, $encryption, $antiVirus, $response, $dbForInternal, $user, $audits) { - /** @var Utopia\Swoole\Request $request */ + ->action(function ($name, $read, $write, $maximumFileSize, $allowedFileExtensions, $enabled, $adapter, $encryption, $antiVirus, $response, $dbForInternal, $audits) { /** @var Appwrite\Utopia\Response $response */ /** @var Utopia\Database\Database $dbForInternal */ - /** @var Appwrite\Database\Document $user */ /** @var Appwrite\Event\Event $audits */ $data = $dbForInternal->createDocument('buckets', new Document([ @@ -138,6 +135,116 @@ App::get('/v1/storage/buckets/:bucketId') $response->dynamic2($bucket, Response::MODEL_BUCKET); }); +App::put('/v1/storage/buckets/:bucketId') + ->desc('Update Bucket') + ->groups(['api', 'storage']) + ->label('scope', 'buckets.write') + ->label('event', 'storage.buckets.update') + ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) + ->label('sdk.namespace', 'storage') + ->label('sdk.method', 'updateBucket') + ->label('sdk.description', '/docs/references/storage/update-bucket.md') + ->label('sdk.response.code', Response::STATUS_CODE_OK) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_BUCKET) + ->param('bucketId', '', new UID(), 'Bucket unique ID.') + ->param('name', null, new Text(128), 'Bucket name', false) + ->param('read', null, new ArrayList(new Text(64)), '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 ArrayList(new Text(64)), '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('maximumFileSize', 0, new Integer(), 'Maximum file size allowed.', true) + ->param('allowedFileExtensions', ['*'], new ArrayList(new Text(64)), 'Allowed file extensions', true) + ->param('enabled', true, new Boolean(), 'Is bucket enabled?', true) + ->param('encryption', true, new Boolean(), 'Is encryption enabled?', true) + ->param('antiVirus', true, new Boolean(), 'Is virus scanning enabled?', true) + ->inject('response') + ->inject('dbForInternal') + ->inject('audits') + ->action(function ($bucketId, $name, $read, $write, $maximumFileSize, $allowedFileExtensions, $enabled, $encryption, $antiVirus, $response, $dbForInternal, $audits) { + /** @var Appwrite\Utopia\Response $response */ + /** @var Utopia\Database\Database $dbForInternal */ + /** @var Appwrite\Event\Event $audits */ + + $bucket = $dbForInternal->getDocument('buckets', $bucketId); + + if ($bucket->isEmpty()) { + throw new Exception('Bucket not found', 404); + } + + $read ??= $bucket->getAttribute('$read', []); // By default inherit read permissions + $write ??= $bucket->getAttribute('$write',[]); // By default inherit write permissions + + $bucket = $dbForInternal->updateDocument('buckets', $bucket->getId(), $bucket + ->setAttribute('name',$name) + ->setAttribute('$read',$read) + ->setAttribute('$write',$write) + ->setAttribute('maximumFileSize',$maximumFileSize) + ->setAttribute('allowedFileExtensions',$allowedFileExtensions) + ->setAttribute('enabled',$enabled) + ->setAttribute('encryption',$encryption) + ->setAttribute('antiVirus',$antiVirus) + ); + + $audits + ->setParam('event', 'storage.buckets.update') + ->setParam('resource', 'storage/buckets/' . $bucket->getId()) + ->setParam('data', $bucket->getArrayCopy()) + ; + + $response->dynamic2($bucket, Response::MODEL_BUCKET); + }); + +App::delete('/v1/storage/buckets/:bucketId') + ->desc('Delete Bucket') + ->groups(['api', 'storage']) + ->label('scope', 'buckets.write') + ->label('event', 'storage.buckets.delete') + ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) + ->label('sdk.namespace', 'storage') + ->label('sdk.method', 'deleteBucket') + ->label('sdk.description', '/docs/references/storage/delete-bucket.md') + ->label('sdk.response.code', Response::STATUS_CODE_NOCONTENT) + ->label('sdk.response.model', Response::MODEL_NONE) + ->param('bucketId', '', new UID(), 'Bucket unique ID.') + ->inject('response') + ->inject('dbForInternal') + ->inject('audits') + ->inject('deletes') + ->inject('events') + ->action(function ($bucketId, $response, $dbForInternal, $audits, $deletes, $events) { + /** @var Appwrite\Utopia\Response $response */ + /** @var Utopia\Database\Database $dbForInternal */ + /** @var Appwrite\Event\Event $audits */ + /** @var Appwrite\Event\Event $deletes */ + /** @var Appwrite\Event\Event $events */ + + $bucket = $dbForInternal->getDocument('buckets', $bucketId); + + if ($bucket->isEmpty()) { + throw new Exception('Bucket not found', 404); + } + + $deletes + ->setParam('type', DELETE_TYPE_DOCUMENT) + ->setParam('document', $bucket) + ; + + if(!$dbForInternal->deleteDocument('buckets', $bucketId)) { + throw new Exception('Failed to remove project from DB', 500); + } + + $events + ->setParam('eventData', $response->output2($bucket, Response::MODEL_BUCKET)) + ; + + $audits + ->setParam('event', 'storage.buckets.delete') + ->setParam('resource', 'storage/buckets/' . $bucket->getId()) + ->setParam('data', $bucket->getArrayCopy()) + ; + + $response->noContent(); + }); + App::post('/v1/storage/files') ->desc('Create File') ->groups(['api', 'storage']) diff --git a/docs/references/storage/delete-bucket.md b/docs/references/storage/delete-bucket.md new file mode 100644 index 0000000000..99b4c792e1 --- /dev/null +++ b/docs/references/storage/delete-bucket.md @@ -0,0 +1 @@ +Delete a storage bucket by its unique ID. \ No newline at end of file diff --git a/docs/references/storage/update-bucket.md b/docs/references/storage/update-bucket.md new file mode 100644 index 0000000000..8a9ca1be7b --- /dev/null +++ b/docs/references/storage/update-bucket.md @@ -0,0 +1 @@ +Update a storage bucket by its unique ID. \ No newline at end of file diff --git a/tests/e2e/Scopes/ProjectCustom.php b/tests/e2e/Scopes/ProjectCustom.php index 53586162c5..2455a45f5d 100644 --- a/tests/e2e/Scopes/ProjectCustom.php +++ b/tests/e2e/Scopes/ProjectCustom.php @@ -127,6 +127,8 @@ trait ProjectCustom 'storage.files.update', 'storage.files.delete', 'storage.buckets.create', + 'storage.buckets.update', + 'storage.buckets.delete', 'users.create', 'users.update.prefs', 'users.update.status', diff --git a/tests/e2e/Services/Storage/StorageCustomServerTest.php b/tests/e2e/Services/Storage/StorageCustomServerTest.php index 87fa92836e..8915bf5526 100644 --- a/tests/e2e/Services/Storage/StorageCustomServerTest.php +++ b/tests/e2e/Services/Storage/StorageCustomServerTest.php @@ -75,7 +75,7 @@ class StorageCustomServerTest extends Scope /** * @depends testCreateBucket */ - public function testGetBucket($data): array + public function testGetBucket(array $data): array { $id = $data['bucketId'] ?? ''; /** @@ -111,4 +111,79 @@ class StorageCustomServerTest extends Scope return $data; } + + /** + * @depends testCreateBucket + */ + public function testUpdateBucket(array $data):array + { + $id = $data['bucketId'] ?? ''; + /** + * Test for SUCCESS + */ + $bucket = $this->client->call(Client::METHOD_POST, '/storage/buckets/' . $id, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'name' => 'Test Bucket Updated', + 'enabled' => false, + ]); + $this->assertEquals(201, $bucket['headers']['status-code']); + $this->assertNotEmpty($bucket['body']['$id']); + $this->assertIsInt($bucket['body']['dateCreated']); + $this->assertIsArray($bucket['body']['$read']); + $this->assertIsArray($bucket['body']['$write']); + $this->assertIsArray($bucket['body']['allowedFileExtensions']); + $this->assertEquals('Test Bucket Updated', $bucket['body']['name']); + $this->assertEquals(false, $bucket['body']['enabled']); + $this->assertEquals(true, $bucket['body']['encryption']); + $this->assertEquals(true, $bucket['body']['antiVirus']); + $this->assertEquals('local', $bucket['body']['adapter']); + $bucketId = $bucket['body']['$id']; + /** + * Test for FAILURE + */ + $bucket = $this->client->call(Client::METHOD_POST, '/storage/buckets/' . $id, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'name' => '', + 'enabled' => 'false', + ]); + $this->assertEquals(400, $bucket['headers']['status-code']); + + return ['bucketId' => $bucketId]; + } + + /** + * @depends testCreateBucket + */ + public function testDeleteBucket(array $data): array + { + $id = $data['bucketId'] ?? ''; + /** + * Test for SUCCESS + */ + $response = $this->client->call(Client::METHOD_DELETE, '/storage/buckets/' . $id, + array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + $this->assertEquals(204, $response['headers']['status-code']); + $this->assertEmpty($response['body']); + + $response = $this->client->call(Client::METHOD_GET, '/storage/buckets/' . $id, + array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + $this->assertEquals(404, $response['headers']['status-code']); + + /** + * Test for FAILURE + */ + + return $data; + } + } \ No newline at end of file diff --git a/tests/e2e/Services/Webhooks/WebhooksBase.php b/tests/e2e/Services/Webhooks/WebhooksBase.php index a6f3bc2785..9d22230c02 100644 --- a/tests/e2e/Services/Webhooks/WebhooksBase.php +++ b/tests/e2e/Services/Webhooks/WebhooksBase.php @@ -587,4 +587,79 @@ trait WebhooksBase return array_merge(['bucketId' => $bucket['body']['$id']]); } + + /** + * @depends testCreateStorageBucket + */ + public function testUpdateStorageBucket(array $data): array + { + $id = $data['bucketId']; + /** + * Test for SUCCESS + */ + $bucket = $this->client->call(Client::METHOD_PUT, '/storage/buckets/' . $id, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'name' => 'Test Bucket Updated', + 'enabled' => false, + ]); + + $this->assertEquals($bucket['headers']['status-code'], 200); + $this->assertNotEmpty($bucket['body']['$id']); + + $webhook = $this->getLastRequest(); + + $this->assertEquals($webhook['method'], 'POST'); + $this->assertEquals($webhook['headers']['Content-Type'], 'application/json'); + $this->assertEquals($webhook['headers']['User-Agent'], 'Appwrite-Server vdev. Please report abuse at security@appwrite.io'); + $this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Event'], 'storage.buckets.update'); + $this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], 'not-yet-implemented'); + $this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Id'] ?? '', $this->getProject()['webhookId']); + $this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Project-Id'] ?? '', $this->getProject()['$id']); + $this->assertEquals(empty($webhook['headers']['X-Appwrite-Webhook-User-Id'] ?? ''), true); + $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']); + + return array_merge(['bucketId' => $bucket['body']['$id']]); + } + + /** + * @depends testCreateStorageBucket + */ + public function testDeleteStorageBucket(array $data) + { + $id = $data['bucketId']; + /** + * Test for SUCCESS + */ + $bucket = $this->client->call(Client::METHOD_DELETE, '/storage/buckets/' . $id, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + + $this->assertEquals($bucket['headers']['status-code'], 204); + $this->assertEmpty($bucket['body']); + + $webhook = $this->getLastRequest(); + + $this->assertEquals($webhook['method'], 'POST'); + $this->assertEquals($webhook['headers']['Content-Type'], 'application/json'); + $this->assertEquals($webhook['headers']['User-Agent'], 'Appwrite-Server vdev. Please report abuse at security@appwrite.io'); + $this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Event'], 'storage.buckets.delete'); + $this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], 'not-yet-implemented'); + $this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Id'] ?? '', $this->getProject()['webhookId']); + $this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Project-Id'] ?? '', $this->getProject()['$id']); + $this->assertEquals(empty($webhook['headers']['X-Appwrite-Webhook-User-Id'] ?? ''), true); + $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']); + } } \ No newline at end of file