From 04ffe6269a4ee3bd4a2b3e042d75268fcde9d254 Mon Sep 17 00:00:00 2001 From: Leah Tacke genannt Unterberg <leah.tgu@pads.rwth-aachen.de> Date: Wed, 9 Apr 2025 17:21:37 +0200 Subject: [PATCH] the app is runnable again --- docker-compose.yml | 4 +- helm/superset/Chart.lock | 8 +- helm/superset/Chart.yaml | 12 +- superset-frontend/package-lock.json | 2 +- .../mitmDatasets/MitMDatasetModal/index.tsx | 2 +- .../UploadMitMDatasetModal/index.tsx | 60 +++++---- .../mitmDatasets/asyncExternalService.ts | 4 +- .../src/pages/MitMDatasetList/index.tsx | 61 +++++---- superset/cli/importexport.py | 85 +++++++++++- .../mitm/mitm_dataset/importers/dispatcher.py | 72 +++++++++++ .../mitm_dataset/importers/v1/__init__.py | 2 +- superset/initialization/__init__.py | 26 ++-- .../2025-03-20_14-24_8e7de5b14153_.py | 90 ------------- ...0_14-24_8e7de5b14153_added_mitm_dataset.py | 121 ++++++++++++++++++ superset/models/mitm.py | 18 +++ tests/requests/list_resources.http | 33 +++++ tests/requests/mitm_dataset_api.http | 2 + tests/requests/mitm_service_call.http | 13 +- 18 files changed, 434 insertions(+), 181 deletions(-) create mode 100644 superset/commands/mitm/mitm_dataset/importers/dispatcher.py delete mode 100644 superset/migrations/versions/2025-03-20_14-24_8e7de5b14153_.py create mode 100644 superset/migrations/versions/2025-03-20_14-24_8e7de5b14153_added_mitm_dataset.py create mode 100644 tests/requests/list_resources.http diff --git a/docker-compose.yml b/docker-compose.yml index f74147e15e..d8f3a39287 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -273,10 +273,10 @@ services: superset-mitm-service: build: - context: "C:/Users/leah/PycharmProjects/superset-mitm-service" + context: "https://git-ce.rwth-aachen.de/machine-data/superset-mitm-service.git" + # context: "C:/Users/leah/PycharmProjects/superset-mitm-service" pull: true no_cache: true - # context: "https://git-ce.rwth-aachen.de/machine-data/superset-mitm-service.git" container_name: superset_mitm_service env_file: - path: docker/.env-mitm # default diff --git a/helm/superset/Chart.lock b/helm/superset/Chart.lock index aab1b386d4..b8cc122bb9 100644 --- a/helm/superset/Chart.lock +++ b/helm/superset/Chart.lock @@ -1,12 +1,12 @@ dependencies: - name: postgresql repository: oci://registry-1.docker.io/bitnamicharts - version: 12.1.6 + version: 13.4.4 - name: redis repository: oci://registry-1.docker.io/bitnamicharts version: 17.9.4 - name: superset-mitm-service repository: oci://registry-1.docker.io/leahtgu - version: 0.1.0 -digest: sha256:39aa1b51029921ce8517209ba4ec5c20fdadd9b879b616fc5a2e30920bee8246 -generated: "2025-03-19T10:31:53.7889567+01:00" + version: 0.1.1 +digest: sha256:7453ef9a2931ded80c999853692344ec8c3e779e999f1b8120d86d108c1fef14 +generated: "2025-04-09T14:32:24.7839858+02:00" diff --git a/helm/superset/Chart.yaml b/helm/superset/Chart.yaml index 6d60e3f368..ca0403acb7 100644 --- a/helm/superset/Chart.yaml +++ b/helm/superset/Chart.yaml @@ -17,19 +17,15 @@ apiVersion: v2 appVersion: "4.1.1" description: Apache Superset is a modern, enterprise-ready business intelligence web application -name: superset +name: mitm-superset icon: https://artifacthub.io/image/68c1d717-0e97-491f-b046-754e46f46922@2x home: https://superset.apache.org/ keywords: - business intelligence - data science sources: - - https://github.com/apache/superset -maintainers: - - name: craig-rueda - email: craig@craigrueda.com - url: https://github.com/craig-rueda -version: 0.14.1 + - https://git-ce.rwth-aachen.de/machine-data/mitm-superset +version: 0.1.0 dependencies: - name: postgresql version: 13.4.4 @@ -40,6 +36,6 @@ dependencies: repository: oci://registry-1.docker.io/bitnamicharts condition: redis.enabled - name: superset-mitm-service - version: 0.1.0 + version: 0.1.1 repository: oci://registry-1.docker.io/leahtgu condition: superset-mitm-service.enabled diff --git a/superset-frontend/package-lock.json b/superset-frontend/package-lock.json index 1f9c50d9ad..7391263d43 100644 --- a/superset-frontend/package-lock.json +++ b/superset-frontend/package-lock.json @@ -50731,7 +50731,7 @@ "version": "0.20.3", "license": "Apache-2.0", "dependencies": { - "@types/react-redux": "^7.1.10", + "@types/react-redux": "^7.1.34", "d3-array": "^1.2.0", "dayjs": "^1.11.13", "lodash": "^4.17.21" diff --git a/superset-frontend/src/features/mitmDatasets/MitMDatasetModal/index.tsx b/superset-frontend/src/features/mitmDatasets/MitMDatasetModal/index.tsx index 6229be0388..a6c1243723 100644 --- a/superset-frontend/src/features/mitmDatasets/MitMDatasetModal/index.tsx +++ b/superset-frontend/src/features/mitmDatasets/MitMDatasetModal/index.tsx @@ -29,7 +29,7 @@ import { css, } from '@superset-ui/core'; -import Icons from 'src/components/Icons'; +import { Icons } from 'src/components/Icons'; import Modal from 'src/components/Modal'; import ErrorMessageWithStackTrace from 'src/components/ErrorMessage/ErrorMessageWithStackTrace'; import withToasts from 'src/components/MessageToasts/withToasts'; diff --git a/superset-frontend/src/features/mitmDatasets/UploadMitMDatasetModal/index.tsx b/superset-frontend/src/features/mitmDatasets/UploadMitMDatasetModal/index.tsx index a08d2a1d0f..7ef54e1c88 100644 --- a/superset-frontend/src/features/mitmDatasets/UploadMitMDatasetModal/index.tsx +++ b/superset-frontend/src/features/mitmDatasets/UploadMitMDatasetModal/index.tsx @@ -10,8 +10,7 @@ import { } from '../../databases/UploadDataModel/styles'; import { Form, Input, Select, Upload } from 'antd-v5'; import { AsyncActionResult, useExternalService } from '../asyncExternalService'; -import { Button } from '../../../components'; -import { InboxOutlined, UploadOutlined } from '@ant-design/icons'; +import { InboxOutlined } from '@ant-design/icons'; interface UploadMitMDatasetModalProps { addDangerToast: (msg: string) => void; @@ -49,6 +48,7 @@ const UploadMitMDatasetModal: FunctionComponent< }: Pick<UploadMitMDatasetRequestFormData, 'mitm' | 'datasetName'>, ) => { addSuccessToast(t('Successfully uploaded \"%s\" (%s)', datasetName, mitm)); + onClose(); }; const onFailure = (error: ClientErrorObject) => { setError(error); @@ -92,11 +92,18 @@ const UploadMitMDatasetModal: FunctionComponent< const onFinish = (values: any) => { const fields = form.getFieldsValue(); + const fileList = fields.fileList; - const mitmZip = fields.fileList[0]?.originFileObj; + if (!fileList?.[0]?.originFileObj) { + addDangerToast(t('Please select a file to upload')); + return; + } - const mitm = fields.mitm as MITM; - makeRequest({ datasetName: fields.datasetName, mitm, mitmZip }); + makeRequest({ + datasetName: fields.datasetName, + mitm: fields.mitm as MITM, + mitmZip: fileList[0].originFileObj, + }); }; const onClose = () => { @@ -104,12 +111,11 @@ const UploadMitMDatasetModal: FunctionComponent< onHide(); }; - const extractFileList = (e: any) => { - console.log('Upload event:', e); + const normFile = (e: any) => { if (Array.isArray(e)) { return e; } - return e?.fileList; + return e?.fileList?.slice(-1) || []; // Keep only the latest file }; return ( @@ -120,7 +126,7 @@ const UploadMitMDatasetModal: FunctionComponent< formStyles(theme), ]} primaryButtonLoading={isLoading} - name="database" + name="mitm-dataset" data-test="upload-modal" onHandledPrimaryAction={form.submit} onHide={onClose} @@ -134,7 +140,7 @@ const UploadMitMDatasetModal: FunctionComponent< <Form.Item name="datasetName" label="Dataset Name" - rules={[{ required: true }]} + rules={[{ required: true, message: 'Please enter a dataset name' }]} > <Input /> </Form.Item> @@ -146,32 +152,24 @@ const UploadMitMDatasetModal: FunctionComponent< <Select placeholder="Select the MitM Name" allowClear + defaultValue={'MAED'} options={[{ name: 'MAED', value: 'MAED' }]} ></Select> </Form.Item> <Form.Item - name="upload" + name="fileList" label="Upload" valuePropName="fileList" - getValueFromEvent={extractFileList} - extra="longgggggggggggggggggggggggggggggggggg" + getValueFromEvent={normFile} + rules={[{ required: true, message: 'Please upload a file' }]} > - <Upload - name="logo" - accept=".zip,.maed,.mitm" - customRequest={() => false} - > - <Button icon={<UploadOutlined />}>Click to upload</Button> - </Upload> - </Form.Item> - <Form.Item label="Dragger"> - <Form.Item - name="dragger" - valuePropName="fileList" - getValueFromEvent={extractFileList} - noStyle - > - <Upload.Dragger name="files"> + <Upload> + <Upload.Dragger + name="file" + accept=".zip,.maed,.mitm" + beforeUpload={() => false} + maxCount={1} + > <p className="ant-upload-drag-icon"> <InboxOutlined /> </p> @@ -179,10 +177,10 @@ const UploadMitMDatasetModal: FunctionComponent< Click or drag file to this area to upload </p> <p className="ant-upload-hint"> - Support for a single or bulk upload. + Supported formats: .zip, .maed, .mitm </p> </Upload.Dragger> - </Form.Item> + </Upload> </Form.Item> </Form> </Modal> diff --git a/superset-frontend/src/features/mitmDatasets/asyncExternalService.ts b/superset-frontend/src/features/mitmDatasets/asyncExternalService.ts index 7296040a08..f32b5ab31b 100644 --- a/superset-frontend/src/features/mitmDatasets/asyncExternalService.ts +++ b/superset-frontend/src/features/mitmDatasets/asyncExternalService.ts @@ -102,7 +102,7 @@ export const asyncCall = async (requestConfig: RequestConfig) => { new Promise<AsyncActionResult | JsonObject>((resolve, reject) => { { if (response.status === 202) { - const jm: AsyncJobMetadata = json; + const jm = json as AsyncJobMetadata; waitForAsyncData(jm).then(resolve).catch(reject); } else { const err = parseErrorJson(json); @@ -116,7 +116,7 @@ export const asyncCall = async (requestConfig: RequestConfig) => { export const asyncAction = async ( requestConfig: RequestConfig, ): Promise<AsyncActionResult> => { - return asyncCall(requestConfig); + return asyncCall(requestConfig) as Promise<AsyncActionResult>; }; type CallExternalServiceProps = { diff --git a/superset-frontend/src/pages/MitMDatasetList/index.tsx b/superset-frontend/src/pages/MitMDatasetList/index.tsx index a455f621ad..75210c86c0 100644 --- a/superset-frontend/src/pages/MitMDatasetList/index.tsx +++ b/superset-frontend/src/pages/MitMDatasetList/index.tsx @@ -16,40 +16,40 @@ * specific language governing permissions and limitations * under the License. */ -import { styled, SupersetClient, t } from '@superset-ui/core'; -import { FunctionComponent, useState, useMemo, useCallback } from 'react'; +import { css, styled, SupersetClient, t, useTheme } from '@superset-ui/core'; +import { FunctionComponent, useCallback, useMemo, useState } from 'react'; import { useHistory } from 'react-router-dom'; import rison from 'rison'; -import { createFetchRelated, createErrorHandler } from 'src/views/CRUD/utils'; +import { createErrorHandler, createFetchRelated } from 'src/views/CRUD/utils'; import { useListViewResource } from 'src/views/CRUD/hooks'; import ConfirmStatusChange from 'src/components/ConfirmStatusChange'; import DeleteModal from 'src/components/DeleteModal'; import handleResourceExport from 'src/utils/export'; import ListView, { - ListViewProps, - Filters, FilterOperator, + Filters, + ListViewProps, } from 'src/components/ListView'; import Loading from 'src/components/Loading'; -import SubMenu, { SubMenuProps, ButtonProps } from 'src/features/home/SubMenu'; +import SubMenu, { ButtonProps, SubMenuProps } from 'src/features/home/SubMenu'; import Owner from 'src/types/Owner'; import withToasts from 'src/components/MessageToasts/withToasts'; import { Tooltip } from 'src/components/Tooltip'; -import Icons from 'src/components/Icons'; +import { Icons } from 'src/components/Icons'; import FacePile from 'src/components/FacePile'; import ImportModelsModal from 'src/components/ImportModal/index'; import { isUserAdmin } from 'src/dashboard/util/permissionUtils'; import { MitMDataset } from 'src/features/mitmDatasets/types'; import { + CONFIRM_OVERWRITE_MESSAGE, PAGE_SIZE, - SORT_BY, PASSWORDS_NEEDED_MESSAGE, - CONFIRM_OVERWRITE_MESSAGE, + SORT_BY, } from 'src/features/mitmDatasets/constants'; import { ModifiedInfo } from 'src/components/AuditInfo'; -import MitMDatasetModal from "../../features/mitmDatasets/MitMDatasetModal"; -import UploadMitMDatasetModal from "../../features/mitmDatasets/UploadMitMDatasetModal"; +import MitMDatasetModal from '../../features/mitmDatasets/MitMDatasetModal'; +import UploadMitMDatasetModal from '../../features/mitmDatasets/UploadMitMDatasetModal'; const Actions = styled.div` color: ${({ theme }) => theme.colors.grayscale.base}; @@ -93,6 +93,7 @@ const MitMDatasetList: FunctionComponent<MitMDatasetListProps> = ({ user, }) => { const history = useHistory(); + const theme = useTheme(); const { state: { loading, @@ -116,8 +117,6 @@ const MitMDatasetList: FunctionComponent<MitMDatasetListProps> = ({ const [datasetCurrentlyEditing, setDatasetCurrentlyEditing] = useState<MitMDataset | null>(null); - - const [importingDataset, showImportModal] = useState<boolean>(false); const [uploadModalOpen, setUploadModalOpen] = useState<boolean>(false); const [preparingExport, setPreparingExport] = useState<boolean>(false); @@ -183,11 +182,13 @@ const MitMDatasetList: FunctionComponent<MitMDatasetListProps> = ({ ); const handleBulkDatasetExport = (datasetsToExport: MitMDataset[]) => { - const ids = datasetsToExport.map(({ id }) => id); - handleResourceExport('mitm_dataset', ids, () => { - setPreparingExport(false); - }); - setPreparingExport(true); + if (!!datasetsToExport) { + const ids = datasetsToExport.map(({ id }) => id); + handleResourceExport('mitm_dataset', ids, () => { + setPreparingExport(false); + }); + setPreparingExport(true); + } }; const columns = useMemo( @@ -261,7 +262,7 @@ const MitMDatasetList: FunctionComponent<MitMDatasetListProps> = ({ className="action-button" onClick={handleDelete} > - <Icons.Trash /> + <Icons.DeleteOutlined /> </span> </Tooltip> )} @@ -277,7 +278,7 @@ const MitMDatasetList: FunctionComponent<MitMDatasetListProps> = ({ className="action-button" onClick={handleExport} > - <Icons.Share /> + <Icons.UploadOutlined /> </span> </Tooltip> )} @@ -299,7 +300,7 @@ const MitMDatasetList: FunctionComponent<MitMDatasetListProps> = ({ className={allowEdit ? 'action-button' : 'disabled'} onClick={allowEdit ? handleEdit : undefined} > - <Icons.EditAlt /> + <Icons.EditOutlined /> </span> </Tooltip> )} @@ -415,7 +416,14 @@ const MitMDatasetList: FunctionComponent<MitMDatasetListProps> = ({ buttonArr.push({ name: ( <> - <i className="fa fa-plus" /> {t('MitM Dataset')}{' '} + <Icons.PlusOutlined + iconColor={theme.colors.primary.light5} + iconSize="m" + css={css` + vertical-align: text-top; + `} + /> + {t('MitM Dataset')} </> ), onClick: () => { @@ -431,12 +439,12 @@ const MitMDatasetList: FunctionComponent<MitMDatasetListProps> = ({ title={t('Upload dataset')} placement="bottomRight" > - <Icons.Import data-test="upload-button" /> + <Icons.UploadOutlined data-test="upload-button" /> </Tooltip> ), buttonStyle: 'link', onClick: () => setUploadModalOpen(true), - }) + }); buttonArr.push({ name: ( @@ -445,7 +453,10 @@ const MitMDatasetList: FunctionComponent<MitMDatasetListProps> = ({ title={t('Import datasets')} placement="bottomRight" > - <Icons.Import data-test="import-button" /> + <Icons.DownloadOutlined + iconColor={theme.colors.primary.dark1} + data-test="import-button" + /> </Tooltip> ), buttonStyle: 'link', diff --git a/superset/cli/importexport.py b/superset/cli/importexport.py index 50c8aa78a7..7721c299ee 100755 --- a/superset/cli/importexport.py +++ b/superset/cli/importexport.py @@ -201,6 +201,83 @@ def import_datasources(path: str, username: Optional[str] = "admin") -> None: sys.exit(1) +@click.command() +@with_appcontext +@click.option( + "--path", + "-p", + required=True, + help="Path to a single ZIP file", +) +@click.option( + "--username", + "-u", + required=True, + help="Specify the user name to assign assets to", +) +def import_assets(path: str, username: Optional[str]) -> None: + """Import assets from ZIP file""" + # pylint: disable=import-outside-toplevel + from superset.commands.importers.v1.utils import get_contents_from_bundle + from superset.commands.importers.v1.assets import ImportAssetsCommand + + if username is not None: + g.user = security_manager.find_user(username=username) + if is_zipfile(path): + with ZipFile(path) as bundle: + contents = get_contents_from_bundle(bundle) + else: + with open(path) as file: + contents = {path: file.read()} + try: + ImportAssetsCommand(contents, overwrite=True).run() + except Exception: # pylint: disable=broad-except + logger.exception( + "There was an error when importing the asset(s), please check " + "the exception traceback in the log" + ) + sys.exit(1) + + +@click.command() +@with_appcontext +@click.option( + "--path", + "-p", + required=True, + help="Path to a single ZIP file", +) +@click.option( + "--username", + "-u", + required=True, + help="Specify the user name to assign MitM Datasets to", +) +def import_mitm_datasets(path: str, username: Optional[str]) -> None: + """Import assets from ZIP file""" + # pylint: disable=import-outside-toplevel + from superset.commands.mitm.mitm_dataset.importers.dispatcher import \ + ImportMitMDatasetsCommand + from superset.commands.importers.v1.utils import get_contents_from_bundle + + if username is not None: + g.user = security_manager.find_user(username=username) + if is_zipfile(path): + with ZipFile(path) as bundle: + contents = get_contents_from_bundle(bundle) + else: + with open(path) as file: + contents = {path: file.read()} + try: + ImportMitMDatasetsCommand(contents, overwrite=True).run() + except Exception: # pylint: disable=broad-except + logger.exception( + "There was an error when importing the MitM Dataset(s), please check " + "the exception traceback in the log" + ) + sys.exit(1) + + @click.command() @with_appcontext @click.option( @@ -290,7 +367,7 @@ def legacy_export_datasources( "--path", "-p", help="Path to a single JSON file or path containing multiple JSON " - "files to import (*.json)", + "files to import (*.json)", ) @click.option( "--recursive", @@ -337,7 +414,7 @@ def legacy_import_dashboards(path: str, recursive: bool, username: str) -> None: "--path", "-p", help="Path to a single YAML file or path containing multiple YAML " - "files to import (*.yaml or *.yml)", + "files to import (*.yaml or *.yml)", ) @click.option( "--sync", @@ -345,8 +422,8 @@ def legacy_import_dashboards(path: str, recursive: bool, username: str) -> None: "sync", default="", help="comma separated list of element types to synchronize " - 'e.g. "metrics,columns" deletes metrics and columns in the DB ' - "that are not specified in the YAML file", + 'e.g. "metrics,columns" deletes metrics and columns in the DB ' + "that are not specified in the YAML file", ) @click.option( "--recursive", diff --git a/superset/commands/mitm/mitm_dataset/importers/dispatcher.py b/superset/commands/mitm/mitm_dataset/importers/dispatcher.py new file mode 100644 index 0000000000..c809eb7af2 --- /dev/null +++ b/superset/commands/mitm/mitm_dataset/importers/dispatcher.py @@ -0,0 +1,72 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +import logging +from typing import Any + +from marshmallow.exceptions import ValidationError + +from superset.commands.base import BaseCommand +from . import v1 +from superset.commands.exceptions import CommandInvalidError +from superset.commands.importers.exceptions import IncorrectVersionError + +logger = logging.getLogger(__name__) + +# list of different import formats supported; v0 should be last because +# the files are not versioned +command_versions = [ + v1.ImportMitMDatasetsCommand, +] + + +class ImportMitMDatasetsCommand(BaseCommand): + """ + Import MitM Datasets. + + This command dispatches the import to different versions of the command + until it finds one that matches. + """ + + def __init__(self, contents: dict[str, str], *args: Any, **kwargs: Any): + self.contents = contents + self.args = args + self.kwargs = kwargs + + def run(self) -> None: + # iterate over all commands until we find a version that can + # handle the contents + for version in command_versions: + command = version(self.contents, *self.args, **self.kwargs) + try: + command.run() + return + except IncorrectVersionError: + logger.debug("File not handled by command, skipping") + except (CommandInvalidError, ValidationError): + # found right version, but file is invalid + logger.info("Command failed validation") + raise + except Exception: + # validation succeeded but something went wrong + logger.exception("Error running import command") + raise + + raise CommandInvalidError("Could not find a valid command to import file") + + def validate(self) -> None: + pass diff --git a/superset/commands/mitm/mitm_dataset/importers/v1/__init__.py b/superset/commands/mitm/mitm_dataset/importers/v1/__init__.py index 4fba9bf05e..13e1253b59 100644 --- a/superset/commands/mitm/mitm_dataset/importers/v1/__init__.py +++ b/superset/commands/mitm/mitm_dataset/importers/v1/__init__.py @@ -39,7 +39,7 @@ class ImportMitMDatasetsCommand(ImportModelsCommand): # self.import_base_assets_command = ImportAssetsCommand(contents, *args, **kwargs) @staticmethod - def _import(configs: dict[str, Any], overwrite: bool = False) -> None: + def _import(configs: dict[str, Any], overwrite: bool = False, contents: dict[str, Any] | None = None) -> None: ImportAssetsCommand._import(configs) configs = deepcopy(configs) diff --git a/superset/initialization/__init__.py b/superset/initialization/__init__.py index e744dff472..2d8d64679b 100644 --- a/superset/initialization/__init__.py +++ b/superset/initialization/__init__.py @@ -286,15 +286,15 @@ class SupersetAppInitializer: # pylint: disable=too-many-public-methods if feature_flag_manager.is_feature_enabled("MITM_SUPPORT"): from superset.views.mitm.views import MitMDatasetView - appbuilder.add_view( - MitMDatasetView, - "MitMDatasetView", - label=__("MitMDataset View"), - icon="fa-database", - category="", - category_icon="", - menu_cond=lambda: feature_flag_manager.is_feature_enabled('MITM_SUPPORT') - ) + # appbuilder.add_view( + # MitMDatasetView, + # "MitMDatasetView", + # label=__("MitMDataset View"), + # icon="fa-database", + # category="", + # category_icon="", + # menu_cond=lambda: feature_flag_manager.is_feature_enabled('MITM_SUPPORT') + # ) appbuilder.add_link( "MitMDatasets", label=__("MitM Datasets"), @@ -304,6 +304,14 @@ class SupersetAppInitializer: # pylint: disable=too-many-public-methods category_icon="", cond=lambda: feature_flag_manager.is_feature_enabled('MITM_SUPPORT') ) + appbuilder.add_link( + "MitMExporter", + label=__("MitM Exporter"), + href="https://maed-exporter.cluster.iop.rwth-aachen.de/", + category="", + category_icon="", + cond=lambda: feature_flag_manager.is_feature_enabled('MITM_SUPPORT') + ) ################### diff --git a/superset/migrations/versions/2025-03-20_14-24_8e7de5b14153_.py b/superset/migrations/versions/2025-03-20_14-24_8e7de5b14153_.py deleted file mode 100644 index d94de6540a..0000000000 --- a/superset/migrations/versions/2025-03-20_14-24_8e7de5b14153_.py +++ /dev/null @@ -1,90 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the License for the -# specific language governing permissions and limitations -# under the License. -"""empty message - -Revision ID: 8e7de5b14153 -Revises: 32bf93dfe2a4 -Create Date: 2025-03-20 14:24:36.237603 - -""" -import sqlalchemy_utils - -# revision identifiers, used by Alembic. -revision = '8e7de5b14153' -down_revision = '32bf93dfe2a4' - -from alembic import op -import sqlalchemy as sa - - - -def upgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.create_table('mitm_datasets', - sa.Column('uuid', sqlalchemy_utils.types.uuid.UUIDType(), nullable=True), - sa.Column('created_on', sa.DateTime(), nullable=True), - sa.Column('changed_on', sa.DateTime(), nullable=True), - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('dataset_name', sa.String(length=255), nullable=False), - sa.Column('mitm', sa.String(length=127), nullable=False), - sa.Column('mitm_header', sa.JSON(), nullable=True), - sa.Column('database_id', sa.Integer(), nullable=False), - sa.Column('created_by_fk', sa.Integer(), nullable=True), - sa.Column('changed_by_fk', sa.Integer(), nullable=True), - sa.ForeignKeyConstraint(['changed_by_fk'], ['ab_user.id'], ), - sa.ForeignKeyConstraint(['created_by_fk'], ['ab_user.id'], ), - sa.ForeignKeyConstraint(['database_id'], ['dbs.id'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('uuid') - ) - op.create_table('mitm_dataset_dashboards', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('mitm_dataset_id', sa.Integer(), nullable=True), - sa.Column('dashboard_id', sa.Integer(), nullable=True), - sa.ForeignKeyConstraint(['dashboard_id'], ['dashboards.id'], ondelete='CASCADE'), - sa.ForeignKeyConstraint(['mitm_dataset_id'], ['mitm_datasets.id'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('mitm_dataset_id', 'dashboard_id') - ) - op.create_table('mitm_dataset_slices', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('mitm_dataset_id', sa.Integer(), nullable=True), - sa.Column('slice_id', sa.Integer(), nullable=True), - sa.ForeignKeyConstraint(['mitm_dataset_id'], ['mitm_datasets.id'], ondelete='CASCADE'), - sa.ForeignKeyConstraint(['slice_id'], ['slices.id'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('mitm_dataset_id', 'slice_id') - ) - op.create_table('mitm_dataset_tables', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('mitm_dataset_id', sa.Integer(), nullable=True), - sa.Column('table_id', sa.Integer(), nullable=True), - sa.ForeignKeyConstraint(['mitm_dataset_id'], ['mitm_datasets.id'], ondelete='CASCADE'), - sa.ForeignKeyConstraint(['table_id'], ['tables.id'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('mitm_dataset_id', 'table_id') - ) - # ### end Alembic commands ### - -def __downgrade():pass - -def downgrade(): - op.drop_table('mitm_dataset_tables') - op.drop_table('mitm_dataset_slices') - op.drop_table('mitm_dataset_dashboards') - op.drop_table('mitm_datasets') - # ### end Alembic commands ### diff --git a/superset/migrations/versions/2025-03-20_14-24_8e7de5b14153_added_mitm_dataset.py b/superset/migrations/versions/2025-03-20_14-24_8e7de5b14153_added_mitm_dataset.py new file mode 100644 index 0000000000..7f69b30711 --- /dev/null +++ b/superset/migrations/versions/2025-03-20_14-24_8e7de5b14153_added_mitm_dataset.py @@ -0,0 +1,121 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +"""added mitm dataset + +Revision ID: 8e7de5b14153 +Revises: 32bf93dfe2a4 +Create Date: 2025-03-20 14:24:36.237603 + +""" +import sqlalchemy_utils + +# revision identifiers, used by Alembic. +revision = '8e7de5b14153' +down_revision = '32bf93dfe2a4' + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('mitm_datasets', + sa.Column('uuid', + sqlalchemy_utils.types.uuid.UUIDType(), + nullable=True), + sa.Column('created_on', sa.DateTime(), nullable=True), + sa.Column('changed_on', sa.DateTime(), nullable=True), + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('dataset_name', sa.String(length=255), nullable=False), + sa.Column('mitm', sa.String(length=127), nullable=False), + sa.Column('mitm_header', sa.JSON(), nullable=True), + sa.Column('database_id', sa.Integer(), nullable=False), + sa.Column('created_by_fk', sa.Integer(), nullable=True), + sa.Column('changed_by_fk', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['changed_by_fk'], ['ab_user.id'], ), + sa.ForeignKeyConstraint(['created_by_fk'], ['ab_user.id'], ), + sa.ForeignKeyConstraint(['database_id'], + ['dbs.id'], + ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('uuid') + ) + op.create_table('mitm_dataset_owners', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('mitm_dataset_id', sa.Integer(), nullable=True), + sa.Column('user_id', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['user_id'], + ['ab_user.id'], + ondelete='CASCADE'), + sa.ForeignKeyConstraint(['mitm_dataset_id'], + ['mitm_datasets.id'], + ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('mitm_dataset_id', 'user_id') + ) + op.create_table('mitm_dataset_dashboards', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('mitm_dataset_id', sa.Integer(), nullable=True), + sa.Column('dashboard_id', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['dashboard_id'], + ['dashboards.id'], + ondelete='CASCADE'), + sa.ForeignKeyConstraint(['mitm_dataset_id'], + ['mitm_datasets.id'], + ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('mitm_dataset_id', 'dashboard_id') + ) + op.create_table('mitm_dataset_slices', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('mitm_dataset_id', sa.Integer(), nullable=True), + sa.Column('slice_id', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['mitm_dataset_id'], + ['mitm_datasets.id'], + ondelete='CASCADE'), + sa.ForeignKeyConstraint(['slice_id'], + ['slices.id'], + ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('mitm_dataset_id', 'slice_id') + ) + op.create_table('mitm_dataset_tables', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('mitm_dataset_id', sa.Integer(), nullable=True), + sa.Column('table_id', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['mitm_dataset_id'], + ['mitm_datasets.id'], + ondelete='CASCADE'), + sa.ForeignKeyConstraint(['table_id'], + ['tables.id'], + ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('mitm_dataset_id', 'table_id') + ) + # ### end Alembic commands ### + + +def __downgrade(): pass + + +def downgrade(): + op.drop_table('mitm_dataset_tables') + op.drop_table('mitm_dataset_slices') + op.drop_table('mitm_dataset_dashboards') + op.drop_table('mitm_dataset_owners') + op.drop_table('mitm_datasets') + # ### end Alembic commands ### diff --git a/superset/models/mitm.py b/superset/models/mitm.py index a4a1786f8f..8efea8558b 100644 --- a/superset/models/mitm.py +++ b/superset/models/mitm.py @@ -11,6 +11,7 @@ from sqlalchemy import ( from sqlalchemy.orm import relationship from sqlalchemy.schema import UniqueConstraint +from superset import security_manager from superset.models.helpers import AuditMixinNullable, ImportExportMixin metadata = Model.metadata # pylint: disable=no-member @@ -45,6 +46,17 @@ mitm_dataset_dashboards = sa.Table( UniqueConstraint("mitm_dataset_id", "dashboard_id"), ) +mitm_dataset_owners = sa.Table( + "mitm_dataset_owners", + metadata, + Column("id", Integer, primary_key=True), + Column("mitm_dataset_id", + Integer, + ForeignKey("mitm_datasets.id", ondelete="CASCADE")), + Column("user_id", Integer, ForeignKey("ab_user.id", ondelete="CASCADE")), + UniqueConstraint("mitm_dataset_id", "user_id"), +) + class MitMDataset(Model, AuditMixinNullable, ImportExportMixin): """An ORM object that stores MitM Dataset related information""" @@ -62,6 +74,12 @@ class MitMDataset(Model, AuditMixinNullable, ImportExportMixin): slices = relationship('Slice', secondary=mitm_dataset_slices) dashboards = relationship('Dashboard', secondary=mitm_dataset_dashboards) + owners = relationship( + security_manager.user_model, + secondary=mitm_dataset_owners, + passive_deletes=True, + ) + export_fields = ['id', 'dataset_name', 'mitm', 'mitm_header', 'database_id'] export_parent = 'database' export_children = ['tables', 'slices', 'dashboards'] diff --git a/tests/requests/list_resources.http b/tests/requests/list_resources.http new file mode 100644 index 0000000000..97bca7e470 --- /dev/null +++ b/tests/requests/list_resources.http @@ -0,0 +1,33 @@ +### + +// @name List DB +// @no-log +GET http://localhost:8088/api/v1/database/ +Authorization: Bearer {{jwt_token}} +X-CSRFToken: {{csrf_token}} + +### + +// @name List Datasets +// @no-log +GET http://localhost:8088/api/v1/dataset/ +Authorization: Bearer {{jwt_token}} +X-CSRFToken: {{csrf_token}} + +### + +// @name List Charts +// @no-log +GET http://localhost:8088/api/v1/chart/ +Authorization: Bearer {{jwt_token}} +X-CSRFToken: {{csrf_token}} + +### + +// @name List Dashboards +// @no-log +GET http://localhost:8088/api/v1/dashboard/ +Authorization: Bearer {{jwt_token}} +X-CSRFToken: {{csrf_token}} + +### diff --git a/tests/requests/mitm_dataset_api.http b/tests/requests/mitm_dataset_api.http index 6d543cf832..e7150e771c 100644 --- a/tests/requests/mitm_dataset_api.http +++ b/tests/requests/mitm_dataset_api.http @@ -15,6 +15,7 @@ X-CSRFToken: {{csrf_token}} ### // @name Import +// @no-redirect POST http://localhost:8088/api/v1/mitm_dataset/import/ Authorization: Bearer {{jwt_token}} X-CSRFToken: {{csrf_token}} @@ -72,3 +73,4 @@ X-CSRFToken: {{csrf_token}} POST http://localhost:8088/api/v1/mitm_dataset/export/ Authorization: Bearer {{jwt_token}} X-CSRFToken: {{csrf_token}} + diff --git a/tests/requests/mitm_service_call.http b/tests/requests/mitm_service_call.http index 8142f383a2..af5cfdab8e 100644 --- a/tests/requests/mitm_service_call.http +++ b/tests/requests/mitm_service_call.http @@ -3,7 +3,7 @@ // @name Call Health POST http://localhost:8088/api/v1/ext_service/call/mitm_service/health/ Authorization: Bearer {{jwt_token}} -X-Csrftoken: {{csrf_token}} +X-CSRFToken: {{csrf_token}} Content-Type: application/json Accept: application/json @@ -15,8 +15,15 @@ Accept: application/json ### // @name Call Result -GET http://localhost:8088/api/v1/ext_service/result/health/ +GET http://localhost:8088/api/v1/ext_service/result/ Authorization: Bearer {{jwt_token}} -X-Csrftoken: {{csrf_token}} +X-CSRFToken: {{csrf_token}} + +### + +// @name Async Events +GET http://localhost:8088/api/v1/async_event/a6da4181-4070-4eaf-b294-0805374a22dc/ +Authorization: Bearer {{jwt_token}} +X-CSRFToken: {{csrf_token}} ### -- GitLab