From 7ea67895f2748018e885cdfff140b048dbe92c45 Mon Sep 17 00:00:00 2001 From: Leah Tacke genannt Unterberg <leah.tgu@pads.rwth-aachen.de> Date: Thu, 10 Apr 2025 16:58:33 +0200 Subject: [PATCH] fixed some things in the api and tasks --- justfile | 2 +- requirements/base.txt | 1 + requirements/development.txt | 12 ++-- .../hooks}/asyncExternalService.ts | 2 +- .../UploadMitMDatasetModal/index.tsx | 2 +- .../src/pages/MitMDatasetList/index.tsx | 57 ++++++++++++++++--- superset/commands/mitm/mitm_dataset/delete.py | 2 +- superset/commands/mitm/mitm_service/delete.py | 4 +- superset/config.py | 2 + superset/customization/mitm_datasets/api.py | 48 +++++++++++++--- superset/tasks/mitm/__init__.py | 4 ++ superset/tasks/mitm/call_external_service.py | 3 +- superset/tasks/mitm/download_mitm_dataset.py | 8 +-- superset/tasks/mitm/drop_mitm_dataset.py | 12 ++-- superset/tasks/mitm/upload_mitm_dataset.py | 8 +-- tests/requests/mitm_dataset_api.http | 9 +++ 16 files changed, 135 insertions(+), 41 deletions(-) rename superset-frontend/src/features/{mitmDatasets => externalServices/hooks}/asyncExternalService.ts (98%) diff --git a/justfile b/justfile index 09a32e10e4..fd99d8d871 100644 --- a/justfile +++ b/justfile @@ -1,7 +1,7 @@ set windows-shell := ["pwsh", "-c"] up: - docker compose up + docker compose up --build pyvenv: ./.venv/Scripts/python.exe -m pip install -r requirements/development.txt diff --git a/requirements/base.txt b/requirements/base.txt index 24a8442a62..5d921416e3 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -151,6 +151,7 @@ greenlet==3.1.1 # via # apache-superset (pyproject.toml) # shillelagh + # sqlalchemy gunicorn==23.0.0 # via apache-superset (pyproject.toml) h11==0.14.0 diff --git a/requirements/development.txt b/requirements/development.txt index ddf315bfa5..0802dfa87d 100644 --- a/requirements/development.txt +++ b/requirements/development.txt @@ -110,7 +110,7 @@ click-didyoumean==0.3.1 # via # -c requirements/base.txt # celery -click-option-group==0.5.6 +click-option-group==0.5.7 # via # -c requirements/base.txt # apache-superset @@ -200,7 +200,7 @@ flask==2.3.3 # flask-sqlalchemy # flask-testing # flask-wtf -flask-appbuilder==4.6.0 +flask-appbuilder==4.6.1 # via # -c requirements/base.txt # apache-superset @@ -340,7 +340,7 @@ holidays==0.25 # -c requirements/base.txt # apache-superset # prophet -humanize==4.12.1 +humanize==4.12.2 # via # -c requirements/base.txt # apache-superset @@ -439,7 +439,7 @@ marshmallow==3.26.1 # -c requirements/base.txt # flask-appbuilder # marshmallow-sqlalchemy -marshmallow-sqlalchemy==1.3.0 +marshmallow-sqlalchemy==1.4.0 # via # -c requirements/base.txt # flask-appbuilder @@ -554,7 +554,7 @@ pillow==10.4.0 # via # apache-superset # matplotlib -platformdirs==4.3.6 +platformdirs==4.3.7 # via # -c requirements/base.txt # black @@ -653,7 +653,7 @@ pyopenssl==25.0.0 # via # -c requirements/base.txt # shillelagh -pyparsing==3.2.1 +pyparsing==3.2.2 # via # -c requirements/base.txt # apache-superset diff --git a/superset-frontend/src/features/mitmDatasets/asyncExternalService.ts b/superset-frontend/src/features/externalServices/hooks/asyncExternalService.ts similarity index 98% rename from superset-frontend/src/features/mitmDatasets/asyncExternalService.ts rename to superset-frontend/src/features/externalServices/hooks/asyncExternalService.ts index f32b5ab31b..983adbf04d 100644 --- a/superset-frontend/src/features/mitmDatasets/asyncExternalService.ts +++ b/superset-frontend/src/features/externalServices/hooks/asyncExternalService.ts @@ -8,7 +8,7 @@ import { SupersetClient, SupersetError, } from '@superset-ui/core'; -import {useAsyncEventHandling} from "../../middleware/asyncEvent"; +import {useAsyncEventHandling} from "../../../middleware/asyncEvent"; const extServiceCallApiEndpoint = '/api/v1/ext_service/call'; diff --git a/superset-frontend/src/features/mitmDatasets/UploadMitMDatasetModal/index.tsx b/superset-frontend/src/features/mitmDatasets/UploadMitMDatasetModal/index.tsx index 7ef54e1c88..737a6f45fc 100644 --- a/superset-frontend/src/features/mitmDatasets/UploadMitMDatasetModal/index.tsx +++ b/superset-frontend/src/features/mitmDatasets/UploadMitMDatasetModal/index.tsx @@ -9,7 +9,7 @@ import { formStyles, } from '../../databases/UploadDataModel/styles'; import { Form, Input, Select, Upload } from 'antd-v5'; -import { AsyncActionResult, useExternalService } from '../asyncExternalService'; +import { AsyncActionResult, useExternalService } from '../../externalServices/hooks/asyncExternalService'; import { InboxOutlined } from '@ant-design/icons'; interface UploadMitMDatasetModalProps { diff --git a/superset-frontend/src/pages/MitMDatasetList/index.tsx b/superset-frontend/src/pages/MitMDatasetList/index.tsx index 75210c86c0..e78607f420 100644 --- a/superset-frontend/src/pages/MitMDatasetList/index.tsx +++ b/superset-frontend/src/pages/MitMDatasetList/index.tsx @@ -50,6 +50,7 @@ import { import { ModifiedInfo } from 'src/components/AuditInfo'; import MitMDatasetModal from '../../features/mitmDatasets/MitMDatasetModal'; import UploadMitMDatasetModal from '../../features/mitmDatasets/UploadMitMDatasetModal'; +import CopyToClipboard from '../../components/CopyToClipboard'; const Actions = styled.div` color: ${({ theme }) => theme.colors.grayscale.base}; @@ -121,6 +122,18 @@ const MitMDatasetList: FunctionComponent<MitMDatasetListProps> = ({ const [uploadModalOpen, setUploadModalOpen] = useState<boolean>(false); const [preparingExport, setPreparingExport] = useState<boolean>(false); + const [passwordFields, setPasswordFields] = useState<string[]>([]); + const [sshTunnelPasswordFields, setSSHTunnelPasswordFields] = useState< + string[] + >([]); + const [sshTunnelPrivateKeyFields, setSSHTunnelPrivateKeyFields] = useState< + string[] + >([]); + const [ + sshTunnelPrivateKeyPasswordFields, + setSSHTunnelPrivateKeyPasswordFields, + ] = useState<string[]>([]); + const openDatasetImportModal = () => { showImportModal(true); }; @@ -235,6 +248,21 @@ const MitMDatasetList: FunctionComponent<MitMDatasetListProps> = ({ accessor: 'changed_on_delta_humanized', size: 'xl', }, + { + Cell: ({ + row: { + original: { mitm_header }, + }, + }: any) => ( + <CopyToClipboard + text={JSON.stringify(mitm_header, null, 2)} + wrapped={false} + /> + ), + accessor: 'mitm_header', + hidden: true, + disableSortBy: true, + }, { Cell: ({ row: { original } }: any) => { // Verify owner or isAdmin @@ -474,12 +502,13 @@ const MitMDatasetList: FunctionComponent<MitMDatasetListProps> = ({ setDatasetCurrentlyEditing(null); }; - const handleDatasetDelete = ({ - id, - dataset_name: datasetName, - }: MitMDataset) => { + const handleDatasetDelete = ( + { id, dataset_name: datasetName }: MitMDataset, + deleteRelated: boolean = false, + ) => { + const endpoint = deleteRelated ? '/delete_with_related' : ''; SupersetClient.delete({ - endpoint: `/api/v1/mitm_dataset/${id}`, + endpoint: `/api/v1/mitm_dataset${endpoint}/${id}`, }).then( () => { refreshData(); @@ -494,9 +523,13 @@ const MitMDatasetList: FunctionComponent<MitMDatasetListProps> = ({ ); }; - const handleBulkDatasetDelete = (datasetsToDelete: MitMDataset[]) => { + const handleBulkDatasetDelete = ( + datasetsToDelete: MitMDataset[], + deleteRelated: boolean = false, + ) => { + const endpoint = deleteRelated ? '/delete_with_related' : ''; SupersetClient.delete({ - endpoint: `/api/v1/mitm_dataset/?q=${rison.encode( + endpoint: `/api/v1/mitm_dataset${endpoint}/?q=${rison.encode( datasetsToDelete.map(({ id }) => id), )}`, }).then( @@ -607,6 +640,16 @@ const MitMDatasetList: FunctionComponent<MitMDatasetListProps> = ({ onModelImport={handleDatasetImport} show={importingDataset} onHide={closeDatasetImportModal} + passwordFields={passwordFields} + setPasswordFields={setPasswordFields} + sshTunnelPasswordFields={sshTunnelPasswordFields} + setSSHTunnelPasswordFields={setSSHTunnelPasswordFields} + sshTunnelPrivateKeyFields={sshTunnelPrivateKeyFields} + setSSHTunnelPrivateKeyFields={setSSHTunnelPrivateKeyFields} + sshTunnelPrivateKeyPasswordFields={sshTunnelPrivateKeyPasswordFields} + setSSHTunnelPrivateKeyPasswordFields={ + setSSHTunnelPrivateKeyPasswordFields + } /> {preparingExport && <Loading />} diff --git a/superset/commands/mitm/mitm_dataset/delete.py b/superset/commands/mitm/mitm_dataset/delete.py index e2975a5fc8..80c01f3941 100644 --- a/superset/commands/mitm/mitm_dataset/delete.py +++ b/superset/commands/mitm/mitm_dataset/delete.py @@ -13,7 +13,7 @@ from ...chart.delete import DeleteChartCommand from ...database.delete import DeleteDatabaseCommand from ...dashboard.delete import DeleteDashboardCommand -class DeleteMitMDatasetCommand(BaseCommand): +class DeleteMitMDatasetsCommand(BaseCommand): def __init__(self, model_ids: list[int], delete_related: bool = False): self._model_ids = model_ids self._models: list[MitMDataset] | None = None diff --git a/superset/commands/mitm/mitm_service/delete.py b/superset/commands/mitm/mitm_service/delete.py index cfd78b2a3e..6033b10587 100644 --- a/superset/commands/mitm/mitm_service/delete.py +++ b/superset/commands/mitm/mitm_service/delete.py @@ -17,8 +17,8 @@ class DeleteUploadedMitMDatasetCommand(MitMDatasetBaseCommand): id = self._model.id uuid = str(self._model.uuid) - from superset.commands.mitm.mitm_dataset.delete import DeleteMitMDatasetCommand - DeleteMitMDatasetCommand([self._model.id], delete_related=True).run() + from superset.commands.mitm.mitm_dataset.delete import DeleteMitMDatasetsCommand + DeleteMitMDatasetsCommand([self._model.id], delete_related=True).run() delete_upload_request = ForwardableRequest(method='DELETE', headers=[], diff --git a/superset/config.py b/superset/config.py index ced2f56c89..f9f874ed68 100644 --- a/superset/config.py +++ b/superset/config.py @@ -1060,6 +1060,8 @@ class CeleryConfig: # pylint: disable=too-few-public-methods # }, } +if FEATURE_FLAGS.get('MITM_SUPPORT'): + CeleryConfig.imports = CeleryConfig.imports + ("superset.tasks.mitm",) CELERY_CONFIG: type[CeleryConfig] = CeleryConfig diff --git a/superset/customization/mitm_datasets/api.py b/superset/customization/mitm_datasets/api.py index 9c0aa9af8e..3d0bce9f1b 100644 --- a/superset/customization/mitm_datasets/api.py +++ b/superset/customization/mitm_datasets/api.py @@ -30,7 +30,7 @@ from ...commands.importers.exceptions import NoValidFilesFoundError from ...commands.importers.v1.utils import get_contents_from_bundle from ...commands.mitm.exceptions import * from ...commands.mitm.mitm_dataset.create import CreateMitMDatasetCommand -from ...commands.mitm.mitm_dataset.delete import DeleteMitMDatasetCommand +from ...commands.mitm.mitm_dataset.delete import DeleteMitMDatasetsCommand from ...commands.mitm.mitm_dataset.export import ExportMitMDatasetsCommand from ...commands.mitm.mitm_dataset.importers.v1 import ImportMitMDatasetsCommand from ...commands.mitm.mitm_dataset.update import UpdateMitMDatasetCommand @@ -78,6 +78,7 @@ class MitMDatasetRestApi(BaseSupersetModelRestApi): RouteMethod.RELATED, 'bulk_delete' 'delete_with_related' + 'bulk_delete_with_related' 'upload' } @@ -85,6 +86,7 @@ class MitMDatasetRestApi(BaseSupersetModelRestApi): class_permission_name = 'MitMDataset' method_permission_name = MODEL_API_RW_METHOD_PERMISSION_MAP | { 'delete_with_related': 'write', + 'bulk_delete_with_related': 'write', 'upload': 'write', 'export': 'read' } @@ -118,7 +120,7 @@ class MitMDatasetRestApi(BaseSupersetModelRestApi): 'dashboards.uuid', 'dashboards.dashboard_title', ] - show_select_columns = show_columns + ['slices.datasource_id', 'slices.datasource_type'] + show_select_columns = show_columns + ['slices.datasource_id', 'slices.datasource_type'] # this is necessary due to eager query manipuation in the slice model list_columns = [ 'id', 'dataset_name', @@ -130,6 +132,9 @@ class MitMDatasetRestApi(BaseSupersetModelRestApi): 'changed_by.last_name', 'created_by.first_name', 'created_by.last_name', + 'owners.id', + 'owners.first_name', + 'owners.last_name', 'database.database_name', 'tables.id', 'tables.table_name', @@ -162,7 +167,7 @@ class MitMDatasetRestApi(BaseSupersetModelRestApi): 'changed_by', ] - allowed_rel_fields = {'database', 'creator', 'tables', 'slices', 'dashboards', + allowed_rel_fields = {'database', 'creator', 'owners', 'tables', 'slices', 'dashboards', 'created_by', 'changed_by'} base_related_field_filters = { 'owners': [['id', BaseFilterRelatedUsers, lambda: []]], @@ -335,7 +340,7 @@ class MitMDatasetRestApi(BaseSupersetModelRestApi): ) def delete(self, pk: int) -> Response: try: - DeleteMitMDatasetCommand([pk]).run() + DeleteMitMDatasetsCommand([pk]).run() return self.response(200, message='OK') except MitMDatasetNotFoundError: return self.response_404() @@ -361,7 +366,7 @@ class MitMDatasetRestApi(BaseSupersetModelRestApi): ) def delete_with_related(self, pk: int) -> Response: try: - DeleteMitMDatasetCommand([pk], delete_related=True).run() + DeleteMitMDatasetsCommand([pk], delete_related=True).run() return self.response(200, message='OK') except MitMDatasetNotFoundError: return self.response_404() @@ -369,7 +374,7 @@ class MitMDatasetRestApi(BaseSupersetModelRestApi): return self.response_403() except MitMDatasetDeleteFailedError as ex: logger.error( - 'Error deleting model %s: %s', + 'Error deleting model with its related objects %s: %s', self.__class__.__name__, str(ex), exc_info=True, @@ -389,7 +394,7 @@ class MitMDatasetRestApi(BaseSupersetModelRestApi): def bulk_delete(self, **kwargs: Any) -> Response: item_ids = kwargs['rison'] try: - DeleteMitMDatasetCommand(item_ids).run() + DeleteMitMDatasetsCommand(item_ids).run() return self.response( 200, message=_( @@ -405,6 +410,35 @@ class MitMDatasetRestApi(BaseSupersetModelRestApi): except MitMDatasetDeleteFailedError as ex: return self.response_422(message=str(ex)) + @expose('/delete_with_related/', methods=('DELETE',)) + @protect() + @safe + @statsd_metrics + @rison(get_delete_ids_schema) + @event_logger.log_this_with_context( + action=lambda self, *args, + **kwargs: f'{self.__class__.__name__}.bulk_delete_with_related', + log_to_statsd=False, + ) + def bulk_delete_with_related(self, **kwargs: Any) -> Response: + item_ids = kwargs['rison'] + try: + DeleteMitMDatasetsCommand(item_ids, delete_related=True).run() + return self.response( + 200, + message=_( + 'Deleted %(num)d dataset with its related objects', + 'Deleted %(num)d datasets along with their related objects', + num=len(item_ids), + ), + ) + except MitMDatasetNotFoundError: + return self.response_404() + except MitMDatasetForbiddenError: + return self.response_403() + except MitMDatasetDeleteFailedError as ex: + return self.response_422(message=str(ex)) + @expose('/export/', methods=('GET',)) @protect() @safe diff --git a/superset/tasks/mitm/__init__.py b/superset/tasks/mitm/__init__.py index e69de29bb2..0e0c5a1eba 100644 --- a/superset/tasks/mitm/__init__.py +++ b/superset/tasks/mitm/__init__.py @@ -0,0 +1,4 @@ +from .call_external_service import call_external_service_task +from .download_mitm_dataset import download_mitm_dataset_task +from .drop_mitm_dataset import drop_mitm_dataset_task +from .upload_mitm_dataset import upload_mitm_dataset_task diff --git a/superset/tasks/mitm/call_external_service.py b/superset/tasks/mitm/call_external_service.py index 16e91197eb..0483eac2eb 100644 --- a/superset/tasks/mitm/call_external_service.py +++ b/superset/tasks/mitm/call_external_service.py @@ -28,6 +28,7 @@ from superset.utils.cache import generate_cache_key from superset.utils.core import override_user from .common import service_call_timeout from ..async_queries import _load_user_from_job_metadata +from superset.extensions import celery_app logger = logging.getLogger(__name__) @@ -37,7 +38,7 @@ if TYPE_CHECKING: CompleteForwardableRequest -@shared_task(name='call_external_service', +@celery_app.task(name='call_external_service', soft_time_limit=service_call_timeout, ignore_result=True, pydantic=True) diff --git a/superset/tasks/mitm/download_mitm_dataset.py b/superset/tasks/mitm/download_mitm_dataset.py index b1cd2f923a..541f4440f7 100644 --- a/superset/tasks/mitm/download_mitm_dataset.py +++ b/superset/tasks/mitm/download_mitm_dataset.py @@ -3,9 +3,9 @@ from __future__ import annotations import logging from typing import TYPE_CHECKING -from celery import shared_task from celery.exceptions import SoftTimeLimitExceeded +from superset.extensions import celery_app from superset.utils.core import override_user from .common import * from ..async_queries import _load_user_from_job_metadata @@ -19,9 +19,9 @@ if TYPE_CHECKING: from superset.customization.external_service_support.common import AsyncJobMetadata -@shared_task(name='download_mitm_dataset', - soft_time_limit=service_call_timeout, - ignore_result=True) +@celery_app.task(name='download_mitm_dataset', + soft_time_limit=service_call_timeout, + ignore_result=True) def download_mitm_dataset_task(job_metadata: AsyncJobMetadata, mitm_dataset_id: int, result_base_url: str) -> None: external_service_call_manager.call_started(job_metadata) diff --git a/superset/tasks/mitm/drop_mitm_dataset.py b/superset/tasks/mitm/drop_mitm_dataset.py index 09b536019c..2a2062d122 100644 --- a/superset/tasks/mitm/drop_mitm_dataset.py +++ b/superset/tasks/mitm/drop_mitm_dataset.py @@ -3,9 +3,9 @@ from __future__ import annotations import logging from typing import TYPE_CHECKING -from celery import shared_task from celery.exceptions import SoftTimeLimitExceeded +from superset.extensions import celery_app from superset.utils.core import override_user from .common import * from ..async_queries import _load_user_from_job_metadata @@ -18,11 +18,11 @@ if TYPE_CHECKING: from superset.customization.external_service_support.common import AsyncJobMetadata -@shared_task(name='drop_mitm_dataset', - soft_time_limit=service_call_timeout, - ignore_result=True) -def drop_mitm_dataset_task(job_metadata: AsyncJobMetadata, mitm_dataset_id: int) -> None: - +@celery_app.task(name='drop_mitm_dataset', + soft_time_limit=service_call_timeout, + ignore_result=True) +def drop_mitm_dataset_task(job_metadata: AsyncJobMetadata, + mitm_dataset_id: int) -> None: external_service_call_manager.call_started(job_metadata) from ...commands.mitm.mitm_service.delete import DeleteUploadedMitMDatasetCommand diff --git a/superset/tasks/mitm/upload_mitm_dataset.py b/superset/tasks/mitm/upload_mitm_dataset.py index 074407d6da..1009f1e15a 100644 --- a/superset/tasks/mitm/upload_mitm_dataset.py +++ b/superset/tasks/mitm/upload_mitm_dataset.py @@ -3,9 +3,9 @@ from __future__ import annotations import logging from typing import TYPE_CHECKING -from celery import shared_task from celery.exceptions import SoftTimeLimitExceeded +from superset.extensions import celery_app from superset.utils.core import override_user from .common import * from ..async_queries import _load_user_from_job_metadata @@ -18,9 +18,9 @@ if TYPE_CHECKING: from superset.customization.external_service_support.common import AsyncJobMetadata -@shared_task(name='upload_mitm_dataset', - soft_time_limit=service_call_timeout, - ignore_result=True) +@celery_app.task(name='upload_mitm_dataset', + soft_time_limit=service_call_timeout, + ignore_result=True) def upload_mitm_dataset_task(job_metadata: AsyncJobMetadata, dataset_name: str, mitm_zip: bytes, mitm_name: str = 'MAED') -> None: diff --git a/tests/requests/mitm_dataset_api.http b/tests/requests/mitm_dataset_api.http index e7150e771c..e2ffa92afc 100644 --- a/tests/requests/mitm_dataset_api.http +++ b/tests/requests/mitm_dataset_api.http @@ -74,3 +74,12 @@ POST http://localhost:8088/api/v1/mitm_dataset/export/ Authorization: Bearer {{jwt_token}} X-CSRFToken: {{csrf_token}} +### + +// @name Get Specific MitM Dataset +GET http://localhost:8088/api/v1/mitm_dataset/d4a65922-f083-441f-be9d-3b7c88b283c9 +Authorization: Bearer {{jwt_token}} +X-CSRFToken: {{csrf_token}} + +### + -- GitLab