From dcc38491b59e460b611c3dca470ff6a4c932f143 Mon Sep 17 00:00:00 2001 From: Noah Paige <69586985+noah-paige@users.noreply.github.com> Date: Mon, 23 Sep 2024 12:19:11 -0700 Subject: [PATCH] Feat/integration tests dashboards (#1560) ### Feature or Bugfix - Feature ### Detail - Add integration tests for dashboards - Remove `shareDashboard` unused API - Small update to notebook tests to now use `set_env_params` util ### Relates - https://github.com/data-dot-all/dataall/issues/1546 ### Note For dashboard tests to run successfully the follow pre-requisite work must be complete: - QS Enterprise Account created in `session_env1` with Capacity Pricing - QS Dashboard created with `dataall` QS Group having `Owner` permissions - And `dashboardId` added to the `testdata.json` as shown in `README` ### 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. --- .../modules/dashboards/api/mutations.py | 12 -- .../modules/dashboards/api/resolvers.py | 9 -- .../services/dashboard_share_service.py | 18 --- tests/modules/dashboards/test_dashboards.py | 34 ---- tests_new/integration_tests/README.md | 9 ++ tests_new/integration_tests/conftest.py | 7 + .../core/environment/queries.py | 6 +- .../modules/dashboards/conftest.py | 47 ++++++ .../modules/dashboards/mutations.py | 108 +++++++++++++ .../modules/dashboards/queries.py | 147 ++++++++++++++++++ .../modules/dashboards/test_dashboard.py | 115 ++++++++++++++ .../modules/notebooks/conftest.py | 3 + 12 files changed, 441 insertions(+), 74 deletions(-) create mode 100644 tests_new/integration_tests/modules/dashboards/conftest.py create mode 100644 tests_new/integration_tests/modules/dashboards/mutations.py create mode 100644 tests_new/integration_tests/modules/dashboards/queries.py create mode 100644 tests_new/integration_tests/modules/dashboards/test_dashboard.py diff --git a/backend/dataall/modules/dashboards/api/mutations.py b/backend/dataall/modules/dashboards/api/mutations.py index 93a6e0a02..b6deeebba 100644 --- a/backend/dataall/modules/dashboards/api/mutations.py +++ b/backend/dataall/modules/dashboards/api/mutations.py @@ -6,7 +6,6 @@ import_dashboard, reject_dashboard_share, request_dashboard_share, - share_dashboard, update_dashboard, ) @@ -35,17 +34,6 @@ resolver=delete_dashboard, ) - -shareDashboard = gql.MutationField( - name='shareDashboard', - type=gql.Ref('DashboardShare'), - args=[ - gql.Argument(name='principalId', type=gql.NonNullableType(gql.String)), - gql.Argument(name='dashboardUri', type=gql.NonNullableType(gql.String)), - ], - resolver=share_dashboard, -) - requestDashboardShare = gql.MutationField( name='requestDashboardShare', type=gql.Ref('DashboardShare'), diff --git a/backend/dataall/modules/dashboards/api/resolvers.py b/backend/dataall/modules/dashboards/api/resolvers.py index 86d0fb671..67ae59169 100644 --- a/backend/dataall/modules/dashboards/api/resolvers.py +++ b/backend/dataall/modules/dashboards/api/resolvers.py @@ -89,15 +89,6 @@ def list_dashboard_shares( return DashboardShareService.list_dashboard_shares(uri=dashboardUri, data=filter) -def share_dashboard( - context: Context, - source: Dashboard, - principalId: str = None, - dashboardUri: str = None, -): - return DashboardShareService.share_dashboard(uri=dashboardUri, principal_id=principalId) - - def delete_dashboard(context: Context, source, dashboardUri: str = None): return DashboardService.delete_dashboard(uri=dashboardUri) diff --git a/backend/dataall/modules/dashboards/services/dashboard_share_service.py b/backend/dataall/modules/dashboards/services/dashboard_share_service.py index 82edb0a44..aa344f87f 100644 --- a/backend/dataall/modules/dashboards/services/dashboard_share_service.py +++ b/backend/dataall/modules/dashboards/services/dashboard_share_service.py @@ -81,24 +81,6 @@ def list_dashboard_shares(uri: str, data: dict): data=data, ) - @staticmethod - @TenantPolicyService.has_tenant_permission(MANAGE_DASHBOARDS) - @ResourcePolicyService.has_resource_permission(SHARE_DASHBOARD) - def share_dashboard(uri: str, principal_id: str): - context = get_context() - with context.db_engine.scoped_session() as session: - dashboard = DashboardRepository.get_dashboard_by_uri(session, uri) - share = DashboardRepository.create_share( - session=session, - username=context.username, - dashboard=dashboard, - principal_id=principal_id, - init_status=DashboardShareStatus.APPROVED, - ) - - DashboardShareService._create_share_policy(session, principal_id, dashboard.dashboardUri) - return share - @staticmethod def _change_share_status(share, status): DashboardShareService._check_share_status(share) diff --git a/tests/modules/dashboards/test_dashboards.py b/tests/modules/dashboards/test_dashboards.py index 6927c62df..8ab48707f 100644 --- a/tests/modules/dashboards/test_dashboards.py +++ b/tests/modules/dashboards/test_dashboards.py @@ -263,40 +263,6 @@ def test_request_dashboard_share( ) assert len(response.data.searchDashboards['nodes']) == 0 - response = client.query( - """ - mutation shareDashboard($dashboardUri:String!, $principalId:String!){ - shareDashboard(dashboardUri:$dashboardUri, principalId:$principalId){ - shareUri - status - } - } - """, - dashboardUri=dashboard.dashboardUri, - principalId=group2.name, - username=user.username, - groups=[group.name], - ) - assert response.data.shareDashboard.shareUri - - response = client.query( - """ - query searchDashboards($filter:DashboardFilter!){ - searchDashboards(filter:$filter){ - count - nodes{ - dashboardUri - userRoleForDashboard - } - } - } - """, - filter={}, - username=user2.username, - groups=[group2.name], - ) - assert len(response.data.searchDashboards['nodes']) == 1 - def test_delete_dashboard(client, env_fixture, db, user, group, module_mocker, dashboard, patch_es): response = client.query( diff --git a/tests_new/integration_tests/README.md b/tests_new/integration_tests/README.md index fe76e45e8..d2458b2cd 100644 --- a/tests_new/integration_tests/README.md +++ b/tests_new/integration_tests/README.md @@ -54,6 +54,10 @@ Currently **we support only Cognito based deployments** but support for any IdP } }, "envs": { + "persistent_env1": { + "accountId": "...", + "region": "us-east-1" + }, "session_env1": { "accountId": "...", "region": "eu-central-1" @@ -62,6 +66,11 @@ Currently **we support only Cognito based deployments** but support for any IdP "accountId": "...", "region": "eu-west-1" } + }, + "dashboards": { + "session_env1": { + "dashboardId": "..." + }, } } ``` diff --git a/tests_new/integration_tests/conftest.py b/tests_new/integration_tests/conftest.py index 2ca8f5f5f..417a09936 100644 --- a/tests_new/integration_tests/conftest.py +++ b/tests_new/integration_tests/conftest.py @@ -33,11 +33,18 @@ class Env: region: str +@dataclass_json +@dataclass +class Dashboard: + dashboardId: str + + @dataclass_json @dataclass class TestData: users: dict[str, User] envs: dict[str, Env] + dashboards: dict[str, Dashboard] @pytest.fixture(scope='session', autouse=True) diff --git a/tests_new/integration_tests/core/environment/queries.py b/tests_new/integration_tests/core/environment/queries.py index a965d1588..9469fb979 100644 --- a/tests_new/integration_tests/core/environment/queries.py +++ b/tests_new/integration_tests/core/environment/queries.py @@ -61,10 +61,14 @@ def create_environment(client, name, group, organizationUri, awsAccountId, regio 'region': region, 'description': 'Created for integration testing', 'tags': tags, + 'type': 'IntegrationTesting', 'parameters': [ {'key': 'notebooksEnabled', 'value': 'true'}, + {'key': 'dashboardsEnabled', 'value': 'true'}, + {'key': 'mlStudiosEnabled', 'value': 'false'}, + {'key': 'pipelinesEnabled', 'value': 'true'}, + {'key': 'omicsEnabled', 'value': 'true'}, ], - 'type': 'IntegrationTesting', } }, 'query': f""" diff --git a/tests_new/integration_tests/modules/dashboards/conftest.py b/tests_new/integration_tests/modules/dashboards/conftest.py new file mode 100644 index 000000000..d2f773f8a --- /dev/null +++ b/tests_new/integration_tests/modules/dashboards/conftest.py @@ -0,0 +1,47 @@ +import pytest +from integration_tests.modules.dashboards.mutations import ( + import_dashboard, + delete_dashboard, + request_dashboard_share, + reject_dashboard_share, +) +from integration_tests.modules.dashboards.queries import get_dashboard +from integration_tests.core.environment.utils import set_env_params + + +def create_dataall_dashboard(client, session_id, dashboard_id, env): + dashboard_input = { + 'label': session_id, + 'dashboardId': dashboard_id, + 'environmentUri': env.environmentUri, + 'description': 'integration test dashboard', + 'SamlGroupName': env.SamlGroupName, + 'tags': [session_id], + 'terms': [], + } + ds = import_dashboard(client, dashboard_input) + return get_dashboard(client, ds.dashboardUri) + + +@pytest.fixture(scope='session') +def dashboard1(session_id, client1, session_env1, testdata): + set_env_params(client1, session_env1, dashboardsEnabled='true') + dashboardId = testdata.dashboards['session_env1'].dashboardId + ds = None + try: + ds = create_dataall_dashboard(client1, session_id, dashboardId, session_env1) + yield ds + finally: + if ds: + delete_dashboard(client1, ds.dashboardUri) + + +@pytest.fixture(scope='function') +def dashboard1_share(client1, client2, dashboard1, group2): + share = None + try: + share = request_dashboard_share(client2, dashboard1.dashboardUri, group2) + yield share + finally: + if share: + reject_dashboard_share(client1, share.shareUri) diff --git a/tests_new/integration_tests/modules/dashboards/mutations.py b/tests_new/integration_tests/modules/dashboards/mutations.py new file mode 100644 index 000000000..eb62bf3ca --- /dev/null +++ b/tests_new/integration_tests/modules/dashboards/mutations.py @@ -0,0 +1,108 @@ +# TODO: This file will be replaced by using the SDK directly + +# # IF MONITORING ON (TODO) +# createQuicksightDataSourceSet (TODO) + + +def import_dashboard(client, input): + query = { + 'operationName': 'importDashboard', + 'variables': {'input': input}, + 'query': """ + mutation importDashboard($input: ImportDashboardInput!) { + importDashboard(input: $input) { + dashboardUri + name + label + DashboardId + created + } + } + """, + } + response = client.query(query=query) + return response.data.importDashboard + + +def update_dashboard(client, input): + query = { + 'operationName': 'updateDashboard', + 'variables': {'input': input}, + 'query': """ + mutation updateDashboard($input: UpdateDashboardInput!) { + updateDashboard(input: $input) { + dashboardUri + name + label + created + } + } + """, + } + response = client.query(query=query) + return response.data.importDashboard + + +def delete_dashboard(client, dashboardUri): + query = { + 'operationName': 'deleteDashboard', + 'variables': {'dashboardUri': dashboardUri}, + 'query': """ + mutation deleteDashboard($dashboardUri: String!) { + deleteDashboard(dashboardUri: $dashboardUri) + } + """, + } + response = client.query(query=query) + return response.data.deleteDashboard + + +def request_dashboard_share(client, dashboardUri, principalId): + query = { + 'operationName': 'requestDashboardShare', + 'variables': {'dashboardUri': dashboardUri, 'principalId': principalId}, + 'query': """ + mutation requestDashboardShare($dashboardUri: String!,$principalId: String!) { + requestDashboardShare(dashboardUri: $dashboardUri,principalId: $principalId) { + shareUri + status + } + } + """, + } + response = client.query(query=query) + return response.data.requestDashboardShare + + +def approve_dashboard_share(client, shareUri): + query = { + 'operationName': 'approveDashboardShare', + 'variables': {'shareUri': shareUri}, + 'query': """ + mutation approveDashboardShare($shareUri: String!) { + approveDashboardShare(shareUri: $shareUri) { + shareUri + status + } + } + """, + } + response = client.query(query=query) + return response.data.approveDashboardShare + + +def reject_dashboard_share(client, shareUri): + query = { + 'operationName': 'rejectDashboardShare', + 'variables': {'shareUri': shareUri}, + 'query': """ + mutation rejectDashboardShare($shareUri: String!) { + rejectDashboardShare(shareUri: $shareUri) { + shareUri + status + } + } + """, + } + response = client.query(query=query) + return response.data.rejectDashboardShare diff --git a/tests_new/integration_tests/modules/dashboards/queries.py b/tests_new/integration_tests/modules/dashboards/queries.py new file mode 100644 index 000000000..901536308 --- /dev/null +++ b/tests_new/integration_tests/modules/dashboards/queries.py @@ -0,0 +1,147 @@ +# TODO: This file will be replaced by using the SDK directly + + +# # IF MONITORING ON (TODO) +# getMonitoringDashboardId +# getMonitoringVpcConnectionId +# getPlatformAuthorSession +# getPlatformReaderSession + + +def search_dashboards(client, filter): + query = { + 'operationName': 'searchDashboards', + 'variables': {'filter': filter}, + 'query': """ + query searchDashboards($filter: DashboardFilter) { + searchDashboards(filter: $filter) { + count + page + pages + hasNext + hasPrevious + nodes { + dashboardUri + name + owner + SamlGroupName + description + label + created + tags + userRoleForDashboard + upvotes + organization { + organizationUri + label + name + } + environment { + environmentUri + name + label + AwsAccountId + region + } + } + } + } + """, + } + response = client.query(query=query) + return response.data.searchDashboards + + +def get_dashboard(client, dashboardUri): + query = { + 'operationName': 'GetDashboard', + 'variables': {'dashboardUri': dashboardUri}, + 'query': """ + query GetDashboard($dashboardUri: String!) { + getDashboard(dashboardUri: $dashboardUri) { + dashboardUri + name + owner + SamlGroupName + description + label + created + tags + userRoleForDashboard + environment { + label + region + } + organization { + organizationUri + label + name + } + terms { + count + nodes { + nodeUri + path + label + } + } + } + } + """, + } + response = client.query(query=query) + return response.data.getDashboard + + +def list_dashboard_shares(client, dashboardUri, filter): + query = { + 'operationName': 'listDashboardShares', + 'variables': {'dashboardUri': dashboardUri, 'filter': filter}, + 'query': """ + query listDashboardShares($dashboardUri: String!,$filter: DashboardShareFilter!) { + listDashboardShares(dashboardUri: $dashboardUri, filter: $filter) { + count + nodes { + dashboardUri + shareUri + SamlGroupName + owner + created + status + } + } + } + """, + } + response = client.query(query=query) + return response.data.listDashboardShares + + +def get_author_session(client, environmentUri): + query = { + 'operationName': 'GetAuthorSession', + 'variables': {'environmentUri': environmentUri}, + 'query': """ + query GetAuthorSession($environmentUri: String!) { + getAuthorSession(environmentUri: $environmentUri) + } + """, + } + response = client.query(query=query) + return response.data.getAuthorSession + + +def get_reader_session(client, dashboardUri): + query = { + 'operationName': 'GetReaderSession', + 'variables': { + 'dashboardUri': dashboardUri, + }, + 'query': """ + query GetReaderSession($dashboardUri: String!) { + getReaderSession(dashboardUri: $dashboardUri) + } + """, + } + response = client.query(query=query) + return response.data.getReaderSession diff --git a/tests_new/integration_tests/modules/dashboards/test_dashboard.py b/tests_new/integration_tests/modules/dashboards/test_dashboard.py new file mode 100644 index 000000000..ee3a255e3 --- /dev/null +++ b/tests_new/integration_tests/modules/dashboards/test_dashboard.py @@ -0,0 +1,115 @@ +from assertpy import assert_that + +from integration_tests.modules.dashboards.queries import ( + search_dashboards, + get_dashboard, + list_dashboard_shares, + get_author_session, + get_reader_session, +) +from integration_tests.modules.dashboards.mutations import ( + update_dashboard, + delete_dashboard, + approve_dashboard_share, + reject_dashboard_share, +) +from integration_tests.modules.dashboards.conftest import create_dataall_dashboard +from integration_tests.core.environment.utils import set_env_params +from integration_tests.errors import GqlError + +UPDATED_DESC = 'new description' + + +def test_get_author_session(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): + 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): + assert_that(dashboard1.label).is_equal_to(session_id) + + +def test_list_dashboards(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): + assert_that(get_dashboard).raises(GqlError).when_called_with(client2, dashboard1.dashboardUri).contains( + 'UnauthorizedOperation', 'GET_DASHBOARD', dashboard1.dashboardUri + ) + + +def test_update_dashboard(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): + 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): + 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): + 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): + 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): + filter = {'term': session_id} + assert_that(search_dashboards(client2, filter).nodes).is_length(0) + ds_share = approve_dashboard_share(client1, dashboard1_share.shareUri) + assert_that(ds_share.status).is_equal_to('APPROVED') + assert_that(get_reader_session(client2, dashboard1.dashboardUri)).starts_with('https://') + assert_that(search_dashboards(client2, filter).nodes).is_length(1) + + +def test_reject_dashboard_share(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): + assert_that(get_reader_session(client1, dashboard1.dashboardUri)).starts_with('https://') + + +def test_get_reader_session_unauthorized(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): + filter = {'term': session_id} + dashboardId = testdata.dashboards['session_env1'].dashboardId + dashboard2 = create_dataall_dashboard(client1, session_id, dashboardId, session_env1) + assert_that(search_dashboards(client1, filter).nodes).is_length(2) + + delete_dashboard(client1, dashboard2.dashboardUri) + assert_that(search_dashboards(client1, filter).nodes).is_length(1) + + +def test_delete_dashboard_unauthorized(client2, dashboard1): + assert_that(delete_dashboard).raises(GqlError).when_called_with(client2, dashboard1.dashboardUri).contains( + 'UnauthorizedOperation', 'DELETE_DASHBOARD', dashboard1.dashboardUri + ) diff --git a/tests_new/integration_tests/modules/notebooks/conftest.py b/tests_new/integration_tests/modules/notebooks/conftest.py index 975f621f0..be7924fe6 100644 --- a/tests_new/integration_tests/modules/notebooks/conftest.py +++ b/tests_new/integration_tests/modules/notebooks/conftest.py @@ -10,6 +10,7 @@ list_sagemaker_notebooks, ) from integration_tests.core.stack.utils import check_stack_ready, wait_stack_delete_complete +from integration_tests.core.environment.utils import set_env_params from integration_tests.modules.notebooks.aws_clients import VpcClient @@ -62,6 +63,7 @@ def session_notebook1(client1, group1, session_env1, session_id, session_env1_aw resource_name = 'sessionnotebook1' notebook = None try: + set_env_params(client1, session_env1, notebooksEnabled='true') vpc_client = VpcClient(session=session_env1_aws_client, region=session_env1['region']) vpc_id = vpc_client.create_vpc(vpc_name=resource_name, cidr='172.31.0.0/26') subnet_id = vpc_client.create_subnet(vpc_id=vpc_id, subnet_name=resource_name, cidr='172.31.0.0/28') @@ -162,6 +164,7 @@ def get_or_create_persistent_notebook(resource_name, client, group, env, session @pytest.fixture(scope='session') def persistent_notebook1(client1, group1, persistent_env1, persistent_env1_aws_client): + set_env_params(client1, persistent_env1, notebooksEnabled='true') return get_or_create_persistent_notebook( 'persistent_notebook1', client1, group1, persistent_env1, persistent_env1_aws_client )