diff --git a/RESOURCES/STANDARD_ROLES.md b/RESOURCES/STANDARD_ROLES.md index 184f86a53d794..27af7bba2ff66 100644 --- a/RESOURCES/STANDARD_ROLES.md +++ b/RESOURCES/STANDARD_ROLES.md @@ -64,7 +64,6 @@ |can user slices on Superset|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O| |can favstar on Superset|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O| |can import dashboards on Superset|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O| -|can search queries on Superset|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:| |can sqllab viz on Superset|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:| |can schemas on Superset|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O| |can sqllab history on Superset|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:| @@ -92,8 +91,6 @@ |can sqllab table viz on Superset|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:| |can profile on Superset|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O| |can available domains on Superset|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O| -|can queries on Superset|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O| -|can stop query on Superset|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:| |can request access on Superset|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O| |can dashboard on Superset|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O| |can post on TableSchemaView|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O| diff --git a/UPDATING.md b/UPDATING.md index d9426ca52a196..72c9f7d6bffe7 100644 --- a/UPDATING.md +++ b/UPDATING.md @@ -34,6 +34,7 @@ assists people when migrating to a new version. ### Breaking Changes +- [24360](https://github.com/apache/superset/pull/24360): Removed deprecated APIs `/superset/stop_query/...`, `/superset/queries/...`, `/superset/search_queries` - [24353](https://github.com/apache/superset/pull/24353): Removed deprecated APIs `/copy_dash/int:dashboard_id/`, `/save_dash/int:dashboard_id/`, `/add_slices/int:dashboard_id/`. - [24198](https://github.com/apache/superset/pull/24198) The FAB views `User Registrations` and `User's Statistics` have been changed to Admin only. To re-enable them for non-admin users, please add the following perms to your custom role: `menu access on User's Statistics` and `menu access on User Registrations`. - [24354](https://github.com/apache/superset/pull/24354): Removed deprecated APIs `/superset/testconn`, `/superset/validate_sql_json/`, `/superset/schemas_access_for_file_upload`, `/superset/extra_table_metadata` diff --git a/superset/security/manager.py b/superset/security/manager.py index f0bd1151707d0..41e522f3eaea9 100644 --- a/superset/security/manager.py +++ b/superset/security/manager.py @@ -241,9 +241,7 @@ class SupersetSecurityManager( # pylint: disable=too-many-public-methods ("can_sqllab_viz", "Superset"), ("can_sqllab_table_viz", "Superset"), # Deprecated permission remove on 3.0.0 ("can_sqllab", "Superset"), - ("can_stop_query", "Superset"), # Deprecated permission remove on 3.0.0 ("can_test_conn", "Superset"), # Deprecated permission remove on 3.0.0 - ("can_search_queries", "Superset"), # Deprecated permission remove on 3.0.0 ("can_activate", "TabStateView"), ("can_get", "TabStateView"), ("can_delete_query", "TabStateView"), diff --git a/superset/views/core.py b/superset/views/core.py index f715b6b7c766b..8483733730b34 100755 --- a/superset/views/core.py +++ b/superset/views/core.py @@ -22,7 +22,6 @@ from typing import Any, Callable, cast from urllib import parse -import backoff import simplejson as json from flask import abort, flash, g, redirect, render_template, request, Response from flask_appbuilder import expose @@ -44,13 +43,11 @@ event_logger, is_feature_enabled, security_manager, - sql_lab, viz, ) from superset.charts.commands.exceptions import ChartNotFoundError from superset.charts.dao import ChartDAO from superset.common.chart_data import ChartDataResultFormat, ChartDataResultType -from superset.common.db_query_status import QueryStatus from superset.connectors.base.models import BaseDatasource from superset.connectors.sqla.models import ( AnnotationDatasource, @@ -58,7 +55,6 @@ SqlMetric, TableColumn, ) -from superset.constants import QUERY_EARLY_CANCEL_KEY from superset.dashboards.commands.exceptions import DashboardAccessDeniedError from superset.dashboards.commands.importers.v0 import ImportDashboardsCommand from superset.dashboards.permalink.commands.get import GetDashboardPermalinkCommand @@ -70,7 +66,6 @@ from superset.exceptions import ( CacheLoadError, DatabaseNotFound, - SupersetCancelQueryException, SupersetErrorException, SupersetException, SupersetGenericErrorException, @@ -94,7 +89,6 @@ from superset.utils.async_query_manager import AsyncQueryTokenException from superset.utils.cache import etag_cache from superset.utils.core import DatasourceType, get_user_id, ReservedUrlParameters -from superset.utils.dates import now_as_float from superset.views.base import ( api, BaseSupersetView, @@ -1433,44 +1427,6 @@ def extra_table_metadata( # pylint: disable=no-self-use def theme(self) -> FlaskResponse: return self.render_template("superset/theme.html") - @has_access_api - @handle_api_exception - @expose("/stop_query/", methods=("POST",)) - @event_logger.log_this - @backoff.on_exception( - backoff.constant, - Exception, - interval=1, - on_backoff=lambda details: db.session.rollback(), - on_giveup=lambda details: db.session.rollback(), - max_tries=5, - ) - @deprecated(new_target="/api/v1/query/stop") - def stop_query(self) -> FlaskResponse: - client_id = request.form.get("client_id") - query = db.session.query(Query).filter_by(client_id=client_id).one() - if query.status in [ - QueryStatus.FAILED, - QueryStatus.SUCCESS, - QueryStatus.TIMED_OUT, - ]: - logger.warning( - "Query with client_id could not be stopped: query already complete", - ) - return self.json_response("OK") - - if not sql_lab.cancel_query(query): - raise SupersetCancelQueryException("Could not cancel query") - - query.status = QueryStatus.STOPPED - # Add the stop identity attribute because the sqlalchemy thread is unsafe - # because of multiple updates to the status in the query table - query.set_extra_json_key(QUERY_EARLY_CANCEL_KEY, True) - query.end_time = now_as_float() - db.session.commit() - - return self.json_response("OK") - @api @handle_api_exception @has_access @@ -1495,102 +1451,6 @@ def fetch_datasource_metadata(self) -> FlaskResponse: # pylint: disable=no-self datasource.raise_for_access() return json_success(json.dumps(sanitize_datasource_data(datasource.data))) - @has_access_api - @event_logger.log_this - @expose("/queries/") - @expose("/queries/") - @deprecated(new_target="api/v1/query/updated_since") - def queries(self, last_updated_ms: float | int) -> FlaskResponse: - """ - Get the updated queries. - - :param last_updated_ms: Unix time (milliseconds) - """ - - return self.queries_exec(last_updated_ms) - - @staticmethod - def queries_exec(last_updated_ms: float | int) -> FlaskResponse: - stats_logger.incr("queries") - if not get_user_id(): - return json_error_response( - "Please login to access the queries.", status=403 - ) - - # UTC date time, same that is stored in the DB. - last_updated_dt = datetime.utcfromtimestamp(last_updated_ms / 1000) - - sql_queries = ( - db.session.query(Query) - .filter(Query.user_id == get_user_id(), Query.changed_on >= last_updated_dt) - .all() - ) - dict_queries = {q.client_id: q.to_dict() for q in sql_queries} - return json_success(json.dumps(dict_queries, default=utils.json_int_dttm_ser)) - - @has_access - @event_logger.log_this - @expose("/search_queries") - @deprecated(new_target="api/v1/query/") - def search_queries(self) -> FlaskResponse: # pylint: disable=no-self-use - """ - Search for previously run sqllab queries. Used for Sqllab Query Search - page /superset/sqllab#search. - - Custom permission can_only_search_queries_owned restricts queries - to only queries run by current user. - - :returns: Response with list of sql query dicts - """ - if security_manager.can_access_all_queries(): - search_user_id = request.args.get("user_id") - elif request.args.get("user_id") is not None: - try: - search_user_id = int(cast(int, request.args.get("user_id"))) - except ValueError: - return Response(status=400, mimetype="application/json") - if search_user_id != get_user_id(): - return Response(status=403, mimetype="application/json") - else: - search_user_id = get_user_id() - database_id = request.args.get("database_id") - search_text = request.args.get("search_text") - # From and To time stamp should be Epoch timestamp in seconds - - query = db.session.query(Query) - if search_user_id: - # Filter on user_id - query = query.filter(Query.user_id == search_user_id) - - if database_id: - # Filter on db Id - query = query.filter(Query.database_id == database_id) - - if status := request.args.get("status"): - # Filter on status - query = query.filter(Query.status == status) - - if search_text: - # Filter on search text - query = query.filter(Query.sql.like(f"%{search_text}%")) - - if from_time := request.args.get("from"): - query = query.filter(Query.start_time > int(from_time)) - - if to_time := request.args.get("to"): - query = query.filter(Query.start_time < int(to_time)) - - query_limit = config["QUERY_SEARCH_LIMIT"] - sql_queries = query.order_by(Query.start_time.asc()).limit(query_limit).all() - - dict_queries = [q.to_dict() for q in sql_queries] - - return Response( - json.dumps(dict_queries, default=utils.json_int_dttm_ser), - status=200, - mimetype="application/json", - ) - @app.errorhandler(500) def show_traceback(self) -> FlaskResponse: # pylint: disable=no-self-use return ( diff --git a/tests/integration_tests/core_tests.py b/tests/integration_tests/core_tests.py index 2c6bae5f8913f..86f4a7e3f499a 100644 --- a/tests/integration_tests/core_tests.py +++ b/tests/integration_tests/core_tests.py @@ -1243,29 +1243,6 @@ def test_dashboard_injected_exceptions(self, mock_db_connection_mutator): data = self.get_resp(url) self.assertIn("Error message", data) - @mock.patch("superset.sql_lab.cancel_query") - @mock.patch("superset.views.core.db.session") - def test_stop_query_not_implemented( - self, mock_superset_db_session, mock_sql_lab_cancel_query - ): - """ - Handles stop query when the DB engine spec does not - have a cancel query method. - """ - form_data = {"client_id": "foo"} - query_mock = mock.Mock() - query_mock.client_id = "foo" - query_mock.status = QueryStatus.RUNNING - self.login(username="admin") - mock_superset_db_session.query().filter_by().one().return_value = query_mock - mock_sql_lab_cancel_query.return_value = False - rv = self.client.post( - "/superset/stop_query/", - data={"form_data": json.dumps(form_data)}, - ) - - assert rv.status_code == 422 - @pytest.mark.usefixtures("load_energy_table_with_slice") @mock.patch("superset.explore.form_data.commands.create.CreateFormDataCommand.run") def test_explore_redirect(self, mock_command: mock.Mock): diff --git a/tests/integration_tests/sqllab_tests.py b/tests/integration_tests/sqllab_tests.py index e9892b1d36c4b..6880fb2325aaa 100644 --- a/tests/integration_tests/sqllab_tests.py +++ b/tests/integration_tests/sqllab_tests.py @@ -17,8 +17,7 @@ # isort:skip_file """Unit tests for Sql Lab""" import json -from datetime import datetime, timedelta -from math import ceil, floor +from datetime import datetime import pytest from celery.exceptions import SoftTimeLimitExceeded @@ -327,132 +326,6 @@ def test_sql_json_schema_access(self): engine.execute(f"DROP TABLE IF EXISTS {CTAS_SCHEMA_NAME}.test_table") db.session.commit() - def test_queries_endpoint(self): - self.run_some_queries() - - # Not logged in, should error out - resp = self.client.get("/superset/queries/0") - # Redirects to the login page - self.assertEqual(401, resp.status_code) - - # Admin sees queries - self.login("admin") - data = self.get_json_resp("/superset/queries/0") - self.assertEqual(2, len(data)) - data = self.get_json_resp("/superset/queries/0.0") - self.assertEqual(2, len(data)) - - # Run 2 more queries - self.run_sql("SELECT * FROM birth_names LIMIT 1", client_id="client_id_4") - self.run_sql("SELECT * FROM birth_names LIMIT 2", client_id="client_id_5") - self.login("admin") - data = self.get_json_resp("/superset/queries/0") - self.assertEqual(4, len(data)) - - now = datetime.now() + timedelta(days=1) - query = ( - db.session.query(Query) - .filter_by(sql="SELECT * FROM birth_names LIMIT 1") - .first() - ) - query.changed_on = now - db.session.commit() - - data = self.get_json_resp( - f"/superset/queries/{float(datetime_to_epoch(now)) - 1000}" - ) - self.assertEqual(1, len(data)) - - self.logout() - resp = self.client.get("/superset/queries/0") - # Redirects to the login page - self.assertEqual(401, resp.status_code) - - def test_search_query_on_db_id(self): - self.run_some_queries() - self.login("admin") - examples_dbid = get_example_database().id - - # Test search queries on database Id - data = self.get_json_resp( - f"/superset/search_queries?database_id={examples_dbid}" - ) - self.assertEqual(3, len(data)) - db_ids = [k["dbId"] for k in data] - self.assertEqual([examples_dbid for i in range(3)], db_ids) - - resp = self.get_resp("/superset/search_queries?database_id=-1") - data = json.loads(resp) - self.assertEqual(0, len(data)) - - def test_search_query_on_user(self): - self.run_some_queries() - self.login("admin") - - # Test search queries on user Id - user_id = security_manager.find_user("admin").id - data = self.get_json_resp(f"/superset/search_queries?user_id={user_id}") - self.assertEqual(2, len(data)) - user_ids = {k["userId"] for k in data} - self.assertEqual({user_id}, user_ids) - - user_id = security_manager.find_user("gamma_sqllab").id - resp = self.get_resp(f"/superset/search_queries?user_id={user_id}") - data = json.loads(resp) - self.assertEqual(1, len(data)) - self.assertEqual(data[0]["userId"], user_id) - - @pytest.mark.usefixtures("load_birth_names_dashboard_with_slices") - def test_search_query_on_status(self): - self.run_some_queries() - self.login("admin") - # Test search queries on status - resp = self.get_resp("/superset/search_queries?status=success") - data = json.loads(resp) - self.assertEqual(2, len(data)) - states = [k["state"] for k in data] - self.assertEqual(["success", "success"], states) - - resp = self.get_resp("/superset/search_queries?status=failed") - data = json.loads(resp) - self.assertEqual(1, len(data)) - self.assertEqual(data[0]["state"], "failed") - - def test_search_query_on_text(self): - self.run_some_queries() - self.login("admin") - url = "/superset/search_queries?search_text=birth" - data = self.get_json_resp(url) - self.assertEqual(2, len(data)) - self.assertIn("birth", data[0]["sql"]) - - def test_search_query_filter_by_time(self): - self.run_some_queries() - self.login("admin") - from_time = floor( - (db.session.query(Query).filter_by(sql=QUERY_1).one()).start_time - ) - to_time = ceil( - (db.session.query(Query).filter_by(sql=QUERY_2).one()).start_time - ) - url = f"/superset/search_queries?from={from_time}&to={to_time}" - assert len(self.client.get(url).json) == 2 - - def test_search_query_only_owned(self) -> None: - """ - Test a search query with a user that does not have can_access_all_queries. - """ - # Test search_queries for Alpha user - self.run_some_queries() - self.login("gamma_sqllab") - - user_id = security_manager.find_user("gamma_sqllab").id - data = self.get_json_resp("/superset/search_queries") - - self.assertEqual(1, len(data)) - user_ids = {k["userId"] for k in data} - self.assertEqual({user_id}, user_ids) - def test_alias_duplicate(self): self.run_sql( "SELECT name as col, gender as col FROM birth_names LIMIT 10",