From c9b2d6eb902aa6146a168a1bc97fdbf6472c7bb3 Mon Sep 17 00:00:00 2001 From: loks0n <22452787+loks0n@users.noreply.github.com> Date: Fri, 1 Nov 2024 11:38:56 +0000 Subject: [PATCH] feat: teamHideSensitiveFields --- app/controllers/api/projects.php | 32 ++++++++++ app/controllers/api/teams.php | 38 ++++++------ .../Utopia/Response/Model/Project.php | 7 +++ tests/e2e/Services/Teams/TeamsBaseClient.php | 61 +++++++++++++++++++ 4 files changed, 121 insertions(+), 17 deletions(-) diff --git a/app/controllers/api/projects.php b/app/controllers/api/projects.php index 5705e7576b..2efee6543e 100644 --- a/app/controllers/api/projects.php +++ b/app/controllers/api/projects.php @@ -111,6 +111,7 @@ App::post('/v1/projects') 'personalDataCheck' => false, 'mockNumbers' => [], 'sessionAlerts' => false, + 'teamHideSensitiveFields' => false, ]; foreach ($auth as $method) { @@ -648,6 +649,37 @@ App::patch('/v1/projects/:projectId/auth/session-alerts') $response->dynamic($project, Response::MODEL_PROJECT); }); +App::patch('/v1/projects/:projectId/auth/session-alerts') + ->desc('Update project team hide sensitive fields') + ->groups(['api', 'projects']) + ->label('scope', 'projects.write') + ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN]) + ->label('sdk.namespace', 'projects') + ->label('sdk.method', 'updateTeamHideSensitiveFields') + ->label('sdk.response.code', Response::STATUS_CODE_OK) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_PROJECT) + ->param('projectId', '', new UID(), 'Project unique ID.') + ->param('teamHideSensitiveFields', false, new Boolean(true), 'Set to true to hide sensitive fields from team members.') + ->inject('response') + ->inject('dbForConsole') + ->action(function (string $projectId, bool $alerts, Response $response, Database $dbForConsole) { + + $project = $dbForConsole->getDocument('projects', $projectId); + + if ($project->isEmpty()) { + throw new Exception(Exception::PROJECT_NOT_FOUND); + } + + $auths = $project->getAttribute('auths', []); + $auths['teamShowSensitiveFields'] = $alerts; + + $dbForConsole->updateDocument('projects', $project->getId(), $project + ->setAttribute('auths', $auths)); + + $response->dynamic($project, Response::MODEL_PROJECT); + }); + App::patch('/v1/projects/:projectId/auth/limit') ->desc('Update project users limit') ->groups(['api', 'projects']) diff --git a/app/controllers/api/teams.php b/app/controllers/api/teams.php index 0b97be0e55..776a2b0046 100644 --- a/app/controllers/api/teams.php +++ b/app/controllers/api/teams.php @@ -727,8 +727,9 @@ App::get('/v1/teams/:teamId/memberships') ->param('queries', [], new Memberships(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Memberships::ALLOWED_ATTRIBUTES), true) ->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true) ->inject('response') + ->inject('project') ->inject('dbForProject') - ->action(function (string $teamId, array $queries, string $search, Response $response, Database $dbForProject) { + ->action(function (string $teamId, array $queries, string $search, Response $response, Document $project, Database $dbForProject) { $team = $dbForProject->getDocument('teams', $teamId); @@ -790,27 +791,30 @@ App::get('/v1/teams/:teamId/memberships') $memberships = array_filter($memberships, fn (Document $membership) => !empty($membership->getAttribute('userId'))); - $memberships = array_map(function ($membership) use ($dbForProject, $team) { + $hideSensitiveFields = $project->getAttribute('auths', [])['teamHideSensitiveFields'] ?? false; + + $memberships = array_map(function ($membership) use ($dbForProject, $team, $hideSensitiveFields) { $user = $dbForProject->getDocument('users', $membership->getAttribute('userId')); + $membership->setAttribute('teamName', $team->getAttribute('name')); - $mfa = $user->getAttribute('mfa', false); - if ($mfa) { - $totp = TOTP::getAuthenticatorFromUser($user); - $totpEnabled = $totp && $totp->getAttribute('verified', false); - $emailEnabled = $user->getAttribute('email', false) && $user->getAttribute('emailVerification', false); - $phoneEnabled = $user->getAttribute('phone', false) && $user->getAttribute('phoneVerification', false); + if (!$hideSensitiveFields) { + $mfa = $user->getAttribute('mfa', false); + if ($mfa) { + $totp = TOTP::getAuthenticatorFromUser($user); + $totpEnabled = $totp && $totp->getAttribute('verified', false); + $emailEnabled = $user->getAttribute('email', false) && $user->getAttribute('emailVerification', false); + $phoneEnabled = $user->getAttribute('phone', false) && $user->getAttribute('phoneVerification', false); - if (!$totpEnabled && !$emailEnabled && !$phoneEnabled) { - $mfa = false; + if (!$totpEnabled && !$emailEnabled && !$phoneEnabled) { + $mfa = false; + } } - } - $membership - ->setAttribute('mfa', $mfa) - ->setAttribute('teamName', $team->getAttribute('name')) - ->setAttribute('userName', $user->getAttribute('name')) - ->setAttribute('userEmail', $user->getAttribute('email')) - ; + $membership + ->setAttribute('mfa', $mfa) + ->setAttribute('userName', $user->getAttribute('name')) + ->setAttribute('userEmail', $user->getAttribute('email')); + } return $membership; }, $memberships); diff --git a/src/Appwrite/Utopia/Response/Model/Project.php b/src/Appwrite/Utopia/Response/Model/Project.php index e1d0105587..98a6ef52b7 100644 --- a/src/Appwrite/Utopia/Response/Model/Project.php +++ b/src/Appwrite/Utopia/Response/Model/Project.php @@ -151,6 +151,12 @@ class Project extends Model 'default' => false, 'example' => true, ]) + ->addRule('teamHideSensitiveFields', [ + 'type' => self::TYPE_BOOLEAN, + 'description' => 'Whether or not to hide sensitive data in the teams API.', + 'default' => false, + 'example' => true, + ]) ->addRule('oAuthProviders', [ 'type' => Response::MODEL_AUTH_PROVIDER, 'description' => 'List of Auth Providers.', @@ -348,6 +354,7 @@ class Project extends Model $document->setAttribute('authPersonalDataCheck', $authValues['personalDataCheck'] ?? false); $document->setAttribute('authMockNumbers', $authValues['mockNumbers'] ?? []); $document->setAttribute('authSessionAlerts', $authValues['sessionAlerts'] ?? false); + $document->setAttribute('authTeamHideSensitiveFields', $authValues['teamHideSensitiveFields'] ?? false); foreach ($auth as $index => $method) { $key = $method['key']; diff --git a/tests/e2e/Services/Teams/TeamsBaseClient.php b/tests/e2e/Services/Teams/TeamsBaseClient.php index 8a1fed028e..5993acb639 100644 --- a/tests/e2e/Services/Teams/TeamsBaseClient.php +++ b/tests/e2e/Services/Teams/TeamsBaseClient.php @@ -130,6 +130,67 @@ trait TeamsBaseClient $this->assertEmpty($response['body']['memberships']); $this->assertEquals(0, $response['body']['total']); + /** + * Update project settings to hide sensitive fields + */ + $projectId = $this->getProject()['$id']; + $response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $projectId . '/auth/session-alerts', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => 'console', + 'x-appwrite-key' => $this->getRoot()['secret'], + ]), [ + 'teamHideSensitiveFields' => true, + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + + /** + * Test that sensitive fields are hidden + */ + $response = $this->client->call(Client::METHOD_GET, '/teams/' . $teamUid . '/memberships', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $projectId, + ], $this->getHeaders())); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertIsInt($response['body']['total']); + $this->assertNotEmpty($response['body']['memberships'][0]['$id']); + + // Assert that sensitive fields are not present + $this->assertArrayNotHasKey('userName', $response['body']['memberships'][0]); + $this->assertArrayNotHasKey('userEmail', $response['body']['memberships'][0]); + $this->assertArrayNotHasKey('mfa', $response['body']['memberships'][0]); + + /** + * Update project settings to show sensitive fields + */ + $response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $projectId . '/auth/session-alerts', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => 'console', + 'x-appwrite-key' => $this->getRoot()['secret'], + ]), [ + 'teamHideSensitiveFields' => false, + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + + /** + * Test that sensitive fields are shown + */ + $response = $this->client->call(Client::METHOD_GET, '/teams/' . $teamUid . '/memberships', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $projectId, + ], $this->getHeaders())); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertIsInt($response['body']['total']); + $this->assertNotEmpty($response['body']['memberships'][0]['$id']); + + // Assert that sensitive fields are present + $this->assertEquals($this->getUser()['name'], $response['body']['memberships'][0]['userName']); + $this->assertEquals($this->getUser()['email'], $response['body']['memberships'][0]['userEmail']); + $this->assertFalse($response['body']['memberships'][0]['mfa']); + /** * Test for FAILURE */