# 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 __future__ import annotations import logging import os from typing import Any, Callable, Dict, TYPE_CHECKING import wtforms_json from deprecation import deprecated from flask import Flask, redirect from flask_appbuilder import expose, IndexView from flask_babel import gettext as __, lazy_gettext as _ from flask_compress import Compress from werkzeug.middleware.proxy_fix import ProxyFix from superset.connectors.connector_registry import ConnectorRegistry from superset.extensions import ( _event_logger, APP_DIR, appbuilder, async_query_manager, cache_manager, celery_app, csrf, db, encrypted_field_factory, feature_flag_manager, machine_auth_provider_factory, manifest_processor, migrate, profiling, results_backend_manager, talisman, ) from superset.security import SupersetSecurityManager from superset.typing import FlaskResponse from superset.utils.core import pessimistic_connection_handling from superset.utils.log import DBEventLogger, get_event_logger_from_cfg_value if TYPE_CHECKING: from superset.app import SupersetApp logger = logging.getLogger(__name__) # pylint: disable=R0904 class SupersetAppInitializer: def __init__(self, app: SupersetApp) -> None: super().__init__() self.superset_app = app self.config = app.config self.manifest: Dict[Any, Any] = {} @deprecated(details="use self.superset_app instead of self.flask_app") # type: ignore # pylint: disable=line-too-long,useless-suppression @property def flask_app(self) -> SupersetApp: return self.superset_app def pre_init(self) -> None: """ Called before all other init tasks are complete """ wtforms_json.init() if not os.path.exists(self.config["DATA_DIR"]): os.makedirs(self.config["DATA_DIR"]) def post_init(self) -> None: """ Called after any other init tasks """ def configure_celery(self) -> None: celery_app.config_from_object(self.config["CELERY_CONFIG"]) celery_app.set_default() superset_app = self.superset_app # Here, we want to ensure that every call into Celery task has an app context # setup properly task_base = celery_app.Task class AppContextTask(task_base): # type: ignore # pylint: disable=too-few-public-methods abstract = True # Grab each call into the task and set up an app context def __call__(self, *args: Any, **kwargs: Any) -> Any: with superset_app.app_context(): return task_base.__call__(self, *args, **kwargs) celery_app.Task = AppContextTask def init_views(self) -> None: # # We're doing local imports, as several of them import # models which in turn try to import # the global Flask app # # pylint: disable=import-outside-toplevel,too-many-locals,too-many-statements from superset.annotation_layers.api import AnnotationLayerRestApi from superset.annotation_layers.annotations.api import AnnotationRestApi from superset.async_events.api import AsyncEventsRestApi from superset.cachekeys.api import CacheRestApi from superset.charts.api import ChartRestApi from superset.connectors.druid.views import ( Druid, DruidClusterModelView, DruidColumnInlineView, DruidDatasourceModelView, DruidMetricInlineView, ) from superset.connectors.sqla.views import ( RowLevelSecurityFiltersModelView, SqlMetricInlineView, TableColumnInlineView, TableModelView, ) from superset.css_templates.api import CssTemplateRestApi from superset.dashboards.api import DashboardRestApi from superset.databases.api import DatabaseRestApi from superset.datasets.api import DatasetRestApi from superset.datasets.columns.api import DatasetColumnsRestApi from superset.datasets.metrics.api import DatasetMetricRestApi from superset.queries.api import QueryRestApi from superset.security.api import SecurityRestApi from superset.queries.saved_queries.api import SavedQueryRestApi from superset.reports.api import ReportScheduleRestApi from superset.reports.logs.api import ReportExecutionLogRestApi from superset.views.access_requests import AccessRequestsModelView from superset.views.alerts import ( AlertLogModelView, AlertModelView, AlertObservationModelView, AlertView, ReportView, ) from superset.views.annotations import ( AnnotationLayerModelView, AnnotationModelView, ) from superset.views.api import Api from superset.views.chart.views import SliceAsync, SliceModelView from superset.views.core import Superset from superset.views.css_templates import ( CssTemplateAsyncModelView, CssTemplateModelView, ) from superset.views.dashboard.views import ( Dashboard, DashboardModelView, DashboardModelViewAsync, ) from superset.views.database.views import ( ColumnarToDatabaseView, CsvToDatabaseView, DatabaseView, ExcelToDatabaseView, ) from superset.views.datasource.views import Datasource from superset.views.dynamic_plugins import DynamicPluginsView from superset.views.key_value import KV from superset.views.log.api import LogRestApi from superset.views.log.views import LogModelView from superset.views.redirects import R from superset.views.schedules import ( DashboardEmailScheduleView, SliceEmailScheduleView, ) from superset.views.sql_lab import ( SavedQueryView, SavedQueryViewApi, SqlLab, TableSchemaView, TabStateView, ) from superset.views.tags import TagView # # Setup API views # appbuilder.add_api(AnnotationRestApi) appbuilder.add_api(AnnotationLayerRestApi) appbuilder.add_api(AsyncEventsRestApi) appbuilder.add_api(CacheRestApi) appbuilder.add_api(ChartRestApi) appbuilder.add_api(CssTemplateRestApi) appbuilder.add_api(DashboardRestApi) appbuilder.add_api(DatabaseRestApi) appbuilder.add_api(DatasetRestApi) appbuilder.add_api(DatasetColumnsRestApi) appbuilder.add_api(DatasetMetricRestApi) appbuilder.add_api(QueryRestApi) appbuilder.add_api(SavedQueryRestApi) appbuilder.add_api(ReportScheduleRestApi) appbuilder.add_api(ReportExecutionLogRestApi) # # Setup regular views # appbuilder.add_link( "Home", label=__("Home"), href="/superset/welcome/", cond=lambda: bool(appbuilder.app.config["LOGO_TARGET_PATH"]), ) appbuilder.add_view( AnnotationLayerModelView, "Annotation Layers", label=__("Annotation Layers"), icon="fa-comment", category="Manage", category_label=__("Manage"), category_icon="", ) appbuilder.add_view( DashboardModelView, "Dashboards", label=__("Dashboards"), icon="fa-dashboard", category="", category_icon="", ) appbuilder.add_view( SliceModelView, "Charts", label=__("Charts"), icon="fa-bar-chart", category="", category_icon="", ) appbuilder.add_view( DynamicPluginsView, "Plugins", label=__("Plugins"), category="Manage", category_label=__("Manage"), icon="fa-puzzle-piece", menu_cond=lambda: feature_flag_manager.is_feature_enabled( "DYNAMIC_PLUGINS" ), ) appbuilder.add_view( CssTemplateModelView, "CSS Templates", label=__("CSS Templates"), icon="fa-css3", category="Manage", category_label=__("Manage"), category_icon="", ) appbuilder.add_view( RowLevelSecurityFiltersModelView, "Row Level Security", label=__("Row level security"), category="Security", category_label=__("Security"), icon="fa-lock", menu_cond=lambda: feature_flag_manager.is_feature_enabled( "ROW_LEVEL_SECURITY" ), ) # # Setup views with no menu # appbuilder.add_view_no_menu(Api) appbuilder.add_view_no_menu(CssTemplateAsyncModelView) appbuilder.add_view_no_menu(CsvToDatabaseView) appbuilder.add_view_no_menu(ExcelToDatabaseView) appbuilder.add_view_no_menu(ColumnarToDatabaseView) appbuilder.add_view_no_menu(Dashboard) appbuilder.add_view_no_menu(DashboardModelViewAsync) appbuilder.add_view_no_menu(Datasource) appbuilder.add_view_no_menu(KV) appbuilder.add_view_no_menu(R) appbuilder.add_view_no_menu(SavedQueryView) appbuilder.add_view_no_menu(SavedQueryViewApi) appbuilder.add_view_no_menu(SliceAsync) appbuilder.add_view_no_menu(SqlLab) appbuilder.add_view_no_menu(SqlMetricInlineView) appbuilder.add_view_no_menu(AnnotationModelView) appbuilder.add_view_no_menu(Superset) appbuilder.add_view_no_menu(TableColumnInlineView) appbuilder.add_view_no_menu(TableModelView) appbuilder.add_view_no_menu(TableSchemaView) appbuilder.add_view_no_menu(TabStateView) appbuilder.add_view_no_menu(TagView) # # Add links # appbuilder.add_link( "Import Dashboards", label=__("Import Dashboards"), href="/superset/import_dashboards/", icon="fa-cloud-upload", category="Manage", category_label=__("Manage"), category_icon="fa-wrench", cond=lambda: not feature_flag_manager.is_feature_enabled( "VERSIONED_EXPORT" ), ) appbuilder.add_link( "SQL Editor", label=_("SQL Editor"), href="/superset/sqllab/", category_icon="fa-flask", icon="fa-flask", category="SQL Lab", category_label=__("SQL Lab"), ) appbuilder.add_link( __("Saved Queries"), href="/savedqueryview/list/", icon="fa-save", category="SQL Lab", ) appbuilder.add_link( "Query Search", label=_("Query History"), href="/superset/sqllab/history/", icon="fa-search", category_icon="fa-flask", category="SQL Lab", category_label=__("SQL Lab"), ) appbuilder.add_view( DatabaseView, "Databases", label=__("Databases"), icon="fa-database", category="Data", category_label=__("Data"), category_icon="fa-database", ) appbuilder.add_link( "Datasets", label=__("Datasets"), href="/tablemodelview/list/", icon="fa-table", category="Data", category_label=__("Data"), category_icon="fa-table", ) appbuilder.add_separator("Data") appbuilder.add_link( "Upload a CSV", label=__("Upload a CSV"), href="/csvtodatabaseview/form", icon="fa-upload", category="Data", category_label=__("Data"), category_icon="fa-wrench", cond=lambda: bool( self.config["CSV_EXTENSIONS"].intersection( self.config["ALLOWED_EXTENSIONS"] ) ), ) appbuilder.add_link( "Upload a Columnar file", label=__("Upload a Columnar file"), href="/columnartodatabaseview/form", icon="fa-upload", category="Data", category_label=__("Data"), category_icon="fa-wrench", cond=lambda: bool( self.config["COLUMNAR_EXTENSIONS"].intersection( self.config["ALLOWED_EXTENSIONS"] ) ), ) try: import xlrd # pylint: disable=unused-import appbuilder.add_link( "Upload Excel", label=__("Upload Excel"), href="/exceltodatabaseview/form", icon="fa-upload", category="Data", category_label=__("Data"), category_icon="fa-wrench", cond=lambda: bool( self.config["EXCEL_EXTENSIONS"].intersection( self.config["ALLOWED_EXTENSIONS"] ) ), ) except ImportError: pass appbuilder.add_api(LogRestApi) appbuilder.add_view( LogModelView, "Action Log", label=__("Action Log"), category="Security", category_label=__("Security"), icon="fa-list-ol", menu_cond=lambda: ( self.config["FAB_ADD_SECURITY_VIEWS"] and self.config["SUPERSET_LOG_VIEW"] ), ) appbuilder.add_api(SecurityRestApi) # # Conditionally setup email views # if self.config["ENABLE_SCHEDULED_EMAIL_REPORTS"]: logging.warning( "ENABLE_SCHEDULED_EMAIL_REPORTS " "is deprecated and will be removed in version 2.0.0" ) appbuilder.add_separator( "Manage", cond=lambda: self.config["ENABLE_SCHEDULED_EMAIL_REPORTS"] ) appbuilder.add_view( DashboardEmailScheduleView, "Dashboard Email Schedules", label=__("Dashboard Emails"), category="Manage", category_label=__("Manage"), icon="fa-search", menu_cond=lambda: self.config["ENABLE_SCHEDULED_EMAIL_REPORTS"], ) appbuilder.add_view( SliceEmailScheduleView, "Chart Emails", label=__("Chart Email Schedules"), category="Manage", category_label=__("Manage"), icon="fa-search", menu_cond=lambda: self.config["ENABLE_SCHEDULED_EMAIL_REPORTS"], ) if self.config["ENABLE_ALERTS"]: logging.warning( "ENABLE_ALERTS is deprecated and will be removed in version 2.0.0" ) appbuilder.add_view( AlertModelView, "Alerts", label=__("Alerts"), category="Manage", category_label=__("Manage"), icon="fa-exclamation-triangle", menu_cond=lambda: bool(self.config["ENABLE_ALERTS"]), ) appbuilder.add_view_no_menu(AlertLogModelView) appbuilder.add_view_no_menu(AlertObservationModelView) appbuilder.add_view( AlertView, "Alerts & Report", label=__("Alerts & Reports"), category="Manage", category_label=__("Manage"), icon="fa-exclamation-triangle", menu_cond=lambda: feature_flag_manager.is_feature_enabled("ALERT_REPORTS"), ) appbuilder.add_view_no_menu(ReportView) appbuilder.add_view( AccessRequestsModelView, "Access requests", label=__("Access requests"), category="Security", category_label=__("Security"), icon="fa-table", menu_cond=lambda: bool(self.config["ENABLE_ACCESS_REQUEST"]), ) # # Druid Views # appbuilder.add_separator( "Data", cond=lambda: bool(self.config["DRUID_IS_ACTIVE"]) ) appbuilder.add_view( DruidDatasourceModelView, "Druid Datasources", label=__("Druid Datasources"), category="Data", category_label=__("Data"), icon="fa-cube", menu_cond=lambda: bool(self.config["DRUID_IS_ACTIVE"]), ) appbuilder.add_view( DruidClusterModelView, name="Druid Clusters", label=__("Druid Clusters"), icon="fa-cubes", category="Data", category_label=__("Data"), category_icon="fa-database", menu_cond=lambda: bool(self.config["DRUID_IS_ACTIVE"]), ) appbuilder.add_view_no_menu(DruidMetricInlineView) appbuilder.add_view_no_menu(DruidColumnInlineView) appbuilder.add_view_no_menu(Druid) appbuilder.add_link( "Scan New Datasources", label=__("Scan New Datasources"), href="/druid/scan_new_datasources/", category="Data", category_label=__("Data"), category_icon="fa-database", icon="fa-refresh", cond=lambda: bool( self.config["DRUID_IS_ACTIVE"] and self.config["DRUID_METADATA_LINKS_ENABLED"] ), ) appbuilder.add_link( "Refresh Druid Metadata", label=__("Refresh Druid Metadata"), href="/druid/refresh_datasources/", category="Data", category_label=__("Data"), category_icon="fa-database", icon="fa-cog", cond=lambda: bool( self.config["DRUID_IS_ACTIVE"] and self.config["DRUID_METADATA_LINKS_ENABLED"] ), ) appbuilder.add_separator( "Data", cond=lambda: bool(self.config["DRUID_IS_ACTIVE"]) ) def init_app_in_ctx(self) -> None: """ Runs init logic in the context of the app """ self.configure_fab() self.configure_url_map_converters() self.configure_data_sources() self.configure_auth_provider() self.configure_async_queries() # Hook that provides administrators a handle on the Flask APP # after initialization flask_app_mutator = self.config["FLASK_APP_MUTATOR"] if flask_app_mutator: flask_app_mutator(self.superset_app) self.init_views() def init_app(self) -> None: """ Main entry point which will delegate to other methods in order to fully init the app """ self.pre_init() # Configuration of logging must be done first to apply the formatter properly self.configure_logging() # Configuration of feature_flags must be done first to allow init features # conditionally self.configure_feature_flags() self.configure_db_encrypt() self.setup_db() self.configure_celery() self.enable_profiling() self.setup_event_logger() self.setup_bundle_manifest() self.register_blueprints() self.configure_wtf() self.configure_middlewares() self.configure_cache() with self.superset_app.app_context(): self.init_app_in_ctx() self.post_init() def configure_auth_provider(self) -> None: machine_auth_provider_factory.init_app(self.superset_app) def setup_event_logger(self) -> None: _event_logger["event_logger"] = get_event_logger_from_cfg_value( self.superset_app.config.get("EVENT_LOGGER", DBEventLogger()) ) def configure_data_sources(self) -> None: # Registering sources module_datasource_map = self.config["DEFAULT_MODULE_DS_MAP"] module_datasource_map.update(self.config["ADDITIONAL_MODULE_DS_MAP"]) ConnectorRegistry.register_sources(module_datasource_map) def configure_cache(self) -> None: cache_manager.init_app(self.superset_app) results_backend_manager.init_app(self.superset_app) def configure_feature_flags(self) -> None: feature_flag_manager.init_app(self.superset_app) def configure_fab(self) -> None: if self.config["SILENCE_FAB"]: logging.getLogger("flask_appbuilder").setLevel(logging.ERROR) custom_sm = self.config["CUSTOM_SECURITY_MANAGER"] or SupersetSecurityManager if not issubclass(custom_sm, SupersetSecurityManager): raise Exception( """Your CUSTOM_SECURITY_MANAGER must now extend SupersetSecurityManager, not FAB's security manager. See [4565] in UPDATING.md""" ) appbuilder.indexview = SupersetIndexView appbuilder.base_template = "superset/base.html" appbuilder.security_manager_class = custom_sm appbuilder.init_app(self.superset_app, db.session) def configure_url_map_converters(self) -> None: # # Doing local imports here as model importing causes a reference to # app.config to be invoked and we need the current_app to have been setup # # pylint: disable=import-outside-toplevel from superset.utils.url_map_converters import ( ObjectTypeConverter, RegexConverter, ) self.superset_app.url_map.converters["regex"] = RegexConverter self.superset_app.url_map.converters["object_type"] = ObjectTypeConverter def configure_middlewares(self) -> None: if self.config["ENABLE_CORS"]: # pylint: disable=import-outside-toplevel from flask_cors import CORS CORS(self.superset_app, **self.config["CORS_OPTIONS"]) if self.config["ENABLE_PROXY_FIX"]: self.superset_app.wsgi_app = ProxyFix( # type: ignore self.superset_app.wsgi_app, **self.config["PROXY_FIX_CONFIG"] ) if self.config["ENABLE_CHUNK_ENCODING"]: class ChunkedEncodingFix: # pylint: disable=too-few-public-methods def __init__(self, app: Flask) -> None: self.app = app def __call__( self, environ: Dict[str, Any], start_response: Callable[..., Any] ) -> Any: # Setting wsgi.input_terminated tells werkzeug.wsgi to ignore # content-length and read the stream till the end. if environ.get("HTTP_TRANSFER_ENCODING", "").lower() == "chunked": environ["wsgi.input_terminated"] = True return self.app(environ, start_response) self.superset_app.wsgi_app = ChunkedEncodingFix( # type: ignore self.superset_app.wsgi_app # type: ignore ) if self.config["UPLOAD_FOLDER"]: try: os.makedirs(self.config["UPLOAD_FOLDER"]) except OSError: pass for middleware in self.config["ADDITIONAL_MIDDLEWARE"]: self.superset_app.wsgi_app = middleware( # type: ignore self.superset_app.wsgi_app ) # Flask-Compress Compress(self.superset_app) if self.config["TALISMAN_ENABLED"]: talisman.init_app(self.superset_app, **self.config["TALISMAN_CONFIG"]) def configure_logging(self) -> None: self.config["LOGGING_CONFIGURATOR"].configure_logging( self.config, self.superset_app.debug ) def configure_db_encrypt(self) -> None: encrypted_field_factory.init_app(self.superset_app) def setup_db(self) -> None: db.init_app(self.superset_app) with self.superset_app.app_context(): pessimistic_connection_handling(db.engine) migrate.init_app(self.superset_app, db=db, directory=APP_DIR + "/migrations") def configure_wtf(self) -> None: if self.config["WTF_CSRF_ENABLED"]: csrf.init_app(self.superset_app) csrf_exempt_list = self.config["WTF_CSRF_EXEMPT_LIST"] for ex in csrf_exempt_list: csrf.exempt(ex) def configure_async_queries(self) -> None: if feature_flag_manager.is_feature_enabled("GLOBAL_ASYNC_QUERIES"): async_query_manager.init_app(self.superset_app) def register_blueprints(self) -> None: for bp in self.config["BLUEPRINTS"]: try: logger.info("Registering blueprint: %s", bp.name) self.superset_app.register_blueprint(bp) except Exception: # pylint: disable=broad-except logger.exception("blueprint registration failed") def setup_bundle_manifest(self) -> None: manifest_processor.init_app(self.superset_app) def enable_profiling(self) -> None: if self.config["PROFILING"]: profiling.init_app(self.superset_app) class SupersetIndexView(IndexView): @expose("/") def index(self) -> FlaskResponse: return redirect("/superset/welcome/")