mirror of https://github.com/apache/superset.git
chore: remove deprecated apis on superset, testconn, validate_sql_json, schemas_access_for_file_upload, extra_table_metadata (#24354)
This commit is contained in:
parent
c04bd4c6a7
commit
6f145dfe36
|
@ -82,10 +82,8 @@
|
||||||
|can fetch datasource metadata on Superset|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
|can fetch datasource metadata on Superset|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
||||||
|can override role permissions on Superset|:heavy_check_mark:|O|O|O|
|
|can override role permissions on Superset|:heavy_check_mark:|O|O|O|
|
||||||
|can created dashboards on Superset|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
|can created dashboards on Superset|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
||||||
|can extra table metadata on Superset|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
|
||||||
|can csrf token on Superset|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
|can csrf token on Superset|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
||||||
|can created slices on Superset|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
|can created slices on Superset|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
||||||
|can testconn on Superset|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
|
||||||
|can annotation json on Superset|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
|can annotation json on Superset|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
||||||
|can add slices on Superset|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
|can add slices on Superset|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
||||||
|can fave dashboards on Superset|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
|can fave dashboards on Superset|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
||||||
|
@ -96,7 +94,6 @@
|
||||||
|can warm up cache on Superset|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
|can warm up cache on Superset|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
||||||
|can sqllab table viz on Superset|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|
|
|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 profile on Superset|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
||||||
|can validate sql json 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 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 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 stop query on Superset|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|
|
||||||
|
@ -202,7 +199,6 @@
|
||||||
|can edit on FilterSets|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
|can edit on FilterSets|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
||||||
|can this form get on ColumnarToDatabaseView|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
|can this form get on ColumnarToDatabaseView|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
||||||
|can this form post on ColumnarToDatabaseView|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
|can this form post on ColumnarToDatabaseView|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
||||||
|can schemas access for file upload on Superset|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
|
||||||
|menu access on Upload a Columnar file|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
|menu access on Upload a Columnar file|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
||||||
|can export on Chart|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
|can export on Chart|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
||||||
|can write on DashboardFilterStateRestApi|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
|can write on DashboardFilterStateRestApi|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|
|
||||||
|
|
|
@ -34,6 +34,7 @@ assists people when migrating to a new version.
|
||||||
|
|
||||||
### Breaking Changes
|
### Breaking Changes
|
||||||
|
|
||||||
|
- [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`
|
||||||
- [24381](https://github.com/apache/superset/pull/24381): Removed deprecated API `/superset/available_domains/`
|
- [24381](https://github.com/apache/superset/pull/24381): Removed deprecated API `/superset/available_domains/`
|
||||||
- [24359](https://github.com/apache/superset/pull/24359): Removed deprecated APIs `/superset/estimate_query_cost/..`, `/superset/results/..`, `/superset/sql_json/..`, `/superset/csv/..`
|
- [24359](https://github.com/apache/superset/pull/24359): Removed deprecated APIs `/superset/estimate_query_cost/..`, `/superset/results/..`, `/superset/sql_json/..`, `/superset/csv/..`
|
||||||
- [24345](https://github.com/apache/superset/pull/24345) Converts `ENABLE_BROAD_ACTIVITY_ACCESS` and `MENU_HIDE_USER_INFO` into feature flags and changes the value of `ENABLE_BROAD_ACTIVITY_ACCESS` to `False` as it's more secure.
|
- [24345](https://github.com/apache/superset/pull/24345) Converts `ENABLE_BROAD_ACTIVITY_ACCESS` and `MENU_HIDE_USER_INFO` into feature flags and changes the value of `ENABLE_BROAD_ACTIVITY_ACCESS` to `False` as it's more secure.
|
||||||
|
|
|
@ -18,8 +18,6 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import re
|
|
||||||
from contextlib import closing
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Any, Callable, cast
|
from typing import Any, Callable, cast
|
||||||
from urllib import parse
|
from urllib import parse
|
||||||
|
@ -36,7 +34,7 @@ from flask_appbuilder.security.decorators import (
|
||||||
from flask_appbuilder.security.sqla import models as ab_models
|
from flask_appbuilder.security.sqla import models as ab_models
|
||||||
from flask_babel import gettext as __, lazy_gettext as _
|
from flask_babel import gettext as __, lazy_gettext as _
|
||||||
from sqlalchemy import and_, or_
|
from sqlalchemy import and_, or_
|
||||||
from sqlalchemy.exc import DBAPIError, NoSuchModuleError, SQLAlchemyError
|
from sqlalchemy.exc import SQLAlchemyError
|
||||||
|
|
||||||
from superset import (
|
from superset import (
|
||||||
app,
|
app,
|
||||||
|
@ -66,15 +64,12 @@ from superset.dashboards.commands.importers.v0 import ImportDashboardsCommand
|
||||||
from superset.dashboards.dao import DashboardDAO
|
from superset.dashboards.dao import DashboardDAO
|
||||||
from superset.dashboards.permalink.commands.get import GetDashboardPermalinkCommand
|
from superset.dashboards.permalink.commands.get import GetDashboardPermalinkCommand
|
||||||
from superset.dashboards.permalink.exceptions import DashboardPermalinkGetFailedError
|
from superset.dashboards.permalink.exceptions import DashboardPermalinkGetFailedError
|
||||||
from superset.databases.commands.exceptions import DatabaseInvalidError
|
|
||||||
from superset.databases.dao import DatabaseDAO
|
from superset.databases.dao import DatabaseDAO
|
||||||
from superset.databases.utils import make_url_safe
|
|
||||||
from superset.datasets.commands.exceptions import DatasetNotFoundError
|
from superset.datasets.commands.exceptions import DatasetNotFoundError
|
||||||
from superset.datasource.dao import DatasourceDAO
|
from superset.datasource.dao import DatasourceDAO
|
||||||
from superset.errors import ErrorLevel, SupersetError, SupersetErrorType
|
from superset.errors import ErrorLevel, SupersetError, SupersetErrorType
|
||||||
from superset.exceptions import (
|
from superset.exceptions import (
|
||||||
CacheLoadError,
|
CacheLoadError,
|
||||||
CertificateException,
|
|
||||||
DatabaseNotFound,
|
DatabaseNotFound,
|
||||||
SupersetCancelQueryException,
|
SupersetCancelQueryException,
|
||||||
SupersetErrorException,
|
SupersetErrorException,
|
||||||
|
@ -93,9 +88,7 @@ from superset.models.dashboard import Dashboard
|
||||||
from superset.models.slice import Slice
|
from superset.models.slice import Slice
|
||||||
from superset.models.sql_lab import Query, TabState
|
from superset.models.sql_lab import Query, TabState
|
||||||
from superset.models.user_attributes import UserAttribute
|
from superset.models.user_attributes import UserAttribute
|
||||||
from superset.security.analytics_db_safety import check_sqlalchemy_uri
|
|
||||||
from superset.sql_parse import ParsedQuery
|
from superset.sql_parse import ParsedQuery
|
||||||
from superset.sql_validators import get_validator_by_name
|
|
||||||
from superset.superset_typing import FlaskResponse
|
from superset.superset_typing import FlaskResponse
|
||||||
from superset.tasks.async_queries import load_explore_json_into_cache
|
from superset.tasks.async_queries import load_explore_json_into_cache
|
||||||
from superset.utils import core as utils
|
from superset.utils import core as utils
|
||||||
|
@ -985,90 +978,6 @@ class Superset(BaseSupersetView): # pylint: disable=too-many-public-methods
|
||||||
session.close()
|
session.close()
|
||||||
return "SLICES ADDED"
|
return "SLICES ADDED"
|
||||||
|
|
||||||
@api
|
|
||||||
@has_access_api
|
|
||||||
@event_logger.log_this
|
|
||||||
@expose(
|
|
||||||
"/testconn",
|
|
||||||
methods=(
|
|
||||||
"GET",
|
|
||||||
"POST",
|
|
||||||
),
|
|
||||||
) # pylint: disable=no-self-use
|
|
||||||
@deprecated(new_target="/api/v1/database/test_connection/")
|
|
||||||
def testconn(self) -> FlaskResponse:
|
|
||||||
"""Tests a sqla connection"""
|
|
||||||
db_name = request.json.get("name")
|
|
||||||
uri = request.json.get("uri")
|
|
||||||
try:
|
|
||||||
if app.config["PREVENT_UNSAFE_DB_CONNECTIONS"]:
|
|
||||||
check_sqlalchemy_uri(make_url_safe(uri))
|
|
||||||
# if the database already exists in the database, only its safe
|
|
||||||
# (password-masked) URI would be shown in the UI and would be passed in the
|
|
||||||
# form data so if the database already exists and the form was submitted
|
|
||||||
# with the safe URI, we assume we should retrieve the decrypted URI to test
|
|
||||||
# the connection.
|
|
||||||
if db_name:
|
|
||||||
existing_database = (
|
|
||||||
db.session.query(Database)
|
|
||||||
.filter_by(database_name=db_name)
|
|
||||||
.one_or_none()
|
|
||||||
)
|
|
||||||
if existing_database and uri == existing_database.safe_sqlalchemy_uri():
|
|
||||||
uri = existing_database.sqlalchemy_uri_decrypted
|
|
||||||
|
|
||||||
# This is the database instance that will be tested. Note the extra fields
|
|
||||||
# are represented as JSON encoded strings in the model.
|
|
||||||
database = Database(
|
|
||||||
server_cert=request.json.get("server_cert"),
|
|
||||||
extra=json.dumps(request.json.get("extra", {})),
|
|
||||||
impersonate_user=request.json.get("impersonate_user"),
|
|
||||||
encrypted_extra=json.dumps(request.json.get("encrypted_extra", {})),
|
|
||||||
)
|
|
||||||
database.set_sqlalchemy_uri(uri)
|
|
||||||
database.db_engine_spec.mutate_db_for_connection_test(database)
|
|
||||||
|
|
||||||
with database.get_sqla_engine_with_context() as engine:
|
|
||||||
with closing(engine.raw_connection()) as conn:
|
|
||||||
if engine.dialect.do_ping(conn):
|
|
||||||
return json_success('"OK"')
|
|
||||||
|
|
||||||
raise DBAPIError(None, None, None)
|
|
||||||
except CertificateException as ex:
|
|
||||||
logger.info("Certificate exception")
|
|
||||||
return json_error_response(ex.message)
|
|
||||||
except (NoSuchModuleError, ModuleNotFoundError):
|
|
||||||
logger.info("Invalid driver")
|
|
||||||
driver_name = make_url_safe(uri).drivername
|
|
||||||
return json_error_response(
|
|
||||||
_(
|
|
||||||
"Could not load database driver: %(driver_name)s",
|
|
||||||
driver_name=driver_name,
|
|
||||||
),
|
|
||||||
400,
|
|
||||||
)
|
|
||||||
except DatabaseInvalidError:
|
|
||||||
logger.info("Invalid URI")
|
|
||||||
return json_error_response(
|
|
||||||
_(
|
|
||||||
"Invalid connection string, a valid string usually follows:\n"
|
|
||||||
"'DRIVER://USER:PASSWORD@DB-HOST/DATABASE-NAME'"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
except DBAPIError:
|
|
||||||
logger.warning("Connection failed")
|
|
||||||
return json_error_response(
|
|
||||||
_("Connection failed, please check your connection settings"), 400
|
|
||||||
)
|
|
||||||
except SupersetSecurityException as ex:
|
|
||||||
logger.warning("Stopped an unsafe database connection")
|
|
||||||
return json_error_response(_(str(ex)), 400)
|
|
||||||
except Exception as ex: # pylint: disable=broad-except
|
|
||||||
logger.warning("Unexpected error %s", type(ex).__name__)
|
|
||||||
return json_error_response(
|
|
||||||
_("Unexpected error occurred, please check your logs for details"), 400
|
|
||||||
)
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_user_activity_access_error(user_id: int) -> FlaskResponse | None:
|
def get_user_activity_access_error(user_id: int) -> FlaskResponse | None:
|
||||||
try:
|
try:
|
||||||
|
@ -1695,84 +1604,6 @@ class Superset(BaseSupersetView): # pylint: disable=too-many-public-methods
|
||||||
|
|
||||||
return self.json_response("OK")
|
return self.json_response("OK")
|
||||||
|
|
||||||
@has_access_api
|
|
||||||
@event_logger.log_this
|
|
||||||
@expose(
|
|
||||||
"/validate_sql_json/",
|
|
||||||
methods=(
|
|
||||||
"GET",
|
|
||||||
"POST",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
@deprecated(new_target="/api/v1/database/<pk>/validate_sql/")
|
|
||||||
def validate_sql_json(
|
|
||||||
# pylint: disable=too-many-locals,no-self-use
|
|
||||||
self,
|
|
||||||
) -> FlaskResponse:
|
|
||||||
"""Validates that arbitrary sql is acceptable for the given database.
|
|
||||||
Returns a list of error/warning annotations as json.
|
|
||||||
"""
|
|
||||||
sql = request.form["sql"]
|
|
||||||
database_id = request.form["database_id"]
|
|
||||||
schema = request.form.get("schema") or None
|
|
||||||
template_params = json.loads(request.form.get("templateParams") or "{}")
|
|
||||||
|
|
||||||
if template_params is not None and len(template_params) > 0:
|
|
||||||
# TODO: factor the Database object out of template rendering
|
|
||||||
# or provide it as mydb so we can render template params
|
|
||||||
# without having to also persist a Query ORM object.
|
|
||||||
return json_error_response(
|
|
||||||
"SQL validation does not support template parameters", status=400
|
|
||||||
)
|
|
||||||
|
|
||||||
session = db.session()
|
|
||||||
mydb = session.query(Database).filter_by(id=database_id).one_or_none()
|
|
||||||
if not mydb:
|
|
||||||
return json_error_response(
|
|
||||||
f"Database with id {database_id} is missing.", status=400
|
|
||||||
)
|
|
||||||
|
|
||||||
spec = mydb.db_engine_spec
|
|
||||||
validators_by_engine = app.config["SQL_VALIDATORS_BY_ENGINE"]
|
|
||||||
if not validators_by_engine or spec.engine not in validators_by_engine:
|
|
||||||
return json_error_response(
|
|
||||||
f"no SQL validator is configured for {spec.engine}", status=400
|
|
||||||
)
|
|
||||||
validator_name = validators_by_engine[spec.engine]
|
|
||||||
validator = get_validator_by_name(validator_name)
|
|
||||||
if not validator:
|
|
||||||
return json_error_response(
|
|
||||||
"No validator named {} found (configured for the {} engine)".format(
|
|
||||||
validator_name, spec.engine
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
|
||||||
timeout = config["SQLLAB_VALIDATION_TIMEOUT"]
|
|
||||||
timeout_msg = f"The query exceeded the {timeout} seconds timeout."
|
|
||||||
with utils.timeout(seconds=timeout, error_message=timeout_msg):
|
|
||||||
errors = validator.validate(sql, schema, mydb)
|
|
||||||
payload = json.dumps(
|
|
||||||
[err.to_dict() for err in errors],
|
|
||||||
default=utils.pessimistic_json_iso_dttm_ser,
|
|
||||||
ignore_nan=True,
|
|
||||||
encoding=None,
|
|
||||||
)
|
|
||||||
return json_success(payload)
|
|
||||||
except Exception as ex: # pylint: disable=broad-except
|
|
||||||
logger.exception(ex)
|
|
||||||
msg = _(
|
|
||||||
"%(validator)s was unable to check your query.\n"
|
|
||||||
"Please recheck your query.\n"
|
|
||||||
"Exception: %(ex)s",
|
|
||||||
validator=validator.name,
|
|
||||||
ex=ex,
|
|
||||||
)
|
|
||||||
# Return as a 400 if the database error message says we got a 4xx error
|
|
||||||
if re.search(r"([\W]|^)4\d{2}([\W]|$)", str(ex)):
|
|
||||||
return json_error_response(f"{msg}", status=400)
|
|
||||||
return json_error_response(f"{msg}")
|
|
||||||
|
|
||||||
@api
|
@api
|
||||||
@handle_api_exception
|
@handle_api_exception
|
||||||
@has_access
|
@has_access
|
||||||
|
@ -2042,38 +1873,3 @@ class Superset(BaseSupersetView): # pylint: disable=too-many-public-methods
|
||||||
@event_logger.log_this
|
@event_logger.log_this
|
||||||
def sqllab_history(self) -> FlaskResponse:
|
def sqllab_history(self) -> FlaskResponse:
|
||||||
return super().render_app_template()
|
return super().render_app_template()
|
||||||
|
|
||||||
@api
|
|
||||||
@has_access_api
|
|
||||||
@event_logger.log_this
|
|
||||||
@expose("/schemas_access_for_file_upload")
|
|
||||||
@deprecated(new_target="api/v1/database/{pk}/schemas_access_for_file_upload/")
|
|
||||||
def schemas_access_for_file_upload(self) -> FlaskResponse:
|
|
||||||
"""
|
|
||||||
This method exposes an API endpoint to
|
|
||||||
get the schema access control settings for file upload in this database
|
|
||||||
"""
|
|
||||||
if not request.args.get("db_id"):
|
|
||||||
return json_error_response("No database is allowed for your file upload")
|
|
||||||
|
|
||||||
db_id = int(request.args["db_id"])
|
|
||||||
database = db.session.query(Database).filter_by(id=db_id).one()
|
|
||||||
try:
|
|
||||||
schemas_allowed = database.get_schema_access_for_file_upload()
|
|
||||||
if security_manager.can_access_database(database):
|
|
||||||
return self.json_response(schemas_allowed)
|
|
||||||
# the list schemas_allowed should not be empty here
|
|
||||||
# and the list schemas_allowed_processed returned from security_manager
|
|
||||||
# should not be empty either,
|
|
||||||
# otherwise the database should have been filtered out
|
|
||||||
# in CsvToDatabaseForm
|
|
||||||
schemas_allowed_processed = security_manager.get_schemas_accessible_by_user(
|
|
||||||
database, schemas_allowed, False
|
|
||||||
)
|
|
||||||
return self.json_response(schemas_allowed_processed)
|
|
||||||
except Exception as ex: # pylint: disable=broad-except
|
|
||||||
logger.exception(ex)
|
|
||||||
return json_error_response(
|
|
||||||
"Failed to fetch schemas allowed for csv upload in this database! "
|
|
||||||
"Please contact your Superset Admin!"
|
|
||||||
)
|
|
||||||
|
|
|
@ -398,33 +398,6 @@ class SupersetTestCase(TestCase):
|
||||||
db.session.delete(database)
|
db.session.delete(database)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
def validate_sql(
|
|
||||||
self,
|
|
||||||
sql,
|
|
||||||
client_id=None,
|
|
||||||
username=None,
|
|
||||||
raise_on_error=False,
|
|
||||||
database_name="examples",
|
|
||||||
template_params=None,
|
|
||||||
):
|
|
||||||
if username:
|
|
||||||
self.logout()
|
|
||||||
self.login(username=username)
|
|
||||||
dbid = SupersetTestCase.get_database_by_name(database_name).id
|
|
||||||
resp = self.get_json_resp(
|
|
||||||
"/superset/validate_sql_json/",
|
|
||||||
raise_on_error=False,
|
|
||||||
data=dict(
|
|
||||||
database_id=dbid,
|
|
||||||
sql=sql,
|
|
||||||
client_id=client_id,
|
|
||||||
templateParams=template_params,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
if raise_on_error and "error" in resp:
|
|
||||||
raise Exception("validate_sql failed")
|
|
||||||
return resp
|
|
||||||
|
|
||||||
def get_dash_by_slug(self, dash_slug):
|
def get_dash_by_slug(self, dash_slug):
|
||||||
sesh = db.session()
|
sesh = db.session()
|
||||||
return sesh.query(Dashboard).filter_by(slug=dash_slug).first()
|
return sesh.query(Dashboard).filter_by(slug=dash_slug).first()
|
||||||
|
|
|
@ -342,98 +342,6 @@ class TestCore(SupersetTestCase):
|
||||||
assert self.get_resp("/healthcheck") == "OK"
|
assert self.get_resp("/healthcheck") == "OK"
|
||||||
assert self.get_resp("/ping") == "OK"
|
assert self.get_resp("/ping") == "OK"
|
||||||
|
|
||||||
def test_testconn(self, username="admin"):
|
|
||||||
# need to temporarily allow sqlite dbs, teardown will undo this
|
|
||||||
app.config["PREVENT_UNSAFE_DB_CONNECTIONS"] = False
|
|
||||||
self.login(username=username)
|
|
||||||
database = superset.utils.database.get_example_database()
|
|
||||||
# validate that the endpoint works with the password-masked sqlalchemy uri
|
|
||||||
data = json.dumps(
|
|
||||||
{
|
|
||||||
"uri": database.safe_sqlalchemy_uri(),
|
|
||||||
"name": "examples",
|
|
||||||
"impersonate_user": False,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
response = self.client.post(
|
|
||||||
"/superset/testconn", data=data, content_type="application/json"
|
|
||||||
)
|
|
||||||
assert response.status_code == 200
|
|
||||||
assert response.headers["Content-Type"] == "application/json"
|
|
||||||
|
|
||||||
# validate that the endpoint works with the decrypted sqlalchemy uri
|
|
||||||
data = json.dumps(
|
|
||||||
{
|
|
||||||
"uri": database.sqlalchemy_uri_decrypted,
|
|
||||||
"name": "examples",
|
|
||||||
"impersonate_user": False,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
response = self.client.post(
|
|
||||||
"/superset/testconn", data=data, content_type="application/json"
|
|
||||||
)
|
|
||||||
assert response.status_code == 200
|
|
||||||
assert response.headers["Content-Type"] == "application/json"
|
|
||||||
|
|
||||||
def test_testconn_failed_conn(self, username="admin"):
|
|
||||||
self.login(username=username)
|
|
||||||
|
|
||||||
data = json.dumps(
|
|
||||||
{"uri": "broken://url", "name": "examples", "impersonate_user": False}
|
|
||||||
)
|
|
||||||
response = self.client.post(
|
|
||||||
"/superset/testconn", data=data, content_type="application/json"
|
|
||||||
)
|
|
||||||
assert response.status_code == 400
|
|
||||||
assert response.headers["Content-Type"] == "application/json"
|
|
||||||
response_body = json.loads(response.data.decode("utf-8"))
|
|
||||||
expected_body = {"error": "Could not load database driver: broken"}
|
|
||||||
assert response_body == expected_body, "{} != {}".format(
|
|
||||||
response_body,
|
|
||||||
expected_body,
|
|
||||||
)
|
|
||||||
|
|
||||||
data = json.dumps(
|
|
||||||
{
|
|
||||||
"uri": "mssql+pymssql://url",
|
|
||||||
"name": "examples",
|
|
||||||
"impersonate_user": False,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
response = self.client.post(
|
|
||||||
"/superset/testconn", data=data, content_type="application/json"
|
|
||||||
)
|
|
||||||
assert response.status_code == 400
|
|
||||||
assert response.headers["Content-Type"] == "application/json"
|
|
||||||
response_body = json.loads(response.data.decode("utf-8"))
|
|
||||||
expected_body = {"error": "Could not load database driver: mssql+pymssql"}
|
|
||||||
assert response_body == expected_body, "{} != {}".format(
|
|
||||||
response_body,
|
|
||||||
expected_body,
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_testconn_unsafe_uri(self, username="admin"):
|
|
||||||
self.login(username=username)
|
|
||||||
app.config["PREVENT_UNSAFE_DB_CONNECTIONS"] = True
|
|
||||||
|
|
||||||
response = self.client.post(
|
|
||||||
"/superset/testconn",
|
|
||||||
data=json.dumps(
|
|
||||||
{
|
|
||||||
"uri": "sqlite:///home/superset/unsafe.db",
|
|
||||||
"name": "unsafe",
|
|
||||||
"impersonate_user": False,
|
|
||||||
}
|
|
||||||
),
|
|
||||||
content_type="application/json",
|
|
||||||
)
|
|
||||||
self.assertEqual(400, response.status_code)
|
|
||||||
response_body = json.loads(response.data.decode("utf-8"))
|
|
||||||
expected_body = {
|
|
||||||
"error": "SQLiteDialect_pysqlite cannot be used as a data source for security reasons."
|
|
||||||
}
|
|
||||||
self.assertEqual(expected_body, response_body)
|
|
||||||
|
|
||||||
def test_custom_password_store(self):
|
def test_custom_password_store(self):
|
||||||
database = superset.utils.database.get_example_database()
|
database = superset.utils.database.get_example_database()
|
||||||
conn_pre = sqla.engine.url.make_url(database.sqlalchemy_uri_decrypted)
|
conn_pre = sqla.engine.url.make_url(database.sqlalchemy_uri_decrypted)
|
||||||
|
@ -551,15 +459,6 @@ class TestCore(SupersetTestCase):
|
||||||
assert "Charts" in self.get_resp("/chart/list/")
|
assert "Charts" in self.get_resp("/chart/list/")
|
||||||
assert "Dashboards" in self.get_resp("/dashboard/list/")
|
assert "Dashboards" in self.get_resp("/dashboard/list/")
|
||||||
|
|
||||||
@pytest.mark.usefixtures("load_birth_names_dashboard_with_slices")
|
|
||||||
def test_extra_table_metadata(self):
|
|
||||||
self.login()
|
|
||||||
example_db = superset.utils.database.get_example_database()
|
|
||||||
schema = "default" if example_db.backend in {"presto", "hive"} else "superset"
|
|
||||||
self.get_json_resp(
|
|
||||||
f"/superset/extra_table_metadata/{example_db.id}/birth_names/{schema}/"
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_templated_sql_json(self):
|
def test_templated_sql_json(self):
|
||||||
if superset.utils.database.get_example_database().backend == "presto":
|
if superset.utils.database.get_example_database().backend == "presto":
|
||||||
# TODO: make it work for presto
|
# TODO: make it work for presto
|
||||||
|
@ -994,30 +893,6 @@ class TestCore(SupersetTestCase):
|
||||||
self.assertEqual(rv.status_code, 404)
|
self.assertEqual(rv.status_code, 404)
|
||||||
self.assertEqual(data["error"], "Cached data not found")
|
self.assertEqual(data["error"], "Cached data not found")
|
||||||
|
|
||||||
@mock.patch(
|
|
||||||
"superset.security.SupersetSecurityManager.get_schemas_accessible_by_user"
|
|
||||||
)
|
|
||||||
@mock.patch("superset.security.SupersetSecurityManager.can_access_database")
|
|
||||||
@mock.patch("superset.security.SupersetSecurityManager.can_access_all_datasources")
|
|
||||||
def test_schemas_access_for_csv_upload_endpoint(
|
|
||||||
self,
|
|
||||||
mock_can_access_all_datasources,
|
|
||||||
mock_can_access_database,
|
|
||||||
mock_schemas_accessible,
|
|
||||||
):
|
|
||||||
self.login(username="admin")
|
|
||||||
dbobj = self.create_fake_db()
|
|
||||||
mock_can_access_all_datasources.return_value = False
|
|
||||||
mock_can_access_database.return_value = False
|
|
||||||
mock_schemas_accessible.return_value = ["this_schema_is_allowed_too"]
|
|
||||||
data = self.get_json_resp(
|
|
||||||
url="/superset/schemas_access_for_file_upload?db_id={db_id}".format(
|
|
||||||
db_id=dbobj.id
|
|
||||||
)
|
|
||||||
)
|
|
||||||
assert data == ["this_schema_is_allowed_too"]
|
|
||||||
self.delete_fake_db()
|
|
||||||
|
|
||||||
def test_results_default_deserialization(self):
|
def test_results_default_deserialization(self):
|
||||||
use_new_deserialization = False
|
use_new_deserialization = False
|
||||||
data = [("a", 4, 4.0, "2019-08-18T16:39:16.660000")]
|
data = [("a", 4, 4.0, "2019-08-18T16:39:16.660000")]
|
||||||
|
|
|
@ -3617,44 +3617,3 @@ class TestDatabaseApi(SupersetTestCase):
|
||||||
return
|
return
|
||||||
self.assertEqual(rv.status_code, 422)
|
self.assertEqual(rv.status_code, 422)
|
||||||
self.assertIn("Kaboom!", response["errors"][0]["message"])
|
self.assertIn("Kaboom!", response["errors"][0]["message"])
|
||||||
|
|
||||||
@mock.patch(
|
|
||||||
"superset.security.SupersetSecurityManager.get_schemas_accessible_by_user"
|
|
||||||
)
|
|
||||||
@mock.patch("superset.security.SupersetSecurityManager.can_access_database")
|
|
||||||
@mock.patch("superset.security.SupersetSecurityManager.can_access_all_datasources")
|
|
||||||
def test_schemas_access_for_csv_upload_not_found_endpoint(
|
|
||||||
self,
|
|
||||||
mock_can_access_all_datasources,
|
|
||||||
mock_can_access_database,
|
|
||||||
mock_schemas_accessible,
|
|
||||||
):
|
|
||||||
self.login(username="gamma")
|
|
||||||
self.create_fake_db()
|
|
||||||
mock_can_access_database.return_value = False
|
|
||||||
mock_schemas_accessible.return_value = ["this_schema_is_allowed_too"]
|
|
||||||
rv = self.client.get(f"/api/v1/database/120ff/schemas_access_for_file_upload")
|
|
||||||
self.assertEqual(rv.status_code, 404)
|
|
||||||
self.delete_fake_db()
|
|
||||||
|
|
||||||
@mock.patch(
|
|
||||||
"superset.security.SupersetSecurityManager.get_schemas_accessible_by_user"
|
|
||||||
)
|
|
||||||
@mock.patch("superset.security.SupersetSecurityManager.can_access_database")
|
|
||||||
@mock.patch("superset.security.SupersetSecurityManager.can_access_all_datasources")
|
|
||||||
def test_schemas_access_for_csv_upload_endpoint(
|
|
||||||
self,
|
|
||||||
mock_can_access_all_datasources,
|
|
||||||
mock_can_access_database,
|
|
||||||
mock_schemas_accessible,
|
|
||||||
):
|
|
||||||
self.login(username="admin")
|
|
||||||
dbobj = self.create_fake_db()
|
|
||||||
mock_can_access_all_datasources.return_value = False
|
|
||||||
mock_can_access_database.return_value = False
|
|
||||||
mock_schemas_accessible.return_value = ["this_schema_is_allowed_too"]
|
|
||||||
data = self.get_json_resp(
|
|
||||||
url=f"/api/v1/database/{dbobj.id}/schemas_access_for_file_upload"
|
|
||||||
)
|
|
||||||
assert data == {"schemas": ["this_schema_is_allowed_too"]}
|
|
||||||
self.delete_fake_db()
|
|
||||||
|
|
|
@ -19,12 +19,8 @@
|
||||||
import unittest
|
import unittest
|
||||||
from unittest.mock import MagicMock, patch
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
import pytest
|
|
||||||
from pyhive.exc import DatabaseError
|
from pyhive.exc import DatabaseError
|
||||||
|
|
||||||
from superset import app
|
|
||||||
from superset.sql_validators import SQLValidationAnnotation
|
|
||||||
from superset.sql_validators.base import BaseSQLValidator
|
|
||||||
from superset.sql_validators.postgres import PostgreSQLValidator
|
from superset.sql_validators.postgres import PostgreSQLValidator
|
||||||
from superset.sql_validators.presto_db import (
|
from superset.sql_validators.presto_db import (
|
||||||
PrestoDBSQLValidator,
|
PrestoDBSQLValidator,
|
||||||
|
@ -34,139 +30,6 @@ from superset.utils.database import get_example_database
|
||||||
|
|
||||||
from .base_tests import SupersetTestCase
|
from .base_tests import SupersetTestCase
|
||||||
|
|
||||||
PRESTO_SQL_VALIDATORS_BY_ENGINE = {
|
|
||||||
"presto": "PrestoDBSQLValidator",
|
|
||||||
"sqlite": "PrestoDBSQLValidator",
|
|
||||||
"postgresql": "PrestoDBSQLValidator",
|
|
||||||
"mysql": "PrestoDBSQLValidator",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class TestSqlValidatorEndpoint(SupersetTestCase):
|
|
||||||
"""Testing for Sql Lab querytext validation endpoint"""
|
|
||||||
|
|
||||||
def tearDown(self):
|
|
||||||
self.logout()
|
|
||||||
|
|
||||||
@patch.dict(
|
|
||||||
"superset.config.SQL_VALIDATORS_BY_ENGINE",
|
|
||||||
{},
|
|
||||||
clear=True,
|
|
||||||
)
|
|
||||||
def test_validate_sql_endpoint_noconfig(self):
|
|
||||||
"""Assert that validate_sql_json errors out when no validators are
|
|
||||||
configured for any db"""
|
|
||||||
self.login("admin")
|
|
||||||
|
|
||||||
resp = self.validate_sql(
|
|
||||||
"SELECT * FROM birth_names", client_id="1", raise_on_error=False
|
|
||||||
)
|
|
||||||
self.assertIn("error", resp)
|
|
||||||
self.assertIn("no SQL validator is configured", resp["error"])
|
|
||||||
|
|
||||||
@patch("superset.views.core.get_validator_by_name")
|
|
||||||
@patch.dict(
|
|
||||||
"superset.config.SQL_VALIDATORS_BY_ENGINE",
|
|
||||||
PRESTO_SQL_VALIDATORS_BY_ENGINE,
|
|
||||||
clear=True,
|
|
||||||
)
|
|
||||||
def test_validate_sql_endpoint_mocked(self, get_validator_by_name):
|
|
||||||
"""Assert that, with a mocked validator, annotations make it back out
|
|
||||||
from the validate_sql_json endpoint as a list of json dictionaries"""
|
|
||||||
if get_example_database().backend == "hive":
|
|
||||||
pytest.skip("Hive validator is not implemented")
|
|
||||||
self.login("admin")
|
|
||||||
|
|
||||||
validator = MagicMock()
|
|
||||||
get_validator_by_name.return_value = validator
|
|
||||||
validator.validate.return_value = [
|
|
||||||
SQLValidationAnnotation(
|
|
||||||
message="I don't know what I expected, but it wasn't this",
|
|
||||||
line_number=4,
|
|
||||||
start_column=12,
|
|
||||||
end_column=42,
|
|
||||||
)
|
|
||||||
]
|
|
||||||
|
|
||||||
resp = self.validate_sql(
|
|
||||||
"SELECT * FROM somewhere_over_the_rainbow",
|
|
||||||
client_id="1",
|
|
||||||
raise_on_error=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
self.assertEqual(1, len(resp))
|
|
||||||
self.assertIn("expected,", resp[0]["message"])
|
|
||||||
|
|
||||||
@patch("superset.views.core.get_validator_by_name")
|
|
||||||
@patch.dict(
|
|
||||||
"superset.config.SQL_VALIDATORS_BY_ENGINE",
|
|
||||||
PRESTO_SQL_VALIDATORS_BY_ENGINE,
|
|
||||||
clear=True,
|
|
||||||
)
|
|
||||||
def test_validate_sql_endpoint_mocked_params(self, get_validator_by_name):
|
|
||||||
"""Assert that, with a mocked validator, annotations make it back out
|
|
||||||
from the validate_sql_json endpoint as a list of json dictionaries"""
|
|
||||||
if get_example_database().backend == "hive":
|
|
||||||
pytest.skip("Hive validator is not implemented")
|
|
||||||
self.login("admin")
|
|
||||||
|
|
||||||
validator = MagicMock()
|
|
||||||
get_validator_by_name.return_value = validator
|
|
||||||
validator.validate.return_value = [
|
|
||||||
SQLValidationAnnotation(
|
|
||||||
message="This worked",
|
|
||||||
line_number=4,
|
|
||||||
start_column=12,
|
|
||||||
end_column=42,
|
|
||||||
)
|
|
||||||
]
|
|
||||||
|
|
||||||
resp = self.validate_sql(
|
|
||||||
"SELECT * FROM somewhere_over_the_rainbow",
|
|
||||||
client_id="1",
|
|
||||||
raise_on_error=False,
|
|
||||||
template_params="null",
|
|
||||||
)
|
|
||||||
|
|
||||||
self.assertEqual(1, len(resp))
|
|
||||||
self.assertNotIn("error,", resp[0]["message"])
|
|
||||||
|
|
||||||
@patch("superset.views.core.get_validator_by_name")
|
|
||||||
@patch.dict(
|
|
||||||
"superset.config.SQL_VALIDATORS_BY_ENGINE",
|
|
||||||
PRESTO_SQL_VALIDATORS_BY_ENGINE,
|
|
||||||
clear=True,
|
|
||||||
)
|
|
||||||
def test_validate_sql_endpoint_failure(self, get_validator_by_name):
|
|
||||||
"""Assert that validate_sql_json errors out when the selected validator
|
|
||||||
raises an unexpected exception"""
|
|
||||||
self.login("admin")
|
|
||||||
|
|
||||||
validator = MagicMock()
|
|
||||||
get_validator_by_name.return_value = validator
|
|
||||||
validator.validate.side_effect = Exception("Kaboom!")
|
|
||||||
|
|
||||||
resp = self.validate_sql(
|
|
||||||
"SELECT * FROM birth_names", client_id="1", raise_on_error=False
|
|
||||||
)
|
|
||||||
# TODO(bkyryliuk): properly handle hive error
|
|
||||||
if get_example_database().backend == "hive":
|
|
||||||
assert resp["error"] == "no SQL validator is configured for hive"
|
|
||||||
else:
|
|
||||||
self.assertIn("error", resp)
|
|
||||||
self.assertIn("Kaboom!", resp["error"])
|
|
||||||
|
|
||||||
|
|
||||||
class TestBaseValidator(SupersetTestCase):
|
|
||||||
"""Testing for the base sql validator"""
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
self.validator = BaseSQLValidator
|
|
||||||
|
|
||||||
def test_validator_excepts(self):
|
|
||||||
with self.assertRaises(NotImplementedError):
|
|
||||||
self.validator.validate(None, None, None)
|
|
||||||
|
|
||||||
|
|
||||||
class TestPrestoValidator(SupersetTestCase):
|
class TestPrestoValidator(SupersetTestCase):
|
||||||
"""Testing for the prestodb sql validator"""
|
"""Testing for the prestodb sql validator"""
|
||||||
|
@ -236,22 +99,6 @@ class TestPrestoValidator(SupersetTestCase):
|
||||||
|
|
||||||
self.assertEqual(1, len(errors))
|
self.assertEqual(1, len(errors))
|
||||||
|
|
||||||
@patch.dict(
|
|
||||||
"superset.config.SQL_VALIDATORS_BY_ENGINE",
|
|
||||||
{},
|
|
||||||
clear=True,
|
|
||||||
)
|
|
||||||
def test_validate_sql_endpoint(self):
|
|
||||||
self.login("admin")
|
|
||||||
# NB this is effectively an integration test -- when there's a default
|
|
||||||
# validator for sqlite, this test will fail because the validator
|
|
||||||
# will no longer error out.
|
|
||||||
resp = self.validate_sql(
|
|
||||||
"SELECT * FROM birth_names", client_id="1", raise_on_error=False
|
|
||||||
)
|
|
||||||
self.assertIn("error", resp)
|
|
||||||
self.assertIn("no SQL validator is configured", resp["error"])
|
|
||||||
|
|
||||||
|
|
||||||
class TestPostgreSQLValidator(SupersetTestCase):
|
class TestPostgreSQLValidator(SupersetTestCase):
|
||||||
def test_valid_syntax(self):
|
def test_valid_syntax(self):
|
||||||
|
|
Loading…
Reference in New Issue