refactor: Break up superset/views/core.py (#10078)
* Remove unreferenced function from views/core.py * Remove excess constants from views/core.py * Extract CssTemplate-related views to their own file from core.py * Remove duplicate constant declaration and make the constant less racist * Move free-floating functions in views/core.py to views/utils.py * Move AccessRequestsModelView out of views/core.py into its own module * Move health checks and KV ModelView out of core.py and into their own modules * Move R model view to its own module * Move after-request header setting to views/base.py from views/core.py * black * mypy * isort * Fix reference to imported app * pylint * Fix some imports * Add some missing view imports * Fix a missing import
This commit is contained in:
parent
987cb6e1fe
commit
01aede0652
|
|
@ -48,7 +48,7 @@ unsafe-load-any-extension=no
|
||||||
# A comma-separated list of package or module names from where C extensions may
|
# A comma-separated list of package or module names from where C extensions may
|
||||||
# be loaded. Extensions are loading into the active Python interpreter and may
|
# be loaded. Extensions are loading into the active Python interpreter and may
|
||||||
# run arbitrary code
|
# run arbitrary code
|
||||||
extension-pkg-whitelist=
|
extension-pkg-whitelist=pyarrow
|
||||||
|
|
||||||
# Allow optimization of some AST trees. This will activate a peephole AST
|
# Allow optimization of some AST trees. This will activate a peephole AST
|
||||||
# optimizer, which will apply various small optimizations. For instance, it can
|
# optimizer, which will apply various small optimizations. For instance, it can
|
||||||
|
|
|
||||||
|
|
@ -145,14 +145,12 @@ class SupersetAppInitializer:
|
||||||
AnnotationModelView,
|
AnnotationModelView,
|
||||||
)
|
)
|
||||||
from superset.views.api import Api
|
from superset.views.api import Api
|
||||||
from superset.views.core import (
|
from superset.views.core import Superset
|
||||||
AccessRequestsModelView,
|
from superset.views.redirects import R
|
||||||
KV,
|
from superset.views.key_value import KV
|
||||||
R,
|
from superset.views.access_requests import AccessRequestsModelView
|
||||||
Superset,
|
from superset.views.css_templates import CssTemplateAsyncModelView
|
||||||
CssTemplateModelView,
|
from superset.views.css_templates import CssTemplateModelView
|
||||||
CssTemplateAsyncModelView,
|
|
||||||
)
|
|
||||||
from superset.charts.api import ChartRestApi
|
from superset.charts.api import ChartRestApi
|
||||||
from superset.views.chart.views import SliceModelView, SliceAsync
|
from superset.views.chart.views import SliceModelView, SliceAsync
|
||||||
from superset.dashboards.api import DashboardRestApi
|
from superset.dashboards.api import DashboardRestApi
|
||||||
|
|
|
||||||
|
|
@ -15,12 +15,16 @@
|
||||||
# specific language governing permissions and limitations
|
# specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
from . import (
|
from . import (
|
||||||
|
access_requests,
|
||||||
annotations,
|
annotations,
|
||||||
api,
|
api,
|
||||||
base,
|
base,
|
||||||
core,
|
core,
|
||||||
|
css_templates,
|
||||||
dashboard,
|
dashboard,
|
||||||
datasource,
|
datasource,
|
||||||
|
health,
|
||||||
|
redirects,
|
||||||
schedules,
|
schedules,
|
||||||
sql_lab,
|
sql_lab,
|
||||||
tags,
|
tags,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
# 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.
|
||||||
|
from flask_appbuilder.models.sqla.interface import SQLAInterface
|
||||||
|
from flask_babel import lazy_gettext as _
|
||||||
|
|
||||||
|
from superset.constants import RouteMethod
|
||||||
|
from superset.views.base import DeleteMixin, SupersetModelView
|
||||||
|
from superset.views.core import DAR
|
||||||
|
|
||||||
|
|
||||||
|
class AccessRequestsModelView( # pylint: disable=too-many-ancestors
|
||||||
|
SupersetModelView, DeleteMixin
|
||||||
|
):
|
||||||
|
datamodel = SQLAInterface(DAR)
|
||||||
|
include_route_methods = RouteMethod.CRUD_SET
|
||||||
|
list_columns = [
|
||||||
|
"username",
|
||||||
|
"user_roles",
|
||||||
|
"datasource_link",
|
||||||
|
"roles_with_datasource",
|
||||||
|
"created_on",
|
||||||
|
]
|
||||||
|
order_columns = ["created_on"]
|
||||||
|
base_order = ("changed_on", "desc")
|
||||||
|
label_columns = {
|
||||||
|
"username": _("User"),
|
||||||
|
"user_roles": _("User Roles"),
|
||||||
|
"database": _("Database URL"),
|
||||||
|
"datasource_link": _("Datasource"),
|
||||||
|
"roles_with_datasource": _("Roles to grant"),
|
||||||
|
"created_on": _("Created On"),
|
||||||
|
}
|
||||||
|
|
@ -375,7 +375,7 @@ class YamlExportMixin: # pylint: disable=too-few-public-methods
|
||||||
|
|
||||||
|
|
||||||
class DeleteMixin: # pylint: disable=too-few-public-methods
|
class DeleteMixin: # pylint: disable=too-few-public-methods
|
||||||
def _delete(self: BaseView, primary_key: int,) -> None:
|
def _delete(self: BaseView, primary_key: int) -> None:
|
||||||
"""
|
"""
|
||||||
Delete function logic, override to implement diferent logic
|
Delete function logic, override to implement diferent logic
|
||||||
deletes the record with primary_key = primary_key
|
deletes the record with primary_key = primary_key
|
||||||
|
|
@ -520,3 +520,18 @@ def bind_field(
|
||||||
|
|
||||||
|
|
||||||
FlaskForm.Meta.bind_field = bind_field
|
FlaskForm.Meta.bind_field = bind_field
|
||||||
|
|
||||||
|
|
||||||
|
@superset_app.after_request
|
||||||
|
def apply_http_headers(response: Response) -> Response:
|
||||||
|
"""Applies the configuration's http headers to all responses"""
|
||||||
|
|
||||||
|
# HTTP_HEADERS is deprecated, this provides backwards compatibility
|
||||||
|
response.headers.extend( # type: ignore
|
||||||
|
{**config["OVERRIDE_HTTP_HEADERS"], **config["HTTP_HEADERS"]}
|
||||||
|
)
|
||||||
|
|
||||||
|
for k, v in config["DEFAULT_HTTP_HEADERS"].items():
|
||||||
|
if k not in response.headers:
|
||||||
|
response.headers[k] = v
|
||||||
|
return response
|
||||||
|
|
|
||||||
|
|
@ -20,22 +20,19 @@ import re
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from contextlib import closing
|
from contextlib import closing
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from typing import Any, Callable, cast, Dict, List, Optional, Union
|
from typing import Any, cast, Dict, List, Optional, Union
|
||||||
from urllib import parse
|
from urllib import parse
|
||||||
|
|
||||||
import backoff
|
import backoff
|
||||||
import msgpack
|
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
import pyarrow as pa
|
|
||||||
import simplejson as json
|
import simplejson as json
|
||||||
from flask import abort, flash, g, Markup, redirect, render_template, request, Response
|
from flask import abort, flash, g, Markup, redirect, render_template, request, Response
|
||||||
from flask_appbuilder import expose
|
from flask_appbuilder import expose
|
||||||
from flask_appbuilder.models.sqla.interface import SQLAInterface
|
from flask_appbuilder.models.sqla.interface import SQLAInterface
|
||||||
from flask_appbuilder.security.decorators import has_access, has_access_api
|
from flask_appbuilder.security.decorators import has_access, has_access_api
|
||||||
from flask_appbuilder.security.sqla import models as ab_models
|
from flask_appbuilder.security.sqla import models as ab_models
|
||||||
from flask_appbuilder.security.sqla.models import User
|
|
||||||
from flask_babel import gettext as __, lazy_gettext as _
|
from flask_babel import gettext as __, lazy_gettext as _
|
||||||
from sqlalchemy import and_, Integer, or_, select
|
from sqlalchemy import and_, or_, select
|
||||||
from sqlalchemy.engine.url import make_url
|
from sqlalchemy.engine.url import make_url
|
||||||
from sqlalchemy.exc import (
|
from sqlalchemy.exc import (
|
||||||
ArgumentError,
|
ArgumentError,
|
||||||
|
|
@ -51,17 +48,14 @@ from superset import (
|
||||||
app,
|
app,
|
||||||
appbuilder,
|
appbuilder,
|
||||||
conf,
|
conf,
|
||||||
dataframe,
|
|
||||||
db,
|
db,
|
||||||
event_logger,
|
event_logger,
|
||||||
get_feature_flags,
|
get_feature_flags,
|
||||||
is_feature_enabled,
|
is_feature_enabled,
|
||||||
result_set,
|
|
||||||
results_backend,
|
results_backend,
|
||||||
results_backend_use_msgpack,
|
results_backend_use_msgpack,
|
||||||
security_manager,
|
security_manager,
|
||||||
sql_lab,
|
sql_lab,
|
||||||
talisman,
|
|
||||||
viz,
|
viz,
|
||||||
)
|
)
|
||||||
from superset.connectors.connector_registry import ConnectorRegistry
|
from superset.connectors.connector_registry import ConnectorRegistry
|
||||||
|
|
@ -71,18 +65,13 @@ from superset.connectors.sqla.models import (
|
||||||
SqlMetric,
|
SqlMetric,
|
||||||
TableColumn,
|
TableColumn,
|
||||||
)
|
)
|
||||||
from superset.constants import RouteMethod
|
|
||||||
from superset.errors import ErrorLevel, SupersetError, SupersetErrorType
|
|
||||||
from superset.exceptions import (
|
from superset.exceptions import (
|
||||||
CertificateException,
|
CertificateException,
|
||||||
DatabaseNotFound,
|
DatabaseNotFound,
|
||||||
QueryObjectValidationError,
|
|
||||||
SupersetException,
|
SupersetException,
|
||||||
SupersetSecurityException,
|
|
||||||
SupersetTimeoutException,
|
SupersetTimeoutException,
|
||||||
)
|
)
|
||||||
from superset.jinja_context import get_template_processor
|
from superset.jinja_context import get_template_processor
|
||||||
from superset.models.core import Database
|
|
||||||
from superset.models.dashboard import Dashboard
|
from superset.models.dashboard import Dashboard
|
||||||
from superset.models.datasource_access_request import DatasourceAccessRequest
|
from superset.models.datasource_access_request import DatasourceAccessRequest
|
||||||
from superset.models.slice import Slice
|
from superset.models.slice import Slice
|
||||||
|
|
@ -98,12 +87,8 @@ from superset.typing import FlaskResponse
|
||||||
from superset.utils import core as utils, dashboard_import_export
|
from superset.utils import core as utils, dashboard_import_export
|
||||||
from superset.utils.dashboard_filter_scopes_converter import copy_filter_scopes
|
from superset.utils.dashboard_filter_scopes_converter import copy_filter_scopes
|
||||||
from superset.utils.dates import now_as_float
|
from superset.utils.dates import now_as_float
|
||||||
from superset.utils.decorators import etag_cache, stats_timing
|
from superset.utils.decorators import etag_cache
|
||||||
from superset.views.database.filters import DatabaseFilter
|
from superset.views.base import (
|
||||||
from superset.views.utils import get_dashboard_extra_filters
|
|
||||||
from superset.viz import BaseViz
|
|
||||||
|
|
||||||
from .base import (
|
|
||||||
api,
|
api,
|
||||||
BaseSupersetView,
|
BaseSupersetView,
|
||||||
check_ownership,
|
check_ownership,
|
||||||
|
|
@ -111,7 +96,6 @@ from .base import (
|
||||||
create_table_permissions,
|
create_table_permissions,
|
||||||
CsvResponse,
|
CsvResponse,
|
||||||
data_payload_response,
|
data_payload_response,
|
||||||
DeleteMixin,
|
|
||||||
generate_download_headers,
|
generate_download_headers,
|
||||||
get_error_msg,
|
get_error_msg,
|
||||||
get_user_roles,
|
get_user_roles,
|
||||||
|
|
@ -119,16 +103,23 @@ from .base import (
|
||||||
json_error_response,
|
json_error_response,
|
||||||
json_errors_response,
|
json_errors_response,
|
||||||
json_success,
|
json_success,
|
||||||
SupersetModelView,
|
|
||||||
validate_sqlatable,
|
validate_sqlatable,
|
||||||
)
|
)
|
||||||
from .utils import (
|
from superset.views.database.filters import DatabaseFilter
|
||||||
|
from superset.views.utils import (
|
||||||
|
_deserialize_results_payload,
|
||||||
apply_display_max_row_limit,
|
apply_display_max_row_limit,
|
||||||
bootstrap_user_data,
|
bootstrap_user_data,
|
||||||
|
check_datasource_perms,
|
||||||
|
check_slice_perms,
|
||||||
|
get_cta_schema_name,
|
||||||
|
get_dashboard_extra_filters,
|
||||||
get_datasource_info,
|
get_datasource_info,
|
||||||
get_form_data,
|
get_form_data,
|
||||||
get_viz,
|
get_viz,
|
||||||
|
is_owner,
|
||||||
)
|
)
|
||||||
|
from superset.viz import BaseViz
|
||||||
|
|
||||||
config = app.config
|
config = app.config
|
||||||
CACHE_DEFAULT_TIMEOUT = config["CACHE_DEFAULT_TIMEOUT"]
|
CACHE_DEFAULT_TIMEOUT = config["CACHE_DEFAULT_TIMEOUT"]
|
||||||
|
|
@ -153,249 +144,9 @@ DATABASE_KEYS = [
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
ALL_DATASOURCE_ACCESS_ERR = __(
|
|
||||||
"This endpoint requires the `all_datasource_access` permission"
|
|
||||||
)
|
|
||||||
DATASOURCE_MISSING_ERR = __("The data source seems to have been deleted")
|
DATASOURCE_MISSING_ERR = __("The data source seems to have been deleted")
|
||||||
ACCESS_REQUEST_MISSING_ERR = __("The access requests seem to have been deleted")
|
|
||||||
USER_MISSING_ERR = __("The user seems to have been deleted")
|
USER_MISSING_ERR = __("The user seems to have been deleted")
|
||||||
|
|
||||||
FORM_DATA_KEY_BLACKLIST: List[str] = []
|
|
||||||
if not config["ENABLE_JAVASCRIPT_CONTROLS"]:
|
|
||||||
FORM_DATA_KEY_BLACKLIST = ["js_tooltip", "js_onclick_href", "js_data_mutator"]
|
|
||||||
|
|
||||||
|
|
||||||
def get_database_access_error_msg(database_name: str) -> str:
|
|
||||||
return __(
|
|
||||||
"This view requires the database %(name)s or "
|
|
||||||
"`all_datasource_access` permission",
|
|
||||||
name=database_name,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def is_owner(obj: Union[Dashboard, Slice], user: User) -> bool:
|
|
||||||
""" Check if user is owner of the slice """
|
|
||||||
return obj and user in obj.owners
|
|
||||||
|
|
||||||
|
|
||||||
def check_datasource_perms(
|
|
||||||
self: "Superset",
|
|
||||||
datasource_type: Optional[str] = None,
|
|
||||||
datasource_id: Optional[int] = None,
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
Check if user can access a cached response from explore_json.
|
|
||||||
|
|
||||||
This function takes `self` since it must have the same signature as the
|
|
||||||
the decorated method.
|
|
||||||
|
|
||||||
:param datasource_type: The datasource type, i.e., 'druid' or 'table'
|
|
||||||
:param datasource_id: The datasource ID
|
|
||||||
:raises SupersetSecurityException: If the user cannot access the resource
|
|
||||||
"""
|
|
||||||
|
|
||||||
form_data = get_form_data()[0]
|
|
||||||
|
|
||||||
try:
|
|
||||||
datasource_id, datasource_type = get_datasource_info(
|
|
||||||
datasource_id, datasource_type, form_data
|
|
||||||
)
|
|
||||||
except SupersetException as ex:
|
|
||||||
raise SupersetSecurityException(
|
|
||||||
SupersetError(
|
|
||||||
error_type=SupersetErrorType.FAILED_FETCHING_DATASOURCE_INFO_ERROR,
|
|
||||||
level=ErrorLevel.ERROR,
|
|
||||||
message=str(ex),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
if datasource_type is None:
|
|
||||||
raise SupersetSecurityException(
|
|
||||||
SupersetError(
|
|
||||||
error_type=SupersetErrorType.UNKNOWN_DATASOURCE_TYPE_ERROR,
|
|
||||||
level=ErrorLevel.ERROR,
|
|
||||||
message="Could not determine datasource type",
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
viz_obj = get_viz(
|
|
||||||
datasource_type=datasource_type,
|
|
||||||
datasource_id=datasource_id,
|
|
||||||
form_data=form_data,
|
|
||||||
force=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
security_manager.assert_viz_permission(viz_obj)
|
|
||||||
|
|
||||||
|
|
||||||
def check_slice_perms(self: "Superset", slice_id: int) -> None:
|
|
||||||
"""
|
|
||||||
Check if user can access a cached response from slice_json.
|
|
||||||
|
|
||||||
This function takes `self` since it must have the same signature as the
|
|
||||||
the decorated method.
|
|
||||||
"""
|
|
||||||
|
|
||||||
form_data, slc = get_form_data(slice_id, use_slice_data=True)
|
|
||||||
|
|
||||||
if slc:
|
|
||||||
viz_obj = get_viz(
|
|
||||||
datasource_type=slc.datasource.type,
|
|
||||||
datasource_id=slc.datasource.id,
|
|
||||||
form_data=form_data,
|
|
||||||
force=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
security_manager.assert_viz_permission(viz_obj)
|
|
||||||
|
|
||||||
|
|
||||||
def _deserialize_results_payload(
|
|
||||||
payload: Union[bytes, str], query: Query, use_msgpack: Optional[bool] = False
|
|
||||||
) -> Dict[str, Any]:
|
|
||||||
logger.debug(f"Deserializing from msgpack: {use_msgpack}")
|
|
||||||
if use_msgpack:
|
|
||||||
with stats_timing(
|
|
||||||
"sqllab.query.results_backend_msgpack_deserialize", stats_logger
|
|
||||||
):
|
|
||||||
ds_payload = msgpack.loads(payload, raw=False)
|
|
||||||
|
|
||||||
with stats_timing("sqllab.query.results_backend_pa_deserialize", stats_logger):
|
|
||||||
pa_table = pa.deserialize(ds_payload["data"])
|
|
||||||
|
|
||||||
df = result_set.SupersetResultSet.convert_table_to_df(pa_table)
|
|
||||||
ds_payload["data"] = dataframe.df_to_records(df) or []
|
|
||||||
|
|
||||||
db_engine_spec = query.database.db_engine_spec
|
|
||||||
all_columns, data, expanded_columns = db_engine_spec.expand_data(
|
|
||||||
ds_payload["selected_columns"], ds_payload["data"]
|
|
||||||
)
|
|
||||||
ds_payload.update(
|
|
||||||
{"data": data, "columns": all_columns, "expanded_columns": expanded_columns}
|
|
||||||
)
|
|
||||||
|
|
||||||
return ds_payload
|
|
||||||
else:
|
|
||||||
with stats_timing(
|
|
||||||
"sqllab.query.results_backend_json_deserialize", stats_logger
|
|
||||||
):
|
|
||||||
return json.loads(payload)
|
|
||||||
|
|
||||||
|
|
||||||
def get_cta_schema_name(
|
|
||||||
database: Database, user: ab_models.User, schema: str, sql: str
|
|
||||||
) -> Optional[str]:
|
|
||||||
func: Optional[Callable[[Database, ab_models.User, str, str], str]] = config[
|
|
||||||
"SQLLAB_CTAS_SCHEMA_NAME_FUNC"
|
|
||||||
]
|
|
||||||
if not func:
|
|
||||||
return None
|
|
||||||
return func(database, user, schema, sql)
|
|
||||||
|
|
||||||
|
|
||||||
class AccessRequestsModelView(SupersetModelView, DeleteMixin):
|
|
||||||
datamodel = SQLAInterface(DAR)
|
|
||||||
include_route_methods = RouteMethod.CRUD_SET
|
|
||||||
list_columns = [
|
|
||||||
"username",
|
|
||||||
"user_roles",
|
|
||||||
"datasource_link",
|
|
||||||
"roles_with_datasource",
|
|
||||||
"created_on",
|
|
||||||
]
|
|
||||||
order_columns = ["created_on"]
|
|
||||||
base_order = ("changed_on", "desc")
|
|
||||||
label_columns = {
|
|
||||||
"username": _("User"),
|
|
||||||
"user_roles": _("User Roles"),
|
|
||||||
"database": _("Database URL"),
|
|
||||||
"datasource_link": _("Datasource"),
|
|
||||||
"roles_with_datasource": _("Roles to grant"),
|
|
||||||
"created_on": _("Created On"),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@talisman(force_https=False)
|
|
||||||
@app.route("/health")
|
|
||||||
def health() -> FlaskResponse:
|
|
||||||
return "OK"
|
|
||||||
|
|
||||||
|
|
||||||
@talisman(force_https=False)
|
|
||||||
@app.route("/healthcheck")
|
|
||||||
def healthcheck() -> FlaskResponse:
|
|
||||||
return "OK"
|
|
||||||
|
|
||||||
|
|
||||||
@talisman(force_https=False)
|
|
||||||
@app.route("/ping")
|
|
||||||
def ping() -> FlaskResponse:
|
|
||||||
return "OK"
|
|
||||||
|
|
||||||
|
|
||||||
class KV(BaseSupersetView):
|
|
||||||
|
|
||||||
"""Used for storing and retrieving key value pairs"""
|
|
||||||
|
|
||||||
@event_logger.log_this
|
|
||||||
@has_access_api
|
|
||||||
@expose("/store/", methods=["POST"])
|
|
||||||
def store(self) -> FlaskResponse:
|
|
||||||
try:
|
|
||||||
value = request.form.get("data")
|
|
||||||
obj = models.KeyValue(value=value)
|
|
||||||
db.session.add(obj)
|
|
||||||
db.session.commit()
|
|
||||||
except Exception as ex:
|
|
||||||
return json_error_response(utils.error_msg_from_exception(ex))
|
|
||||||
return Response(json.dumps({"id": obj.id}), status=200)
|
|
||||||
|
|
||||||
@event_logger.log_this
|
|
||||||
@has_access_api
|
|
||||||
@expose("/<int:key_id>/", methods=["GET"])
|
|
||||||
def get_value(self, key_id: int) -> FlaskResponse:
|
|
||||||
try:
|
|
||||||
kv = db.session.query(models.KeyValue).filter_by(id=key_id).scalar()
|
|
||||||
if not kv:
|
|
||||||
return Response(status=404, content_type="text/plain")
|
|
||||||
except Exception as ex:
|
|
||||||
return json_error_response(utils.error_msg_from_exception(ex))
|
|
||||||
return Response(kv.value, status=200, content_type="text/plain")
|
|
||||||
|
|
||||||
|
|
||||||
class R(BaseSupersetView):
|
|
||||||
|
|
||||||
"""used for short urls"""
|
|
||||||
|
|
||||||
@event_logger.log_this
|
|
||||||
@expose("/<int:url_id>")
|
|
||||||
def index(self, url_id: int) -> FlaskResponse:
|
|
||||||
url = db.session.query(models.Url).get(url_id)
|
|
||||||
if url and url.url:
|
|
||||||
explore_url = "//superset/explore/?"
|
|
||||||
if url.url.startswith(explore_url):
|
|
||||||
explore_url += f"r={url_id}"
|
|
||||||
return redirect(explore_url[1:])
|
|
||||||
else:
|
|
||||||
return redirect(url.url[1:])
|
|
||||||
else:
|
|
||||||
flash("URL to nowhere...", "danger")
|
|
||||||
return redirect("/")
|
|
||||||
|
|
||||||
@event_logger.log_this
|
|
||||||
@has_access_api
|
|
||||||
@expose("/shortner/", methods=["POST"])
|
|
||||||
def shortner(self) -> FlaskResponse:
|
|
||||||
url = request.form.get("data")
|
|
||||||
obj = models.Url(url=url)
|
|
||||||
db.session.add(obj)
|
|
||||||
db.session.commit()
|
|
||||||
return Response(
|
|
||||||
"{scheme}://{request.headers[Host]}/r/{obj.id}".format(
|
|
||||||
scheme=request.scheme, request=request, obj=obj
|
|
||||||
),
|
|
||||||
mimetype="text/plain",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class Superset(BaseSupersetView):
|
class Superset(BaseSupersetView):
|
||||||
"""The base views for Superset!"""
|
"""The base views for Superset!"""
|
||||||
|
|
@ -556,8 +307,9 @@ class Superset(BaseSupersetView):
|
||||||
)
|
)
|
||||||
|
|
||||||
if not requests:
|
if not requests:
|
||||||
flash(ACCESS_REQUEST_MISSING_ERR, "alert")
|
err = __("The access requests seem to have been deleted")
|
||||||
return json_error_response(ACCESS_REQUEST_MISSING_ERR)
|
flash(err, "alert")
|
||||||
|
return json_error_response(err)
|
||||||
|
|
||||||
# check if you can approve
|
# check if you can approve
|
||||||
if security_manager.can_access_all_datasources() or check_ownership(
|
if security_manager.can_access_all_datasources() or check_ownership(
|
||||||
|
|
@ -2113,10 +1865,12 @@ class Superset(BaseSupersetView):
|
||||||
def extra_table_metadata(
|
def extra_table_metadata(
|
||||||
self, database_id: int, table_name: str, schema: str
|
self, database_id: int, table_name: str, schema: str
|
||||||
) -> FlaskResponse:
|
) -> FlaskResponse:
|
||||||
schema = utils.parse_js_uri_path_item(schema, eval_undefined=True) # type: ignore
|
parsed_schema = utils.parse_js_uri_path_item(schema, eval_undefined=True)
|
||||||
table_name = utils.parse_js_uri_path_item(table_name) # type: ignore
|
table_name = utils.parse_js_uri_path_item(table_name) # type: ignore
|
||||||
mydb = db.session.query(models.Database).filter_by(id=database_id).one()
|
mydb = db.session.query(models.Database).filter_by(id=database_id).one()
|
||||||
payload = mydb.db_engine_spec.extra_table_metadata(mydb, table_name, schema)
|
payload = mydb.db_engine_spec.extra_table_metadata(
|
||||||
|
mydb, table_name, parsed_schema
|
||||||
|
)
|
||||||
return json_success(json.dumps(payload))
|
return json_success(json.dumps(payload))
|
||||||
|
|
||||||
@has_access
|
@has_access
|
||||||
|
|
@ -2913,38 +2667,3 @@ class Superset(BaseSupersetView):
|
||||||
"Failed to fetch schemas allowed for csv upload in this database! "
|
"Failed to fetch schemas allowed for csv upload in this database! "
|
||||||
"Please contact your Superset Admin!"
|
"Please contact your Superset Admin!"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class CssTemplateModelView(SupersetModelView, DeleteMixin):
|
|
||||||
datamodel = SQLAInterface(models.CssTemplate)
|
|
||||||
include_route_methods = RouteMethod.CRUD_SET
|
|
||||||
|
|
||||||
list_title = _("CSS Templates")
|
|
||||||
show_title = _("Show CSS Template")
|
|
||||||
add_title = _("Add CSS Template")
|
|
||||||
edit_title = _("Edit CSS Template")
|
|
||||||
|
|
||||||
list_columns = ["template_name"]
|
|
||||||
edit_columns = ["template_name", "css"]
|
|
||||||
add_columns = edit_columns
|
|
||||||
label_columns = {"template_name": _("Template Name")}
|
|
||||||
|
|
||||||
|
|
||||||
class CssTemplateAsyncModelView(CssTemplateModelView):
|
|
||||||
include_route_methods = {RouteMethod.API_READ}
|
|
||||||
list_columns = ["template_name", "css"]
|
|
||||||
|
|
||||||
|
|
||||||
@app.after_request
|
|
||||||
def apply_http_headers(response: Response) -> Response:
|
|
||||||
"""Applies the configuration's http headers to all responses"""
|
|
||||||
|
|
||||||
# HTTP_HEADERS is deprecated, this provides backwards compatibility
|
|
||||||
response.headers.extend( # type: ignore
|
|
||||||
{**config["OVERRIDE_HTTP_HEADERS"], **config["HTTP_HEADERS"]}
|
|
||||||
)
|
|
||||||
|
|
||||||
for k, v in config["DEFAULT_HTTP_HEADERS"].items():
|
|
||||||
if k not in response.headers:
|
|
||||||
response.headers[k] = v
|
|
||||||
return response
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
# 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.
|
||||||
|
from flask_appbuilder.models.sqla.interface import SQLAInterface
|
||||||
|
from flask_babel import lazy_gettext as _
|
||||||
|
|
||||||
|
from superset.constants import RouteMethod
|
||||||
|
from superset.models import core as models
|
||||||
|
from superset.views.base import DeleteMixin, SupersetModelView
|
||||||
|
|
||||||
|
|
||||||
|
class CssTemplateModelView( # pylint: disable=too-many-ancestors
|
||||||
|
SupersetModelView, DeleteMixin
|
||||||
|
):
|
||||||
|
datamodel = SQLAInterface(models.CssTemplate)
|
||||||
|
include_route_methods = RouteMethod.CRUD_SET
|
||||||
|
|
||||||
|
list_title = _("CSS Templates")
|
||||||
|
show_title = _("Show CSS Template")
|
||||||
|
add_title = _("Add CSS Template")
|
||||||
|
edit_title = _("Edit CSS Template")
|
||||||
|
|
||||||
|
list_columns = ["template_name"]
|
||||||
|
edit_columns = ["template_name", "css"]
|
||||||
|
add_columns = edit_columns
|
||||||
|
label_columns = {"template_name": _("Template Name")}
|
||||||
|
|
||||||
|
|
||||||
|
class CssTemplateAsyncModelView( # pylint: disable=too-many-ancestors
|
||||||
|
CssTemplateModelView
|
||||||
|
):
|
||||||
|
include_route_methods = {RouteMethod.API_READ}
|
||||||
|
list_columns = ["template_name", "css"]
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
# 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.
|
||||||
|
from superset import app, talisman
|
||||||
|
from superset.typing import FlaskResponse
|
||||||
|
|
||||||
|
|
||||||
|
@talisman(force_https=False)
|
||||||
|
@app.route("/ping")
|
||||||
|
def ping() -> FlaskResponse:
|
||||||
|
return "OK"
|
||||||
|
|
||||||
|
|
||||||
|
@talisman(force_https=False)
|
||||||
|
@app.route("/healthcheck")
|
||||||
|
def healthcheck() -> FlaskResponse:
|
||||||
|
return "OK"
|
||||||
|
|
||||||
|
|
||||||
|
@talisman(force_https=False)
|
||||||
|
@app.route("/health")
|
||||||
|
def health() -> FlaskResponse:
|
||||||
|
return "OK"
|
||||||
|
|
@ -0,0 +1,56 @@
|
||||||
|
# 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 simplejson as json
|
||||||
|
from flask import request, Response
|
||||||
|
from flask_appbuilder import expose
|
||||||
|
from flask_appbuilder.security.decorators import has_access_api
|
||||||
|
|
||||||
|
from superset import db, event_logger
|
||||||
|
from superset.models import core as models
|
||||||
|
from superset.typing import FlaskResponse
|
||||||
|
from superset.utils import core as utils
|
||||||
|
from superset.views.base import BaseSupersetView, json_error_response
|
||||||
|
|
||||||
|
|
||||||
|
class KV(BaseSupersetView):
|
||||||
|
|
||||||
|
"""Used for storing and retrieving key value pairs"""
|
||||||
|
|
||||||
|
@event_logger.log_this
|
||||||
|
@has_access_api
|
||||||
|
@expose("/store/", methods=["POST"])
|
||||||
|
def store(self) -> FlaskResponse: # pylint: disable=no-self-use
|
||||||
|
try:
|
||||||
|
value = request.form.get("data")
|
||||||
|
obj = models.KeyValue(value=value)
|
||||||
|
db.session.add(obj)
|
||||||
|
db.session.commit()
|
||||||
|
except Exception as ex: # pylint: disable=broad-except
|
||||||
|
return json_error_response(utils.error_msg_from_exception(ex))
|
||||||
|
return Response(json.dumps({"id": obj.id}), status=200)
|
||||||
|
|
||||||
|
@event_logger.log_this
|
||||||
|
@has_access_api
|
||||||
|
@expose("/<int:key_id>/", methods=["GET"])
|
||||||
|
def get_value(self, key_id: int) -> FlaskResponse: # pylint: disable=no-self-use
|
||||||
|
try:
|
||||||
|
kv = db.session.query(models.KeyValue).filter_by(id=key_id).scalar()
|
||||||
|
if not kv:
|
||||||
|
return Response(status=404, content_type="text/plain")
|
||||||
|
except Exception as ex: # pylint: disable=broad-except
|
||||||
|
return json_error_response(utils.error_msg_from_exception(ex))
|
||||||
|
return Response(kv.value, status=200, content_type="text/plain")
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
# 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.
|
||||||
|
from flask import flash, request, Response
|
||||||
|
from flask_appbuilder import expose
|
||||||
|
from flask_appbuilder.security.decorators import has_access_api
|
||||||
|
from werkzeug.utils import redirect
|
||||||
|
|
||||||
|
from superset import db, event_logger
|
||||||
|
from superset.models import core as models
|
||||||
|
from superset.typing import FlaskResponse
|
||||||
|
from superset.views.base import BaseSupersetView
|
||||||
|
|
||||||
|
|
||||||
|
class R(BaseSupersetView): # pylint: disable=invalid-name
|
||||||
|
|
||||||
|
"""used for short urls"""
|
||||||
|
|
||||||
|
@event_logger.log_this
|
||||||
|
@expose("/<int:url_id>")
|
||||||
|
def index(self, url_id: int) -> FlaskResponse: # pylint: disable=no-self-use
|
||||||
|
url = db.session.query(models.Url).get(url_id)
|
||||||
|
if url and url.url:
|
||||||
|
explore_url = "//superset/explore/?"
|
||||||
|
if url.url.startswith(explore_url):
|
||||||
|
explore_url += f"r={url_id}"
|
||||||
|
return redirect(explore_url[1:])
|
||||||
|
|
||||||
|
return redirect(url.url[1:])
|
||||||
|
|
||||||
|
flash("URL to nowhere...", "danger")
|
||||||
|
return redirect("/")
|
||||||
|
|
||||||
|
@event_logger.log_this
|
||||||
|
@has_access_api
|
||||||
|
@expose("/shortner/", methods=["POST"])
|
||||||
|
def shortner(self) -> FlaskResponse: # pylint: disable=no-self-use
|
||||||
|
url = request.form.get("data")
|
||||||
|
obj = models.Url(url=url)
|
||||||
|
db.session.add(obj)
|
||||||
|
db.session.commit()
|
||||||
|
return Response(
|
||||||
|
"{scheme}://{request.headers[Host]}/r/{obj.id}".format(
|
||||||
|
scheme=request.scheme, request=request, obj=obj
|
||||||
|
),
|
||||||
|
mimetype="text/plain",
|
||||||
|
)
|
||||||
|
|
@ -14,35 +14,53 @@
|
||||||
# KIND, either express or implied. See the License for the
|
# KIND, either express or implied. See the License for the
|
||||||
# specific language governing permissions and limitations
|
# specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
import logging
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from datetime import date
|
from datetime import date
|
||||||
from typing import Any, DefaultDict, Dict, List, Optional, Set, Tuple
|
from typing import Any, Callable, DefaultDict, Dict, List, Optional, Set, Tuple, Union
|
||||||
from urllib import parse
|
from urllib import parse
|
||||||
|
|
||||||
|
import msgpack
|
||||||
|
import pyarrow as pa
|
||||||
import simplejson as json
|
import simplejson as json
|
||||||
from flask import g, request
|
from flask import g, request
|
||||||
|
from flask_appbuilder.security.sqla import models as ab_models
|
||||||
from flask_appbuilder.security.sqla.models import User
|
from flask_appbuilder.security.sqla.models import User
|
||||||
|
|
||||||
import superset.models.core as models
|
import superset.models.core as models
|
||||||
from superset import app, db, is_feature_enabled
|
from superset import (
|
||||||
|
app,
|
||||||
|
dataframe,
|
||||||
|
db,
|
||||||
|
is_feature_enabled,
|
||||||
|
result_set,
|
||||||
|
security_manager,
|
||||||
|
)
|
||||||
from superset.connectors.connector_registry import ConnectorRegistry
|
from superset.connectors.connector_registry import ConnectorRegistry
|
||||||
from superset.exceptions import SupersetException
|
from superset.errors import ErrorLevel, SupersetError, SupersetErrorType
|
||||||
|
from superset.exceptions import SupersetException, SupersetSecurityException
|
||||||
from superset.legacy import update_time_range
|
from superset.legacy import update_time_range
|
||||||
|
from superset.models.core import Database
|
||||||
from superset.models.dashboard import Dashboard
|
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
|
||||||
from superset.typing import FormData
|
from superset.typing import FormData
|
||||||
from superset.utils.core import QueryStatus, TimeRangeEndpoint
|
from superset.utils.core import QueryStatus, TimeRangeEndpoint
|
||||||
|
from superset.utils.decorators import stats_timing
|
||||||
from superset.viz import BaseViz
|
from superset.viz import BaseViz
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
stats_logger = app.config["STATS_LOGGER"]
|
||||||
|
|
||||||
if is_feature_enabled("SIP_38_VIZ_REARCHITECTURE"):
|
if is_feature_enabled("SIP_38_VIZ_REARCHITECTURE"):
|
||||||
from superset import viz_sip38 as viz
|
from superset import viz_sip38 as viz
|
||||||
else:
|
else:
|
||||||
from superset import viz # type: ignore
|
from superset import viz # type: ignore
|
||||||
|
|
||||||
|
|
||||||
FORM_DATA_KEY_BLACKLIST: List[str] = []
|
REJECTED_FORM_DATA_KEYS: List[str] = []
|
||||||
if not app.config["ENABLE_JAVASCRIPT_CONTROLS"]:
|
if not app.config["ENABLE_JAVASCRIPT_CONTROLS"]:
|
||||||
FORM_DATA_KEY_BLACKLIST = ["js_tooltip", "js_onclick_href", "js_data_mutator"]
|
REJECTED_FORM_DATA_KEYS = ["js_tooltip", "js_onclick_href", "js_data_mutator"]
|
||||||
|
|
||||||
|
|
||||||
def bootstrap_user_data(user: User, include_perms: bool = False) -> Dict[str, Any]:
|
def bootstrap_user_data(user: User, include_perms: bool = False) -> Dict[str, Any]:
|
||||||
|
|
@ -91,7 +109,7 @@ def get_permissions(
|
||||||
|
|
||||||
|
|
||||||
def get_viz(
|
def get_viz(
|
||||||
form_data: FormData, datasource_type: str, datasource_id: int, force: bool = False,
|
form_data: FormData, datasource_type: str, datasource_id: int, force: bool = False
|
||||||
) -> BaseViz:
|
) -> BaseViz:
|
||||||
viz_type = form_data.get("viz_type", "table")
|
viz_type = form_data.get("viz_type", "table")
|
||||||
datasource = ConnectorRegistry.get_datasource(
|
datasource = ConnectorRegistry.get_datasource(
|
||||||
|
|
@ -129,7 +147,7 @@ def get_form_data(
|
||||||
url_form_data.update(form_data)
|
url_form_data.update(form_data)
|
||||||
form_data = url_form_data
|
form_data = url_form_data
|
||||||
|
|
||||||
form_data = {k: v for k, v in form_data.items() if k not in FORM_DATA_KEY_BLACKLIST}
|
form_data = {k: v for k, v in form_data.items() if k not in REJECTED_FORM_DATA_KEYS}
|
||||||
|
|
||||||
# When a slice_id is present, load from DB and override
|
# When a slice_id is present, load from DB and override
|
||||||
# the form_data from the DB with the other form_data provided
|
# the form_data from the DB with the other form_data provided
|
||||||
|
|
@ -160,7 +178,7 @@ def get_form_data(
|
||||||
|
|
||||||
|
|
||||||
def get_datasource_info(
|
def get_datasource_info(
|
||||||
datasource_id: Optional[int], datasource_type: Optional[str], form_data: FormData,
|
datasource_id: Optional[int], datasource_type: Optional[str], form_data: FormData
|
||||||
) -> Tuple[int, Optional[str]]:
|
) -> Tuple[int, Optional[str]]:
|
||||||
"""
|
"""
|
||||||
Compatibility layer for handling of datasource info
|
Compatibility layer for handling of datasource info
|
||||||
|
|
@ -222,7 +240,7 @@ def apply_display_max_row_limit(
|
||||||
|
|
||||||
|
|
||||||
def get_time_range_endpoints(
|
def get_time_range_endpoints(
|
||||||
form_data: FormData, slc: Optional[Slice] = None, slice_id: Optional[int] = None,
|
form_data: FormData, slc: Optional[Slice] = None, slice_id: Optional[int] = None
|
||||||
) -> Optional[Tuple[TimeRangeEndpoint, TimeRangeEndpoint]]:
|
) -> Optional[Tuple[TimeRangeEndpoint, TimeRangeEndpoint]]:
|
||||||
"""
|
"""
|
||||||
Get the slice aware time range endpoints from the form-data falling back to the SQL
|
Get the slice aware time range endpoints from the form-data falling back to the SQL
|
||||||
|
|
@ -361,3 +379,122 @@ def is_slice_in_container(
|
||||||
)
|
)
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def is_owner(obj: Union[Dashboard, Slice], user: User) -> bool:
|
||||||
|
""" Check if user is owner of the slice """
|
||||||
|
return obj and user in obj.owners
|
||||||
|
|
||||||
|
|
||||||
|
def check_datasource_perms(
|
||||||
|
_self: Any,
|
||||||
|
datasource_type: Optional[str] = None,
|
||||||
|
datasource_id: Optional[int] = None,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Check if user can access a cached response from explore_json.
|
||||||
|
|
||||||
|
This function takes `self` since it must have the same signature as the
|
||||||
|
the decorated method.
|
||||||
|
|
||||||
|
:param datasource_type: The datasource type, i.e., 'druid' or 'table'
|
||||||
|
:param datasource_id: The datasource ID
|
||||||
|
:raises SupersetSecurityException: If the user cannot access the resource
|
||||||
|
"""
|
||||||
|
|
||||||
|
form_data = get_form_data()[0]
|
||||||
|
|
||||||
|
try:
|
||||||
|
datasource_id, datasource_type = get_datasource_info(
|
||||||
|
datasource_id, datasource_type, form_data
|
||||||
|
)
|
||||||
|
except SupersetException as ex:
|
||||||
|
raise SupersetSecurityException(
|
||||||
|
SupersetError(
|
||||||
|
error_type=SupersetErrorType.FAILED_FETCHING_DATASOURCE_INFO_ERROR,
|
||||||
|
level=ErrorLevel.ERROR,
|
||||||
|
message=str(ex),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if datasource_type is None:
|
||||||
|
raise SupersetSecurityException(
|
||||||
|
SupersetError(
|
||||||
|
error_type=SupersetErrorType.UNKNOWN_DATASOURCE_TYPE_ERROR,
|
||||||
|
level=ErrorLevel.ERROR,
|
||||||
|
message="Could not determine datasource type",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
viz_obj = get_viz(
|
||||||
|
datasource_type=datasource_type,
|
||||||
|
datasource_id=datasource_id,
|
||||||
|
form_data=form_data,
|
||||||
|
force=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
security_manager.assert_viz_permission(viz_obj)
|
||||||
|
|
||||||
|
|
||||||
|
def check_slice_perms(_self: Any, slice_id: int) -> None:
|
||||||
|
"""
|
||||||
|
Check if user can access a cached response from slice_json.
|
||||||
|
|
||||||
|
This function takes `self` since it must have the same signature as the
|
||||||
|
the decorated method.
|
||||||
|
"""
|
||||||
|
|
||||||
|
form_data, slc = get_form_data(slice_id, use_slice_data=True)
|
||||||
|
|
||||||
|
if slc:
|
||||||
|
viz_obj = get_viz(
|
||||||
|
datasource_type=slc.datasource.type,
|
||||||
|
datasource_id=slc.datasource.id,
|
||||||
|
form_data=form_data,
|
||||||
|
force=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
security_manager.assert_viz_permission(viz_obj)
|
||||||
|
|
||||||
|
|
||||||
|
def _deserialize_results_payload(
|
||||||
|
payload: Union[bytes, str], query: Query, use_msgpack: Optional[bool] = False
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
logger.debug(f"Deserializing from msgpack: {use_msgpack}")
|
||||||
|
if use_msgpack:
|
||||||
|
with stats_timing(
|
||||||
|
"sqllab.query.results_backend_msgpack_deserialize", stats_logger
|
||||||
|
):
|
||||||
|
ds_payload = msgpack.loads(payload, raw=False)
|
||||||
|
|
||||||
|
with stats_timing("sqllab.query.results_backend_pa_deserialize", stats_logger):
|
||||||
|
pa_table = pa.deserialize(ds_payload["data"])
|
||||||
|
|
||||||
|
df = result_set.SupersetResultSet.convert_table_to_df(pa_table)
|
||||||
|
ds_payload["data"] = dataframe.df_to_records(df) or []
|
||||||
|
|
||||||
|
db_engine_spec = query.database.db_engine_spec
|
||||||
|
all_columns, data, expanded_columns = db_engine_spec.expand_data(
|
||||||
|
ds_payload["selected_columns"], ds_payload["data"]
|
||||||
|
)
|
||||||
|
ds_payload.update(
|
||||||
|
{"data": data, "columns": all_columns, "expanded_columns": expanded_columns}
|
||||||
|
)
|
||||||
|
|
||||||
|
return ds_payload
|
||||||
|
else:
|
||||||
|
with stats_timing(
|
||||||
|
"sqllab.query.results_backend_json_deserialize", stats_logger
|
||||||
|
):
|
||||||
|
return json.loads(payload)
|
||||||
|
|
||||||
|
|
||||||
|
def get_cta_schema_name(
|
||||||
|
database: Database, user: ab_models.User, schema: str, sql: str
|
||||||
|
) -> Optional[str]:
|
||||||
|
func: Optional[Callable[[Database, ab_models.User, str, str], str]] = app.config[
|
||||||
|
"SQLLAB_CTAS_SCHEMA_NAME_FUNC"
|
||||||
|
]
|
||||||
|
if not func:
|
||||||
|
return None
|
||||||
|
return func(database, user, schema, sql)
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,7 @@ from unittest import mock, skipUnless
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
import sqlalchemy as sqla
|
import sqlalchemy as sqla
|
||||||
|
|
||||||
|
import superset.views.utils
|
||||||
from tests.test_app import app
|
from tests.test_app import app
|
||||||
from superset import (
|
from superset import (
|
||||||
dataframe,
|
dataframe,
|
||||||
|
|
@ -1106,7 +1107,7 @@ class CoreTests(SupersetTestCase):
|
||||||
self.assertIsInstance(serialized_payload, str)
|
self.assertIsInstance(serialized_payload, str)
|
||||||
|
|
||||||
query_mock = mock.Mock()
|
query_mock = mock.Mock()
|
||||||
deserialized_payload = views._deserialize_results_payload(
|
deserialized_payload = superset.views.utils._deserialize_results_payload(
|
||||||
serialized_payload, query_mock, use_new_deserialization
|
serialized_payload, query_mock, use_new_deserialization
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -1159,7 +1160,7 @@ class CoreTests(SupersetTestCase):
|
||||||
query_mock = mock.Mock()
|
query_mock = mock.Mock()
|
||||||
query_mock.database.db_engine_spec.expand_data = expand_data
|
query_mock.database.db_engine_spec.expand_data = expand_data
|
||||||
|
|
||||||
deserialized_payload = views._deserialize_results_payload(
|
deserialized_payload = superset.views.utils._deserialize_results_payload(
|
||||||
serialized_payload, query_mock, use_new_deserialization
|
serialized_payload, query_mock, use_new_deserialization
|
||||||
)
|
)
|
||||||
df = results.to_pandas_df()
|
df = results.to_pandas_df()
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue