From 1a0c4b59eb5092c168fd58346c7b522040ee1084 Mon Sep 17 00:00:00 2001 From: Noah Paige <69586985+noah-paige@users.noreply.github.com> Date: Thu, 10 Oct 2024 14:25:47 -0400 Subject: [PATCH] Dashboard Integration Test Improvements (#1623) ### Feature or Bugfix - Test Enhancement ### Detail - Add documentation in README on how to set up dashboard tests - Add check for QS Account and skip if no Account exists in `session_env1` ### Relates ### Security Please answer the questions below briefly where applicable, or write `N/A`. Based on [OWASP 10](https://owasp.org/Top10/en/). - Does this PR introduce or modify any input fields or queries - this includes fetching data from storage outside the application (e.g. a database, an S3 bucket)? - Is the input sanitized? - What precautions are you taking before deserializing the data you consume? - Is injection prevented by parametrizing queries? - Have you ensured no `eval` or similar functions are used? - Does this PR introduce any functionality or component that requires authorization? - How have you ensured it respects the existing AuthN/AuthZ mechanisms? - Are you logging failed auth attempts? - Are you using or adding any cryptographic features? - Do you use a standard proven implementations? - Are the used keys controlled by the customer? Where are they stored? - Are you introducing any new policies/roles/users? - Have you used the least-privilege principle? How? By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license. --- .../core/environment/cdk/environment_stack.py | 10 +++++ tests_new/integration_tests/README.md | 12 ++++++ .../modules/dashboards/aws_clients.py | 37 +++++++++++++++++++ .../modules/dashboards/conftest.py | 9 +++++ .../modules/dashboards/test_dashboard.py | 32 ++++++++-------- 5 files changed, 84 insertions(+), 16 deletions(-) create mode 100644 tests_new/integration_tests/modules/dashboards/aws_clients.py diff --git a/backend/dataall/core/environment/cdk/environment_stack.py b/backend/dataall/core/environment/cdk/environment_stack.py index 51d5feac5..c978f944c 100644 --- a/backend/dataall/core/environment/cdk/environment_stack.py +++ b/backend/dataall/core/environment/cdk/environment_stack.py @@ -678,3 +678,13 @@ def create_integration_tests_role(self): resources=[f'arn:aws:iam::{self.account}:role/dataall-test-*'], ), ) + + self.test_role.add_to_policy( + iam.PolicyStatement( + actions=[ + 'quicksight:DescribeAccountSubscription', + ], + effect=iam.Effect.ALLOW, + resources=[f'arn:aws:quicksight:*:{self.account}:*'], + ), + ) diff --git a/tests_new/integration_tests/README.md b/tests_new/integration_tests/README.md index 157508dd8..6e21b25b5 100644 --- a/tests_new/integration_tests/README.md +++ b/tests_new/integration_tests/README.md @@ -83,6 +83,18 @@ Currently **we support only Cognito based deployments** but support for any IdP - The pipeline will create the users/groups +### Dashboard Tests Pre-Requisities + +In order to run the tests on the dashboards module the following steps are required: + +- Create Enterprise QuickSight Subscription in `session_env1` AWS Account +- Update QuickSight Account with a Reader Capacity Pricing Plan (required for generating embed URLs - `GenerateEmbedUrlForAnonymousUser`) +- Create / Publish a QuickSight Dashboard +- Create a QuickSight Group named `dataall` and give owner access of the published dashboard to the `dataall` group +- Provide the `dashboardId` in the `config.json` as shown above + +Rather than failing if the above pre-requisites are not completed, if ther eis no QuickSight Account is detected in `session_env1` the dashboard tests will be **skipped**. + ## Run tests The tests are executed in CodeBuild as part of the CICD pipeline if the cdk.json parameter `with_approval_tests` is set diff --git a/tests_new/integration_tests/modules/dashboards/aws_clients.py b/tests_new/integration_tests/modules/dashboards/aws_clients.py new file mode 100644 index 000000000..286c9e82e --- /dev/null +++ b/tests_new/integration_tests/modules/dashboards/aws_clients.py @@ -0,0 +1,37 @@ +import logging + +log = logging.getLogger(__name__) + + +class QuickSightClient: + def __init__(self, session, account_id, region): + self._client = session.client('quicksight', region_name=region) + self._region = region + self._account_id = account_id + + def check_enterprise_account_exists(self): + """ + Check if a QuickSight Account exists in the account. + :param + :return: True if the account exists, False otherwise + """ + try: + response = self._client.describe_account_subscription(AwsAccountId=self._account_id) + if not response['AccountInfo']: + log.info(f'Quicksight Enterprise Subscription not found in Account: {self._account_id}') + return False + else: + if response['AccountInfo']['Edition'] not in ['ENTERPRISE', 'ENTERPRISE_AND_Q']: + log.info( + f"Quicksight Subscription found in Account: {self._account_id} of incorrect type: {response['AccountInfo']['Edition']}" + ) + return False + else: + if response['AccountInfo']['AccountSubscriptionStatus'] == 'ACCOUNT_CREATED': + return True + log.info( + f"Quicksight Subscription found in Account: {self._account_id} not active. Status = {response['AccountInfo']['AccountSubscriptionStatus']}" + ) + return False + except self._client.exceptions.ResourceNotFoundException: + return False diff --git a/tests_new/integration_tests/modules/dashboards/conftest.py b/tests_new/integration_tests/modules/dashboards/conftest.py index d2f773f8a..3cca4cdc5 100644 --- a/tests_new/integration_tests/modules/dashboards/conftest.py +++ b/tests_new/integration_tests/modules/dashboards/conftest.py @@ -7,6 +7,15 @@ ) from integration_tests.modules.dashboards.queries import get_dashboard from integration_tests.core.environment.utils import set_env_params +from integration_tests.modules.dashboards.aws_clients import QuickSightClient + + +@pytest.fixture(scope='session') +def quicksight_account_exists(session_env1, session_env1_aws_client): + if not QuickSightClient( + session_env1_aws_client, session_env1.AwsAccountId, session_env1.region + ).check_enterprise_account_exists(): + pytest.skip('Skipping QuickSight tests because QuickSight account does not exist') def create_dataall_dashboard(client, session_id, dashboard_id, env): diff --git a/tests_new/integration_tests/modules/dashboards/test_dashboard.py b/tests_new/integration_tests/modules/dashboards/test_dashboard.py index ee3a255e3..f453cb28b 100644 --- a/tests_new/integration_tests/modules/dashboards/test_dashboard.py +++ b/tests_new/integration_tests/modules/dashboards/test_dashboard.py @@ -20,61 +20,61 @@ UPDATED_DESC = 'new description' -def test_get_author_session(client1, session_env1): +def test_get_author_session(quicksight_account_exists, client1, session_env1): set_env_params(client1, session_env1, dashboardsEnabled='true') assert_that(get_author_session(client1, session_env1.environmentUri)).starts_with('https://') -def test_get_author_session_unauthorized(client2, session_env1): +def test_get_author_session_unauthorized(quicksight_account_exists, client2, session_env1): assert_that(get_author_session).raises(GqlError).when_called_with(client2, session_env1.environmentUri).contains( 'UnauthorizedOperation', 'CREATE_DASHBOARD', session_env1.environmentUri ) -def test_get_dashboard(session_id, dashboard1): +def test_get_dashboard(quicksight_account_exists, session_id, dashboard1): assert_that(dashboard1.label).is_equal_to(session_id) -def test_list_dashboards(client1, client2, session_id, dashboard1): +def test_list_dashboards(quicksight_account_exists, client1, client2, session_id, dashboard1): filter = {'term': session_id} assert_that(search_dashboards(client1, filter).nodes).is_length(1) assert_that(search_dashboards(client2, filter).nodes).is_length(0) -def test_get_dashboard_unauthorized(client2, dashboard1): +def test_get_dashboard_unauthorized(quicksight_account_exists, client2, dashboard1): assert_that(get_dashboard).raises(GqlError).when_called_with(client2, dashboard1.dashboardUri).contains( 'UnauthorizedOperation', 'GET_DASHBOARD', dashboard1.dashboardUri ) -def test_update_dashboard(client1, dashboard1): +def test_update_dashboard(quicksight_account_exists, client1, dashboard1): update_dashboard(client1, {'dashboardUri': dashboard1.dashboardUri, 'description': UPDATED_DESC}) ds = get_dashboard(client1, dashboard1.dashboardUri) assert_that(ds.description).is_equal_to(UPDATED_DESC) -def test_update_dashboard_unauthorized(client2, dashboard1): +def test_update_dashboard_unauthorized(quicksight_account_exists, client2, dashboard1): assert_that(update_dashboard).raises(GqlError).when_called_with( client2, {'dashboardUri': dashboard1.dashboardUri, 'description': UPDATED_DESC} ).contains('UnauthorizedOperation', 'UPDATE_DASHBOARD', dashboard1.dashboardUri) -def test_request_dashboard_share(dashboard1_share): +def test_request_dashboard_share(quicksight_account_exists, dashboard1_share): assert_that(dashboard1_share.shareUri).is_not_none() assert_that(dashboard1_share.status).is_equal_to('REQUESTED') -def test_list_dashboard_shares(client1, session_id, dashboard1, dashboard1_share): +def test_list_dashboard_shares(quicksight_account_exists, client1, session_id, dashboard1, dashboard1_share): assert_that(list_dashboard_shares(client1, dashboard1.dashboardUri, {'term': session_id}).nodes).is_length(1) -def test_approve_dashboard_share_unauthorized(client2, dashboard1, dashboard1_share): +def test_approve_dashboard_share_unauthorized(quicksight_account_exists, client2, dashboard1, dashboard1_share): assert_that(approve_dashboard_share).raises(GqlError).when_called_with(client2, dashboard1_share.shareUri).contains( 'UnauthorizedOperation', 'SHARE_DASHBOARD', dashboard1.dashboardUri ) -def test_approve_dashboard_share(client1, client2, session_id, dashboard1, dashboard1_share): +def test_approve_dashboard_share(quicksight_account_exists, client1, client2, session_id, dashboard1, dashboard1_share): filter = {'term': session_id} assert_that(search_dashboards(client2, filter).nodes).is_length(0) ds_share = approve_dashboard_share(client1, dashboard1_share.shareUri) @@ -83,23 +83,23 @@ def test_approve_dashboard_share(client1, client2, session_id, dashboard1, dashb assert_that(search_dashboards(client2, filter).nodes).is_length(1) -def test_reject_dashboard_share(client1, client2, session_id, dashboard1_share): +def test_reject_dashboard_share(quicksight_account_exists, client1, client2, session_id, dashboard1_share): ds_share = reject_dashboard_share(client1, dashboard1_share.shareUri) assert_that(ds_share.status).is_equal_to('REJECTED') assert_that(search_dashboards(client2, {'term': session_id}).nodes).is_length(0) -def test_get_reader_session(client1, dashboard1): +def test_get_reader_session(quicksight_account_exists, client1, dashboard1): assert_that(get_reader_session(client1, dashboard1.dashboardUri)).starts_with('https://') -def test_get_reader_session_unauthorized(client2, dashboard1): +def test_get_reader_session_unauthorized(quicksight_account_exists, client2, dashboard1): assert_that(get_reader_session).raises(GqlError).when_called_with(client2, dashboard1.dashboardUri).contains( 'UnauthorizedOperation', 'GET_DASHBOARD', dashboard1.dashboardUri ) -def test_delete_dashboard(client1, session_id, session_env1, testdata): +def test_delete_dashboard(quicksight_account_exists, client1, session_id, session_env1, testdata): filter = {'term': session_id} dashboardId = testdata.dashboards['session_env1'].dashboardId dashboard2 = create_dataall_dashboard(client1, session_id, dashboardId, session_env1) @@ -109,7 +109,7 @@ def test_delete_dashboard(client1, session_id, session_env1, testdata): assert_that(search_dashboards(client1, filter).nodes).is_length(1) -def test_delete_dashboard_unauthorized(client2, dashboard1): +def test_delete_dashboard_unauthorized(quicksight_account_exists, client2, dashboard1): assert_that(delete_dashboard).raises(GqlError).when_called_with(client2, dashboard1.dashboardUri).contains( 'UnauthorizedOperation', 'DELETE_DASHBOARD', dashboard1.dashboardUri )