From 6ebf6bd1556dea000cebce3e46cd93827e1a2620 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Wed, 16 Jun 2021 19:43:06 +0200 Subject: [PATCH 1/2] 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 2/2] 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(); } }