From 620980b31695810e3c7ceac2b926fd7ce77a9744 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Tue, 15 Jun 2021 08:44:06 +0200 Subject: [PATCH 1/5] Update webhooks.php --- app/workers/webhooks.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/workers/webhooks.php b/app/workers/webhooks.php index aab605193a..0debfd39e4 100644 --- a/app/workers/webhooks.php +++ b/app/workers/webhooks.php @@ -8,7 +8,6 @@ require_once __DIR__.'/../workers.php'; Console::title('Webhooks V1 Worker'); Console::success(APP_NAME.' webhooks worker v1 has started'); -use Appwrite\Resque\Worker; class WebhooksV1 extends Worker { @@ -92,4 +91,4 @@ class WebhooksV1 extends Worker public function shutdown(): void { } -} \ No newline at end of file +} From 0acbb6097c1033e2f0a2a3bff26814695187261f Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Wed, 16 Jun 2021 11:09:12 +0200 Subject: [PATCH 2/5] fix(realtime): add port env --- app/realtime.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/realtime.php b/app/realtime.php index 6d94b2cc4a..77c4f9fe2f 100644 --- a/app/realtime.php +++ b/app/realtime.php @@ -1,6 +1,7 @@ 64000 // Default maximum Package Size (64kb) ]; -$realtimeServer = new Server($register, config: $config); +$realtimeServer = new Server($register, port: App::getEnv('PORT', 80), config: $config); From 6ebf6bd1556dea000cebce3e46cd93827e1a2620 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Wed, 16 Jun 2021 19:43:06 +0200 Subject: [PATCH 3/5] feat(realtime): team events and permission validation --- app/controllers/api/teams.php | 8 ++- app/controllers/shared/api.php | 1 + src/Appwrite/Event/Realtime.php | 41 ++++++++++++ src/Appwrite/Realtime/Parser.php | 1 + src/Appwrite/Realtime/Server.php | 61 ++++++++++++++---- tests/e2e/Services/Realtime/RealtimeBase.php | 65 ++++++++++++++++++++ 6 files changed, 163 insertions(+), 14 deletions(-) diff --git a/app/controllers/api/teams.php b/app/controllers/api/teams.php index 3b3039a0fd..40e1bd4c60 100644 --- a/app/controllers/api/teams.php +++ b/app/controllers/api/teams.php @@ -37,10 +37,12 @@ App::post('/v1/teams') ->inject('response') ->inject('user') ->inject('projectDB') - ->action(function ($name, $roles, $response, $user, $projectDB) { + ->inject('events') + ->action(function ($name, $roles, $response, $user, $projectDB, $events) { /** @var Appwrite\Utopia\Response $response */ /** @var Appwrite\Database\Document $user */ /** @var Appwrite\Database\Database $projectDB */ + /** @var Appwrite\Event\Event $events */ Authorization::disable(); @@ -90,6 +92,10 @@ App::post('/v1/teams') } } + if (!empty($user->getId())) { + $events->setParam('userId', $user->getId()); + } + $response ->setStatusCode(Response::STATUS_CODE_CREATED) ->dynamic($team, Response::MODEL_TEAM) diff --git a/app/controllers/shared/api.php b/app/controllers/shared/api.php index 5a1eeda8de..57ffe867de 100644 --- a/app/controllers/shared/api.php +++ b/app/controllers/shared/api.php @@ -201,6 +201,7 @@ App::shutdown(function ($utopia, $request, $response, $project, $events, $audits if ($project->getId() !== 'console') { $realtime ->setEvent($events->getParam('event')) + ->setUserId($events->getParam('userId')) ->setProject($project->getId()) ->setPayload($response->getPayload()) ->trigger(); diff --git a/src/Appwrite/Event/Realtime.php b/src/Appwrite/Event/Realtime.php index fb722be54d..28eb2f7c34 100644 --- a/src/Appwrite/Event/Realtime.php +++ b/src/Appwrite/Event/Realtime.php @@ -17,6 +17,11 @@ class Realtime */ protected $event = ''; + /** + * @var string + */ + protected $userId = ''; + /** * @var array */ @@ -27,6 +32,11 @@ class Realtime */ protected $permissions = []; + /** + * @var false + */ + protected $permissionsChanged = false; + /** * @var Document */ @@ -57,6 +67,16 @@ class Realtime return $this; } + /** + * @param string $userId + * return $this + */ + public function setUserId(string $userId): self + { + $this->userId = $userId; + return $this; + } + /** * @return string */ @@ -120,6 +140,25 @@ class Realtime $this->channels[] = 'account.' . $this->payload->getId(); $this->permissions = ['user:' . $this->payload->getId()]; + break; + case strpos($this->event, 'teams.memberships') === 0: + $this->channels[] = 'memberships'; + $this->channels[] = 'memberships.' . $this->payload->getId(); + $this->permissions = ['team:' . $this->payload->getAttribute('teamId')]; + + break; + case strpos($this->event, 'teams.create') === 0: + $this->permissionsChanged = true; + $this->channels[] = 'teams'; + $this->channels[] = 'teams.' . $this->payload->getId(); + $this->permissions = ['user:' . $this->userId]; + + break; + case strpos($this->event, 'teams.') === 0: + $this->channels[] = 'teams'; + $this->channels[] = 'teams.' . $this->payload->getId(); + $this->permissions = ['team:' . $this->payload->getId()]; + break; case strpos($this->event, 'database.collections.') === 0: $this->channels[] = 'collections'; @@ -166,6 +205,8 @@ class Realtime $redis->publish('realtime', json_encode([ 'project' => $this->project, 'permissions' => $this->permissions, + 'permissionsChanged' => $this->permissionsChanged, + 'userId' => $this->userId, 'data' => [ 'event' => $this->event, 'channels' => $this->channels, diff --git a/src/Appwrite/Realtime/Parser.php b/src/Appwrite/Realtime/Parser.php index f99e7bfbe9..8a5fd1bfcf 100644 --- a/src/Appwrite/Realtime/Parser.php +++ b/src/Appwrite/Realtime/Parser.php @@ -163,6 +163,7 @@ class Parser $connections[$connection] = [ 'projectId' => $projectId, 'roles' => $roles, + 'channels' => $channels ]; } diff --git a/src/Appwrite/Realtime/Server.php b/src/Appwrite/Realtime/Server.php index 0eed3f24b4..e9e79913ec 100644 --- a/src/Appwrite/Realtime/Server.php +++ b/src/Appwrite/Realtime/Server.php @@ -2,6 +2,9 @@ namespace Appwrite\Realtime; +use Appwrite\Database\Database; +use Appwrite\Database\Adapter\MySQL as MySQLAdapter; +use Appwrite\Database\Adapter\Redis as RedisAdapter; use Appwrite\Event\Event; use Appwrite\Network\Validator\Origin; use Appwrite\Utopia\Response; @@ -17,6 +20,7 @@ use Utopia\Abuse\Abuse; use Utopia\Abuse\Adapters\TimeLimit; use Utopia\App; use Utopia\CLI\Console; +use Utopia\Config\Config; use Utopia\Exception as UtopiaException; use Utopia\Registry\Registry; use Utopia\Swoole\Request as SwooleRequest; @@ -176,7 +180,7 @@ class Server return $db; }); - $this->register->set('cache', function () use (&$redis) { // Register cache connection + $this->register->set('cache', function () use (&$redis) { return $redis; }); @@ -318,20 +322,12 @@ class Server */ public function onRedisPublish(string $payload, SwooleServer &$server, int $workerId) { - /** - * Supported Resources: - * - Collection - * - Document - * - File - * - Account - * - Session - * - Team? (not implemented yet) - * - Membership? (not implemented yet) - * - Function - * - Execution - */ $event = json_decode($payload, true); + if ($event['permissionsChanged'] && $event['userId']) { + $this->addPermission($event); + } + $receivers = Parser::identifyReceivers($event, $this->subscriptions); // Temporarily print debug logs by default for Alpha testing. @@ -390,4 +386,43 @@ class Server } } } + + private function addPermission(array $event) + { + $project = $event['project']; + $userId = $event['userId']; + + if (array_key_exists($project, $this->subscriptions) && array_key_exists('user:'.$userId, $this->subscriptions[$project])) { + $connection = array_key_first(reset($this->subscriptions[$project]['user:'.$userId])); + } else { + return; + } + + /** + * This is redundant soon and will be gone with merging the usage branch. + */ + $db = $this->register->get('dbPool')->get(); + $redis = $this->register->get('redisPool')->get(); + + $this->register->set('db', function () use (&$db) { + return $db; + }); + + $this->register->set('cache', function () use (&$redis) { + return $redis; + }); + + $projectDB = new Database(); + $projectDB->setAdapter(new RedisAdapter(new MySQLAdapter($this->register), $this->register)); + $projectDB->setNamespace('app_'.$project); + $projectDB->setMocks(Config::getParam('collections', [])); + + $user = $projectDB->getDocument($userId); + + Parser::setUser($user); + + $roles = Parser::getRoles(); + + Parser::subscribe($project, $connection, $roles, $this->subscriptions, $this->connections, $this->connections[$connection]['channels']); + } } diff --git a/tests/e2e/Services/Realtime/RealtimeBase.php b/tests/e2e/Services/Realtime/RealtimeBase.php index 164bb025c8..305345c11f 100644 --- a/tests/e2e/Services/Realtime/RealtimeBase.php +++ b/tests/e2e/Services/Realtime/RealtimeBase.php @@ -686,4 +686,69 @@ trait RealtimeBase $client->close(); } + + public function testChannelTeams() + { + $user = $this->getUser(); + $session = $user['session'] ?? ''; + $projectId = $this->getProject()['$id']; + + $client = $this->getWebsocket(['teams'], [ + 'origin' => 'http://localhost', + 'cookie' => 'a_session_'.$projectId.'=' . $session + ]); + + $response = json_decode($client->receive(), true); + + $this->assertCount(1, $response); + $this->assertArrayHasKey('teams', $response); + + /** + * Test Team Create + */ + $team = $this->client->call(Client::METHOD_POST, '/teams', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $projectId, + ], $this->getHeaders()), [ + 'name' => 'Arsenal' + ]); + + $teamId = $team['body']['$id'] ?? ''; + + $this->assertEquals(201, $team['headers']['status-code']); + $this->assertNotEmpty($team['body']['$id']); + + $response = json_decode($client->receive(), true); + + $this->assertArrayHasKey('timestamp', $response); + $this->assertCount(2, $response['channels']); + $this->assertContains('teams', $response['channels']); + $this->assertContains('teams.' . $teamId, $response['channels']); + $this->assertEquals('teams.create', $response['event']); + $this->assertNotEmpty($response['payload']); + + /** + * Test Team Update + */ + $team = $this->client->call(Client::METHOD_PUT, '/teams/'.$teamId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $projectId, + ], $this->getHeaders()), [ + 'name' => 'Manchester' + ]); + + $this->assertEquals($team['headers']['status-code'], 200); + $this->assertNotEmpty($team['body']['$id']); + + $response = json_decode($client->receive(), true); + + $this->assertArrayHasKey('timestamp', $response); + $this->assertCount(2, $response['channels']); + $this->assertContains('teams', $response['channels']); + $this->assertContains('teams.' . $teamId, $response['channels']); + $this->assertEquals('teams.update', $response['event']); + $this->assertNotEmpty($response['payload']); + + $client->close(); + } } From 43036a9ba67bf1f64cb83e865d6c3e6604d9cba2 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Thu, 17 Jun 2021 11:37:52 +0200 Subject: [PATCH 4/5] feat(realtime): add membership events --- src/Appwrite/Event/Realtime.php | 11 ++-- src/Appwrite/Realtime/Server.php | 2 +- tests/e2e/Services/Realtime/RealtimeBase.php | 55 +++++++++++++++++++- 3 files changed, 58 insertions(+), 10 deletions(-) diff --git a/src/Appwrite/Event/Realtime.php b/src/Appwrite/Event/Realtime.php index 28eb2f7c34..5942064326 100644 --- a/src/Appwrite/Event/Realtime.php +++ b/src/Appwrite/Event/Realtime.php @@ -142,19 +142,14 @@ class Realtime break; case strpos($this->event, 'teams.memberships') === 0: + $this->permissionsChanged = in_array($this->event, ['teams.memberships.update', 'teams.memberships.delete', 'teams.memberships.update.status']); $this->channels[] = 'memberships'; $this->channels[] = 'memberships.' . $this->payload->getId(); $this->permissions = ['team:' . $this->payload->getAttribute('teamId')]; - break; - case strpos($this->event, 'teams.create') === 0: - $this->permissionsChanged = true; - $this->channels[] = 'teams'; - $this->channels[] = 'teams.' . $this->payload->getId(); - $this->permissions = ['user:' . $this->userId]; - break; case strpos($this->event, 'teams.') === 0: + $this->permissionsChanged = $this->event === 'teams.create'; $this->channels[] = 'teams'; $this->channels[] = 'teams.' . $this->payload->getId(); $this->permissions = ['team:' . $this->payload->getId()]; @@ -187,7 +182,7 @@ class Realtime $this->permissions = $this->payload->getAttribute('$permissions.read'); } break; - } + } } /** diff --git a/src/Appwrite/Realtime/Server.php b/src/Appwrite/Realtime/Server.php index e9e79913ec..4351e22ce2 100644 --- a/src/Appwrite/Realtime/Server.php +++ b/src/Appwrite/Realtime/Server.php @@ -324,7 +324,7 @@ class Server { $event = json_decode($payload, true); - if ($event['permissionsChanged'] && $event['userId']) { + if ($event['permissionsChanged'] && isset($event['userId'])) { $this->addPermission($event); } diff --git a/tests/e2e/Services/Realtime/RealtimeBase.php b/tests/e2e/Services/Realtime/RealtimeBase.php index 305345c11f..c17f90b934 100644 --- a/tests/e2e/Services/Realtime/RealtimeBase.php +++ b/tests/e2e/Services/Realtime/RealtimeBase.php @@ -687,7 +687,7 @@ trait RealtimeBase $client->close(); } - public function testChannelTeams() + public function testChannelTeams(): array { $user = $this->getUser(); $session = $user['session'] ?? ''; @@ -750,5 +750,58 @@ trait RealtimeBase $this->assertNotEmpty($response['payload']); $client->close(); + + return ['teamId' => $teamId]; + } + + /** + * @depends testChannelTeams + */ + public function testChannelMemberships(array $data) + { + $teamId = $data['teamId'] ?? ''; + + $user = $this->getUser(); + $session = $user['session'] ?? ''; + $projectId = $this->getProject()['$id']; + + $client = $this->getWebsocket(['memberships'], [ + 'origin' => 'http://localhost', + 'cookie' => 'a_session_'.$projectId.'='.$session + ]); + + $response = json_decode($client->receive(), true); + + $this->assertCount(1, $response); + $this->assertArrayHasKey('memberships', $response); + + $response = $this->client->call(Client::METHOD_GET, '/teams/'.$teamId.'/memberships', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $membershipId = $response['body']['memberships'][0]['$id']; + + /** + * Test Update Membership + */ + $roles = ['admin', 'editor', 'uncle']; + $this->client->call(Client::METHOD_PATCH, '/teams/'.$teamId.'/memberships/'.$membershipId, array_merge([ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'roles' => $roles + ]); + + $response = json_decode($client->receive(), true); + $this->assertArrayHasKey('timestamp', $response); + $this->assertCount(2, $response['channels']); + $this->assertContains('memberships', $response['channels']); + $this->assertContains('memberships.' . $membershipId, $response['channels']); + $this->assertEquals('teams.memberships.update', $response['event']); + $this->assertNotEmpty($response['payload']); + + $client->close(); } } From 05b7b3da24d1d621625e348ab8d9aa2d04ebd615 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Fri, 18 Jun 2021 10:42:47 +0200 Subject: [PATCH 5/5] chore(composer): update lock file --- composer.lock | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/composer.lock b/composer.lock index 03f829151a..57c562ea8d 100644 --- a/composer.lock +++ b/composer.lock @@ -4823,16 +4823,16 @@ }, { "name": "sebastian/type", - "version": "2.3.2", + "version": "2.3.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/type.git", - "reference": "0d1c587401514d17e8f9258a27e23527cb1b06c1" + "reference": "b8cd8a1c753c90bc1a0f5372170e3e489136f914" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/0d1c587401514d17e8f9258a27e23527cb1b06c1", - "reference": "0d1c587401514d17e8f9258a27e23527cb1b06c1", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/b8cd8a1c753c90bc1a0f5372170e3e489136f914", + "reference": "b8cd8a1c753c90bc1a0f5372170e3e489136f914", "shasum": "" }, "require": { @@ -4867,7 +4867,7 @@ "homepage": "https://github.com/sebastianbergmann/type", "support": { "issues": "https://github.com/sebastianbergmann/type/issues", - "source": "https://github.com/sebastianbergmann/type/tree/2.3.2" + "source": "https://github.com/sebastianbergmann/type/tree/2.3.4" }, "funding": [ { @@ -4875,7 +4875,7 @@ "type": "github" } ], - "time": "2021-06-04T13:02:07+00:00" + "time": "2021-06-15T12:49:02+00:00" }, { "name": "sebastian/version", @@ -4984,16 +4984,16 @@ }, { "name": "symfony/console", - "version": "v5.3.0", + "version": "v5.3.2", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "058553870f7809087fa80fa734704a21b9bcaeb2" + "reference": "649730483885ff2ca99ca0560ef0e5f6b03f2ac1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/058553870f7809087fa80fa734704a21b9bcaeb2", - "reference": "058553870f7809087fa80fa734704a21b9bcaeb2", + "url": "https://api.github.com/repos/symfony/console/zipball/649730483885ff2ca99ca0560ef0e5f6b03f2ac1", + "reference": "649730483885ff2ca99ca0560ef0e5f6b03f2ac1", "shasum": "" }, "require": { @@ -5062,7 +5062,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v5.3.0" + "source": "https://github.com/symfony/console/tree/v5.3.2" }, "funding": [ { @@ -5078,7 +5078,7 @@ "type": "tidelift" } ], - "time": "2021-05-26T17:43:10+00:00" + "time": "2021-06-12T09:42:48+00:00" }, { "name": "symfony/deprecation-contracts", @@ -5635,16 +5635,16 @@ }, { "name": "symfony/string", - "version": "v5.3.0", + "version": "v5.3.2", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "a9a0f8b6aafc5d2d1c116dcccd1573a95153515b" + "reference": "0732e97e41c0a590f77e231afc16a327375d50b0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/a9a0f8b6aafc5d2d1c116dcccd1573a95153515b", - "reference": "a9a0f8b6aafc5d2d1c116dcccd1573a95153515b", + "url": "https://api.github.com/repos/symfony/string/zipball/0732e97e41c0a590f77e231afc16a327375d50b0", + "reference": "0732e97e41c0a590f77e231afc16a327375d50b0", "shasum": "" }, "require": { @@ -5698,7 +5698,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v5.3.0" + "source": "https://github.com/symfony/string/tree/v5.3.2" }, "funding": [ { @@ -5714,7 +5714,7 @@ "type": "tidelift" } ], - "time": "2021-05-26T17:43:10+00:00" + "time": "2021-06-06T09:51:56+00:00" }, { "name": "textalk/websocket",