diff --git a/Dockerfile b/Dockerfile index 22b37982d0..13b018df0f 100755 --- a/Dockerfile +++ b/Dockerfile @@ -28,6 +28,8 @@ RUN \ apk add boost boost-dev; \ fi +RUN apk add libwebp + WORKDIR /usr/src/code COPY --from=composer /usr/local/src/vendor /usr/src/code/vendor diff --git a/app/config/roles.php b/app/config/roles.php index 65b9643b89..1b64067ecc 100644 --- a/app/config/roles.php +++ b/app/config/roles.php @@ -17,7 +17,6 @@ $member = [ 'files.read', 'files.write', 'projects.read', - 'projects.write', 'locale.read', 'avatars.read', 'execution.read', @@ -49,6 +48,7 @@ $admins = [ 'collections.write', 'platforms.read', 'platforms.write', + 'projects.write', 'keys.read', 'keys.write', 'webhooks.read', diff --git a/app/controllers/api/projects.php b/app/controllers/api/projects.php index 3a8c232195..aafd480398 100644 --- a/app/controllers/api/projects.php +++ b/app/controllers/api/projects.php @@ -1199,7 +1199,7 @@ App::delete('/v1/projects/:projectId/webhooks/:webhookId') App::post('/v1/projects/:projectId/keys') ->desc('Create key') ->groups(['api', 'projects']) - ->label('scope', 'projects.write') + ->label('scope', 'keys.write') ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN]) ->label('sdk.namespace', 'projects') ->label('sdk.method', 'createKey') @@ -1249,7 +1249,7 @@ App::post('/v1/projects/:projectId/keys') App::get('/v1/projects/:projectId/keys') ->desc('List keys') ->groups(['api', 'projects']) - ->label('scope', 'projects.read') + ->label('scope', 'keys.read') ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN]) ->label('sdk.namespace', 'projects') ->label('sdk.method', 'listKeys') @@ -1281,7 +1281,7 @@ App::get('/v1/projects/:projectId/keys') App::get('/v1/projects/:projectId/keys/:keyId') ->desc('Get key') ->groups(['api', 'projects']) - ->label('scope', 'projects.read') + ->label('scope', 'keys.read') ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN]) ->label('sdk.namespace', 'projects') ->label('sdk.method', 'getKey') @@ -1315,7 +1315,7 @@ App::get('/v1/projects/:projectId/keys/:keyId') App::put('/v1/projects/:projectId/keys/:keyId') ->desc('Update key') ->groups(['api', 'projects']) - ->label('scope', 'projects.write') + ->label('scope', 'keys.write') ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN]) ->label('sdk.namespace', 'projects') ->label('sdk.method', 'updateKey') @@ -1361,7 +1361,7 @@ App::put('/v1/projects/:projectId/keys/:keyId') App::delete('/v1/projects/:projectId/keys/:keyId') ->desc('Delete key') ->groups(['api', 'projects']) - ->label('scope', 'projects.write') + ->label('scope', 'keys.write') ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN]) ->label('sdk.namespace', 'projects') ->label('sdk.method', 'deleteKey') @@ -1436,7 +1436,7 @@ App::post('/v1/projects/:projectId/platforms') ->desc('Create platform') ->groups(['api', 'projects']) ->label('audits.event', 'platforms.create') - ->label('scope', 'projects.write') + ->label('scope', 'platforms.write') ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN]) ->label('sdk.namespace', 'projects') ->label('sdk.method', 'createPlatform') @@ -1486,7 +1486,7 @@ App::post('/v1/projects/:projectId/platforms') App::get('/v1/projects/:projectId/platforms') ->desc('List platforms') ->groups(['api', 'projects']) - ->label('scope', 'projects.read') + ->label('scope', 'platforms.read') ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN]) ->label('sdk.namespace', 'projects') ->label('sdk.method', 'listPlatforms') @@ -1518,7 +1518,7 @@ App::get('/v1/projects/:projectId/platforms') App::get('/v1/projects/:projectId/platforms/:platformId') ->desc('Get platform') ->groups(['api', 'projects']) - ->label('scope', 'projects.read') + ->label('scope', 'platforms.read') ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN]) ->label('sdk.namespace', 'projects') ->label('sdk.method', 'getPlatform') @@ -1552,7 +1552,7 @@ App::get('/v1/projects/:projectId/platforms/:platformId') App::put('/v1/projects/:projectId/platforms/:platformId') ->desc('Update platform') ->groups(['api', 'projects']) - ->label('scope', 'projects.write') + ->label('scope', 'platforms.write') ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN]) ->label('sdk.namespace', 'projects') ->label('sdk.method', 'updatePlatform') @@ -1600,7 +1600,7 @@ App::delete('/v1/projects/:projectId/platforms/:platformId') ->desc('Delete platform') ->groups(['api', 'projects']) ->label('audits.event', 'platforms.delete') - ->label('scope', 'projects.write') + ->label('scope', 'platforms.write') ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN]) ->label('sdk.namespace', 'projects') ->label('sdk.method', 'deletePlatform') diff --git a/app/controllers/api/teams.php b/app/controllers/api/teams.php index f98cdd721c..146b5d5f81 100644 --- a/app/controllers/api/teams.php +++ b/app/controllers/api/teams.php @@ -43,6 +43,7 @@ use Utopia\Validator\ArrayList; use Utopia\Validator\Assoc; use Utopia\Validator\Host; use Utopia\Validator\Text; +use Utopia\Validator\WhiteList; App::post('/v1/teams') ->desc('Create team') @@ -394,7 +395,17 @@ App::post('/v1/teams/:teamId/memberships') ->param('email', '', new Email(), 'Email of the new team member.', true) ->param('userId', '', new UID(), 'ID of the user to be added to a team.', true) ->param('phone', '', new Phone(), 'Phone number. Format this number with a leading \'+\' and a country code, e.g., +16175551212.', true) - ->param('roles', [], new ArrayList(new Key(), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Array of strings. Use this param to set the user roles in the team. A role can be any string. Learn more about [roles and permissions](https://appwrite.io/docs/permissions). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' roles are allowed, each 32 characters long.') + ->param('roles', [], function (Document $project) { + if ($project->getId() === 'console') { + ; + $roles = array_keys(Config::getParam('roles', [])); + array_filter($roles, function ($role) { + return !in_array($role, [Auth::USER_ROLE_APPS, Auth::USER_ROLE_GUESTS, Auth::USER_ROLE_USERS]); + }); + return new ArrayList(new WhiteList($roles), APP_LIMIT_ARRAY_PARAMS_SIZE); + } + return new ArrayList(new Key(), APP_LIMIT_ARRAY_PARAMS_SIZE); + }, 'Array of strings. Use this param to set the user roles in the team. A role can be any string. Learn more about [roles and permissions](https://appwrite.io/docs/permissions). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' roles are allowed, each 32 characters long.', false, ['project']) ->param('url', '', fn ($clients) => new Host($clients), 'URL to redirect the user back to your app from the invitation email. This parameter is not required when an API key is supplied. Only URLs from hostnames in your project platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.', true, ['clients']) // TODO add our own built-in confirm page ->param('name', '', new Text(128), 'Name of the new team member. Max length: 128 chars.', true) ->inject('response') @@ -868,7 +879,17 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId') ->label('sdk.response.model', Response::MODEL_MEMBERSHIP) ->param('teamId', '', new UID(), 'Team ID.') ->param('membershipId', '', new UID(), 'Membership ID.') - ->param('roles', [], new ArrayList(new Key(), APP_LIMIT_ARRAY_PARAMS_SIZE), 'An array of strings. Use this param to set the user\'s roles in the team. A role can be any string. Learn more about [roles and permissions](https://appwrite.io/docs/permissions). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' roles are allowed, each 32 characters long.') + ->param('roles', [], function (Document $project) { + if ($project->getId() === 'console') { + ; + $roles = array_keys(Config::getParam('roles', [])); + array_filter($roles, function ($role) { + return !in_array($role, [Auth::USER_ROLE_APPS, Auth::USER_ROLE_GUESTS, Auth::USER_ROLE_USERS]); + }); + return new ArrayList(new WhiteList($roles), APP_LIMIT_ARRAY_PARAMS_SIZE); + } + return new ArrayList(new Key(), APP_LIMIT_ARRAY_PARAMS_SIZE); + }, 'An array of strings. Use this param to set the user\'s roles in the team. A role can be any string. Learn more about [roles and permissions](https://appwrite.io/docs/permissions). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' roles are allowed, each 32 characters long.', false, ['project']) ->inject('request') ->inject('response') ->inject('user') diff --git a/app/controllers/mock.php b/app/controllers/mock.php index fdb1d80dcc..bc071fc885 100644 --- a/app/controllers/mock.php +++ b/app/controllers/mock.php @@ -158,7 +158,7 @@ App::patch('/v1/mock/functions-v2') App::post('/v1/mock/api-key-unprefixed') ->desc('Create API Key (without standard prefix)') ->groups(['mock', 'api', 'projects']) - ->label('scope', 'projects.write') + ->label('scope', 'public') ->label('docs', false) ->param('projectId', '', new UID(), 'Project ID.') ->inject('response') diff --git a/app/controllers/shared/api.php b/app/controllers/shared/api.php index 357d73adc8..f0d896c95a 100644 --- a/app/controllers/shared/api.php +++ b/app/controllers/shared/api.php @@ -160,42 +160,22 @@ App::init() ->inject('session') ->inject('servers') ->inject('mode') - ->action(function (App $utopia, Request $request, Database $dbForConsole, Document $project, Document $user, ?Document $session, array $servers, string $mode) { + ->inject('team') + ->action(function (App $utopia, Request $request, Database $dbForConsole, Document $project, Document $user, ?Document $session, array $servers, string $mode, Document $team) { $route = $utopia->getRoute(); if ($project->isEmpty()) { throw new Exception(Exception::PROJECT_NOT_FOUND); } - /** - * ACL Check - */ + /** Default role */ + $roles = Config::getParam('roles', []); $role = ($user->isEmpty()) ? Role::guests()->toString() : Role::users()->toString(); - // Add user roles - $memberships = $user->find('teamId', $project->getAttribute('teamId'), 'memberships'); - - if ($memberships) { - foreach ($memberships->getAttribute('roles', []) as $memberRole) { - switch ($memberRole) { - case 'owner': - $role = Auth::USER_ROLE_OWNER; - break; - case 'admin': - $role = Auth::USER_ROLE_ADMIN; - break; - case 'developer': - $role = Auth::USER_ROLE_DEVELOPER; - break; - } - } - } - - $roles = Config::getParam('roles', []); - $scope = $route->getLabel('scope', 'none'); // Allowed scope for chosen route - $scopes = $roles[$role]['scopes']; // Allowed scopes for user role + /** Allowed Scopes for the role */ + $scopes = $roles[$role]['scopes']; $apiKey = $request->getHeader('x-appwrite-key', ''); @@ -294,13 +274,38 @@ App::init() } } } + // Admin User Authentication + elseif (($project->getId() === 'console' && !$team->isEmpty() && !$user->isEmpty()) || ($project->getId() !== 'console' && !$user->isEmpty() && $mode === APP_MODE_ADMIN)) { + $teamId = $team->getId(); + $adminRoles = []; + $memberships = $user->getAttribute('memberships', []); + foreach ($memberships as $membership) { + if ($membership->getAttribute('confirm', false) === true && $membership->getAttribute('teamId') === $teamId) { + $adminRoles = $membership->getAttribute('roles', []); + break; + } + } + + if (empty($adminRoles)) { + throw new Exception(Exception::USER_UNAUTHORIZED); + } + + $scopes = []; // reset scope if admin + foreach ($adminRoles as $role) { + $scopes = \array_merge($scopes, $roles[$role]['scopes']); + } + + Authorization::setDefaultStatus(false); // Cancel security segmentation for admin users. + } + + $scopes = \array_unique($scopes); Authorization::setRole($role); - foreach (Auth::getRoles($user) as $authRole) { Authorization::setRole($authRole); } + /** Do not allow access to disabled services */ $service = $route->getLabel('sdk.namespace', ''); if (!empty($service)) { if ( @@ -311,14 +316,14 @@ App::init() throw new Exception(Exception::GENERAL_SERVICE_DISABLED); } } - if (!\in_array($scope, $scopes)) { - if ($project->isEmpty()) { // Check if permission is denied because project is missing - throw new Exception(Exception::PROJECT_NOT_FOUND); - } + /** Do now allow access if scope is not allowed */ + $scope = $route->getLabel('scope', 'none'); + if (!\in_array($scope, $scopes)) { throw new Exception(Exception::GENERAL_UNAUTHORIZED_SCOPE, $user->getAttribute('email', 'User') . ' (role: ' . \strtolower($roles[$role]['label']) . ') missing scope (' . $scope . ')'); } + /** Do not allow access to blocked accounts */ if (false === $user->getAttribute('status')) { // Account is blocked throw new Exception(Exception::USER_BLOCKED); } diff --git a/app/init.php b/app/init.php index f94c9665e7..df75e5b3e0 100644 --- a/app/init.php +++ b/app/init.php @@ -41,6 +41,7 @@ use Appwrite\Network\Validator\Email; use Appwrite\Network\Validator\Origin; use Appwrite\OpenSSL\OpenSSL; use Appwrite\URL\URL as AppwriteURL; +use Appwrite\Utopia\Request; use MaxMind\Db\Reader; use PHPMailer\PHPMailer\PHPMailer; use Swoole\Database\PDOProxy; @@ -1258,13 +1259,13 @@ App::setResource('user', function ($mode, $project, $console, $request, $respons $user = new Document([]); } - if (APP_MODE_ADMIN === $mode) { - if ($user->find('teamInternalId', $project->getAttribute('teamInternalId'), 'memberships')) { - Authorization::setDefaultStatus(false); // Cancel security segmentation for admin users. - } else { - $user = new Document([]); - } - } + // if (APP_MODE_ADMIN === $mode) { + // if ($user->find('teamInternalId', $project->getAttribute('teamInternalId'), 'memberships')) { + // Authorization::setDefaultStatus(false); // Cancel security segmentation for admin users. + // } else { + // $user = new Document([]); + // } + // } $authJWT = $request->getHeader('x-appwrite-jwt', ''); @@ -1341,7 +1342,7 @@ App::setResource('console', function () { '$collection' => ID::custom('projects'), 'description' => 'Appwrite core engine', 'logo' => '', - 'teamId' => -1, + 'teamId' => null, 'webhooks' => [], 'keys' => [], 'platforms' => [ @@ -1769,3 +1770,35 @@ App::setResource('requestTimestamp', function ($request) { App::setResource('plan', function (array $plan = []) { return []; }); + + +App::setResource('team', function (Document $project, Database $dbForConsole, App $utopia, Request $request) { + $teamInternalId = ''; + if ($project->getId() !== 'console') { + $teamInternalId = $project->getAttribute('teamInternalId', ''); + } else { + $route = $utopia->match($request); + $path = $route->getPath(); + if (str_starts_with($path, '/v1/projects/:projectId')) { + $uri = $request->getURI(); + $pid = explode('/', $uri)[3]; + $p = Authorization::skip(fn () => $dbForConsole->getDocument('projects', $pid)); + $teamInternalId = $p->getAttribute('teamInternalId', ''); + } elseif ($path === '/v1/projects') { + $teamId = $request->getParam('teamId', ''); + $team = Authorization::skip(fn () => $dbForConsole->getDocument('teams', $teamId)); + return $team; + } + } + + $team = Authorization::skip(function () use ($dbForConsole, $teamInternalId) { + return $dbForConsole->findOne('teams', [ + Query::equal('$internalId', [$teamInternalId]), + ]); + }); + + if (!$team) { + $team = new Document([]); + } + return $team; +}, ['project', 'dbForConsole', 'utopia', 'request']); diff --git a/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php b/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php index c41d861f1d..05893be3a8 100644 --- a/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php +++ b/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php @@ -101,7 +101,7 @@ class ProjectsConsoleClientTest extends Scope 'region' => 'default' ]); - $this->assertEquals(400, $response['headers']['status-code']); + $this->assertEquals(401, $response['headers']['status-code']); return [ 'projectId' => $projectId, @@ -546,7 +546,7 @@ class ProjectsConsoleClientTest extends Scope 'name' => '', ]); - $this->assertEquals(400, $response['headers']['status-code']); + $this->assertEquals(401, $response['headers']['status-code']); return ['projectId' => $projectId]; } diff --git a/tests/e2e/Services/Teams/TeamsBaseClient.php b/tests/e2e/Services/Teams/TeamsBaseClient.php index 03dffac6aa..8a1fed028e 100644 --- a/tests/e2e/Services/Teams/TeamsBaseClient.php +++ b/tests/e2e/Services/Teams/TeamsBaseClient.php @@ -161,7 +161,7 @@ trait TeamsBaseClient $this->assertNotEmpty($response['body']['userEmail']); $this->assertNotEmpty($response['body']['teamId']); $this->assertNotEmpty($response['body']['teamName']); - $this->assertCount(2, $response['body']['roles']); + $this->assertCount(1, $response['body']['roles']); $this->assertEquals(false, (new DatetimeValidator())->isValid($response['body']['joined'])); // is null in DB $this->assertEquals(false, $response['body']['confirm']); @@ -203,7 +203,7 @@ trait TeamsBaseClient ], $this->getHeaders()), [ 'email' => $email, 'name' => $name, - 'roles' => ['admin', 'editor'], + 'roles' => ['developer'], 'url' => 'http://localhost:5000/join-us#title' ]); @@ -214,7 +214,7 @@ trait TeamsBaseClient $this->assertEquals($email, $response['body']['userEmail']); $this->assertNotEmpty($response['body']['teamId']); $this->assertNotEmpty($response['body']['teamName']); - $this->assertCount(2, $response['body']['roles']); + $this->assertCount(1, $response['body']['roles']); $this->assertEquals(false, (new DatetimeValidator())->isValid($response['body']['joined'])); // is null in DB $this->assertEquals(false, $response['body']['confirm']); @@ -255,7 +255,7 @@ trait TeamsBaseClient 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ 'userId' => 'abcdefdg', - 'roles' => ['admin', 'editor'], + 'roles' => ['developer'], 'url' => 'http://localhost:5000/join-us#title' ]); @@ -270,7 +270,7 @@ trait TeamsBaseClient 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ 'userId' => $userId, - 'roles' => ['admin', 'editor'], + 'roles' => ['developer'], 'url' => 'http://localhost:5000/join-us#title' ]); @@ -281,7 +281,7 @@ trait TeamsBaseClient $this->assertEquals($secondEmail, $response['body']['userEmail']); $this->assertNotEmpty($response['body']['teamId']); $this->assertNotEmpty($response['body']['teamName']); - $this->assertCount(2, $response['body']['roles']); + $this->assertCount(1, $response['body']['roles']); $this->assertEquals(false, (new DateTimeValidator())->isValid($response['body']['joined'])); // is null in DB $this->assertEquals(false, $response['body']['confirm']); @@ -301,7 +301,7 @@ trait TeamsBaseClient ], $this->getHeaders()), [ 'email' => $email, 'name' => 'Friend User', - 'roles' => ['admin', 'editor'], + 'roles' => ['developer'], 'url' => 'http://localhost:5000/join-us#title' ]); @@ -313,7 +313,7 @@ trait TeamsBaseClient ], $this->getHeaders()), [ 'email' => 'dasdkaskdjaskdjasjkd', 'name' => $name, - 'roles' => ['admin', 'editor'], + 'roles' => ['developer'], 'url' => 'http://localhost:5000/join-us#title' ]); @@ -337,7 +337,7 @@ trait TeamsBaseClient ], $this->getHeaders()), [ 'email' => $email, 'name' => $name, - 'roles' => ['admin', 'editor'], + 'roles' => ['developer'], 'url' => 'http://example.com/join-us#title' // bad url ]); @@ -413,7 +413,7 @@ trait TeamsBaseClient $this->assertNotEmpty($response['body']['$id']); $this->assertNotEmpty($response['body']['userId']); $this->assertNotEmpty($response['body']['teamId']); - $this->assertCount(2, $response['body']['roles']); + $this->assertCount(1, $response['body']['roles']); $this->assertEquals(true, (new DatetimeValidator())->isValid($response['body']['joined'])); $this->assertEquals(true, $response['body']['confirm']); $session = $response['cookies']['a_session_' . $this->getProject()['$id']]; @@ -571,7 +571,7 @@ trait TeamsBaseClient /** * Test for SUCCESS */ - $roles = ['admin', 'editor', 'uncle']; + $roles = ['editor', 'uncle']; $response = $this->client->call(Client::METHOD_PATCH, '/teams/' . $teamUid . '/memberships/' . $membershipUid, array_merge([ 'origin' => 'http://localhost', 'content-type' => 'application/json', @@ -587,7 +587,6 @@ trait TeamsBaseClient $this->assertCount(count($roles), $response['body']['roles']); $this->assertEquals($roles[0], $response['body']['roles'][0]); $this->assertEquals($roles[1], $response['body']['roles'][1]); - $this->assertEquals($roles[2], $response['body']['roles'][2]); /** * Test for unknown team diff --git a/tests/e2e/Services/Teams/TeamsConsoleClientTest.php b/tests/e2e/Services/Teams/TeamsConsoleClientTest.php index 61d0f6a027..4b5ade7cbf 100644 --- a/tests/e2e/Services/Teams/TeamsConsoleClientTest.php +++ b/tests/e2e/Services/Teams/TeamsConsoleClientTest.php @@ -28,12 +28,13 @@ class TeamsConsoleClientTest extends Scope // Create a user account before we create a invite so we can check if the user has permissions when it shouldn't $user = $this->client->call(Client::METHOD_POST, '/account', [ 'content-type' => 'application/json', - 'x-appwrite-project' => 'console'], [ - 'userId' => 'unique()', - 'email' => $email, - 'password' => $password, - 'name' => $name, - ], false); + 'x-appwrite-project' => 'console' + ], [ + 'userId' => 'unique()', + 'email' => $email, + 'password' => $password, + 'name' => $name, + ], false); $this->assertEquals(201, $user['headers']['status-code']); @@ -46,7 +47,7 @@ class TeamsConsoleClientTest extends Scope ], $this->getHeaders()), [ 'email' => $email, 'name' => $name, - 'roles' => ['admin', 'editor'], + 'roles' => ['developer'], 'url' => 'http://localhost:5000/join-us#title' ]); @@ -76,4 +77,75 @@ class TeamsConsoleClientTest extends Scope return $data; } + + /** @depends testUpdateTeamMembership */ + public function testUpdateTeamMembershipRoles($data): array + { + $teamUid = $data['teamUid'] ?? ''; + $membershipUid = $data['membershipUid'] ?? ''; + $session = $data['session'] ?? ''; + + /** + * Test for SUCCESS + */ + $roles = ['developer']; + $response = $this->client->call(Client::METHOD_PATCH, '/teams/' . $teamUid . '/memberships/' . $membershipUid, array_merge([ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'roles' => $roles + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertNotEmpty($response['body']['$id']); + $this->assertNotEmpty($response['body']['userId']); + $this->assertNotEmpty($response['body']['teamId']); + $this->assertCount(count($roles), $response['body']['roles']); + $this->assertEquals($roles[0], $response['body']['roles'][0]); + + /** + * Test for unknown team + */ + $response = $this->client->call(Client::METHOD_PATCH, '/teams/' . 'abc' . '/memberships/' . $membershipUid, array_merge([ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'roles' => $roles + ]); + + $this->assertEquals(404, $response['headers']['status-code']); + + /** + * Test for unknown membership ID + */ + $response = $this->client->call(Client::METHOD_PATCH, '/teams/' . $teamUid . '/memberships/' . 'abc', array_merge([ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'roles' => $roles + ]); + + $this->assertEquals(404, $response['headers']['status-code']); + + + /** + * Test for when a user other than the owner tries to update membership + */ + $response = $this->client->call(Client::METHOD_PATCH, '/teams/' . $teamUid . '/memberships/' . $membershipUid, [ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $session, + ], [ + 'roles' => $roles + ]); + + $this->assertEquals(401, $response['headers']['status-code']); + $this->assertEquals('User is not allowed to modify roles', $response['body']['message']); + + return $data; + } }