diff --git a/superset/constants.py b/superset/constants.py index 7d759acf6741c..b775926b21586 100644 --- a/superset/constants.py +++ b/superset/constants.py @@ -136,6 +136,7 @@ class RouteMethod: # pylint: disable=too-few-public-methods "validate_sql": "read", "get_data": "read", "samples": "read", + "delete_ssh_tunnel": "write", } EXTRA_FORM_DATA_APPEND_KEYS = { diff --git a/superset/databases/api.py b/superset/databases/api.py index aced8e7c6faa3..92b9635fd8e49 100644 --- a/superset/databases/api.py +++ b/superset/databases/api.py @@ -72,6 +72,11 @@ ValidateSQLRequest, ValidateSQLResponse, ) +from superset.databases.ssh_tunnel.commands.delete import DeleteSSHTunnelCommand +from superset.databases.ssh_tunnel.commands.exceptions import ( + SSHTunnelDeleteFailedError, + SSHTunnelNotFoundError, +) from superset.databases.utils import get_table_metadata from superset.db_engine_specs import get_available_engine_specs from superset.errors import ErrorLevel, SupersetError, SupersetErrorType @@ -107,6 +112,7 @@ class DatabaseRestApi(BaseSupersetModelRestApi): "available", "validate_parameters", "validate_sql", + "delete_ssh_tunnel", } resource_name = "database" class_permission_name = "Database" @@ -1204,3 +1210,58 @@ def validate_parameters(self) -> FlaskResponse: command = ValidateDatabaseParametersCommand(payload) command.run() return self.response(200, message="OK") + + @expose("//ssh_tunnel/", methods=["DELETE"]) + @protect() + @safe + @statsd_metrics + @event_logger.log_this_with_context( + action=lambda self, *args, **kwargs: f"{self.__class__.__name__}" + f".delete_ssh_tunnel", + log_to_statsd=False, + ) + def delete_ssh_tunnel(self, pk: int) -> Response: + """Deletes a SSH Tunnel + --- + delete: + description: >- + Deletes a SSH Tunnel. + parameters: + - in: path + schema: + type: integer + name: pk + responses: + 200: + description: SSH Tunnel deleted + content: + application/json: + schema: + type: object + properties: + message: + type: string + 401: + $ref: '#/components/responses/401' + 403: + $ref: '#/components/responses/403' + 404: + $ref: '#/components/responses/404' + 422: + $ref: '#/components/responses/422' + 500: + $ref: '#/components/responses/500' + """ + try: + DeleteSSHTunnelCommand(pk).run() + return self.response(200, message="OK") + except SSHTunnelNotFoundError: + return self.response_404() + except SSHTunnelDeleteFailedError as ex: + logger.error( + "Error deleting SSH Tunnel %s: %s", + self.__class__.__name__, + str(ex), + exc_info=True, + ) + return self.response_422(message=str(ex)) diff --git a/tests/unit_tests/databases/api_test.py b/tests/unit_tests/databases/api_test.py index d6f8897c4a090..9d95653a22f47 100644 --- a/tests/unit_tests/databases/api_test.py +++ b/tests/unit_tests/databases/api_test.py @@ -191,3 +191,76 @@ def test_non_zip_import(client: Any, full_api_access: None) -> None: } ] } + + +def test_delete_ssh_tunnel( + mocker: MockFixture, + app: Any, + session: Session, + client: Any, + full_api_access: None, +) -> None: + """ + Test that we can delete SSH Tunnel + """ + with app.app_context(): + from superset.databases.api import DatabaseRestApi + from superset.databases.dao import DatabaseDAO + from superset.databases.ssh_tunnel.models import SSHTunnel + from superset.models.core import Database + + DatabaseRestApi.datamodel.session = session + + # create table for databases + Database.metadata.create_all(session.get_bind()) # pylint: disable=no-member + + # Create our Database + database = Database( + database_name="my_database", + sqlalchemy_uri="gsheets://", + encrypted_extra=json.dumps( + { + "service_account_info": { + "type": "service_account", + "project_id": "black-sanctum-314419", + "private_key_id": "259b0d419a8f840056158763ff54d8b08f7b8173", + "private_key": "SECRET", + "client_email": "google-spreadsheets-demo-servi@black-sanctum-314419.iam.gserviceaccount.com", + "client_id": "SSH_TUNNEL_CREDENTIALS_CLIENT", + "auth_uri": "https://accounts.google.com/o/oauth2/auth", + "token_uri": "https://oauth2.googleapis.com/token", + "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", + "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/google-spreadsheets-demo-servi%40black-sanctum-314419.iam.gserviceaccount.com", + }, + } + ), + ) + session.add(database) + session.commit() + + # mock the lookup so that we don't need to include the driver + mocker.patch("sqlalchemy.engine.URL.get_driver_name", return_value="gsheets") + mocker.patch("superset.utils.log.DBEventLogger.log") + + # Create our SSHTunnel + tunnel = SSHTunnel( + database_id=1, + database=database, + ) + + session.add(tunnel) + session.commit() + + # Get our recently created SSHTunnel + response_tunnel = DatabaseDAO.get_ssh_tunnel(1) + assert response_tunnel + assert isinstance(response_tunnel["ssh_tunnel"], SSHTunnel) + assert 1 == response_tunnel["ssh_tunnel"].database_id + + # Delete the recently created SSHTunnel + response_delete_tunnel = client.delete("/api/v1/database/1/ssh_tunnel/") + assert response_delete_tunnel.json["message"] == "OK" + + response_tunnel = DatabaseDAO.get_ssh_tunnel(1) + assert response_tunnel + assert response_tunnel["ssh_tunnel"] is None