# 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 contextlib import logging import os import sys from typing import Any, Callable, TYPE_CHECKING import wtforms_json from colorama import Fore, Style from deprecation import deprecated from flask import abort, current_app, Flask, redirect, request, session, url_for from flask_appbuilder import expose, IndexView from flask_appbuilder.api import safe from flask_appbuilder.utils.base import get_safe_redirect # using lazy_gettext since initialization happens prior to the request scope # and confuses flask-babel from flask_babel import lazy_gettext as _, refresh from flask_compress import Compress from flask_session import Session from sqlalchemy import text from werkzeug.middleware.proxy_fix import ProxyFix from superset.commands.database.exceptions import DatabaseInvalidError from superset.constants import ( CHANGE_ME_GLOBAL_ASYNC_QUERIES_JWT_SECRET, CHANGE_ME_GUEST_TOKEN_JWT_SECRET, CHANGE_ME_SECRET_KEY, ) from superset.databases.utils import make_url_safe from superset.extensions import ( _event_logger, APP_DIR, appbuilder, async_query_manager_factory, cache_manager, celery_app, csrf, db, encrypted_field_factory, feature_flag_manager, machine_auth_provider_factory, manifest_processor, migrate, profiling, results_backend_manager, ssh_manager_factory, stats_logger_manager, talisman, ) from superset.extensions.context import extension_context from superset.security import SupersetSecurityManager from superset.semantic_layers.labels import database_connections_menu_label from superset.sql.parse import SQLGLOT_DIALECTS from superset.superset_typing import FlaskResponse from superset.utils.core import is_test, pessimistic_connection_handling from superset.utils.decorators import transaction from superset.utils.log import DBEventLogger, get_event_logger_from_cfg_value if TYPE_CHECKING: from superset.app import SupersetApp logger = logging.getLogger(__name__) class SupersetAppInitializer: # pylint: disable=too-many-public-methods def __init__(self, app: SupersetApp) -> None: super().__init__() self.superset_app = app self.config = app.config self.manifest: dict[Any, Any] = {} self._db_uri_cache: str | None = None # Cache for valid database URIs @deprecated(details="use self.superset_app instead of self.flask_app") # type: ignore @property def flask_app(self) -> SupersetApp: return self.superset_app @property def database_uri(self) -> str: """Lazy property for database URI to avoid early config access issues""" # If we have a cached valid value, return it if self._db_uri_cache is not None: return self._db_uri_cache # Try to get the URI from config uri = self.config.get("SQLALCHEMY_DATABASE_URI", "") # Check if this is a fallback value that indicates config isn't ready if uri and not any( fallback in uri.lower() for fallback in ["nouser", "nopassword", "nohost", "nodb"] ): # Valid URI - cache it and return self._db_uri_cache = uri return uri # Return the fallback value without caching # This allows retry on next access when config might be ready return uri def pre_init(self) -> None: """ Called before all other init tasks are complete """ wtforms_json.init() os.makedirs(self.config["DATA_DIR"], exist_ok=True) 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.advanced_data_type.api import AdvancedDataTypeRestApi from superset.annotation_layers.annotations.api import AnnotationRestApi from superset.annotation_layers.api import AnnotationLayerRestApi from superset.async_events.api import AsyncEventsRestApi from superset.available_domains.api import AvailableDomainsRestApi from superset.cachekeys.api import CacheRestApi from superset.charts.api import ChartRestApi from superset.charts.data.api import ChartDataRestApi from superset.css_templates.api import CssTemplateRestApi from superset.dashboards.api import DashboardRestApi from superset.dashboards.filter_state.api import DashboardFilterStateRestApi from superset.dashboards.permalink.api import DashboardPermalinkRestApi 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.datasource.api import DatasourceRestApi from superset.embedded.api import EmbeddedDashboardRestApi from superset.embedded.view import EmbeddedView from superset.explore.api import ExploreRestApi from superset.explore.form_data.api import ExploreFormDataRestApi from superset.explore.permalink.api import ExplorePermalinkRestApi from superset.extensions.view import ExtensionsView from superset.importexport.api import ImportExportRestApi from superset.queries.api import QueryRestApi from superset.queries.saved_queries.api import SavedQueryRestApi from superset.reports.api import ReportScheduleRestApi from superset.reports.logs.api import ReportExecutionLogRestApi from superset.row_level_security.api import RLSRestApi from superset.security.api import ( RoleRestAPI, SecurityRestApi, UserRegistrationsRestAPI, ) from superset.sqllab.api import SqlLabRestApi from superset.sqllab.permalink.api import SqlLabPermalinkRestApi from superset.subjects.api import SubjectRestApi from superset.tags.api import TagRestApi from superset.themes.api import ThemeRestApi from superset.views.alerts import AlertView, ReportView from superset.views.all_entities import TaggedObjectsModelView from superset.views.annotations import AnnotationLayerView from superset.views.api import Api from superset.views.chart.views import SliceModelView from superset.views.core import Superset from superset.views.css_templates import CssTemplateModelView from superset.views.dashboard.views import ( Dashboard, DashboardModelView, ) from superset.views.database.views import DatabaseView from superset.views.datasource.views import DatasetEditor, Datasource from superset.views.dynamic_plugins import DynamicPluginsView from superset.views.error_handling import set_app_error_handlers from superset.views.explore import ExplorePermalinkView, ExploreView from superset.views.groups import GroupsListView from superset.views.log.api import LogRestApi from superset.views.logs import ActionLogView from superset.views.pwa_manifest import PwaManifestView from superset.views.redirect import RedirectView from superset.views.roles import RolesListView from superset.views.sql_lab.views import ( SavedQueryView, TableSchemaView, TabStateView, ) from superset.views.sqla import ( RowLevelSecurityView, TableModelView, ) from superset.views.sqllab import SqllabView from superset.views.tags import TagModelView, TagView from superset.views.tasks import TaskModelView from superset.views.themes import ThemeModelView from superset.views.user_info import UserInfoView from superset.views.user_registrations import UserRegistrationsView from superset.views.users.api import CurrentUserRestApi, UserRestApi from superset.views.users_list import UsersListView set_app_error_handlers(self.superset_app) self.register_request_handlers() # Register health blueprint from superset.views.health import health_blueprint self.superset_app.register_blueprint(health_blueprint) # # Setup API views # appbuilder.add_api(AnnotationRestApi) appbuilder.add_api(AnnotationLayerRestApi) appbuilder.add_api(AsyncEventsRestApi) appbuilder.add_api(AdvancedDataTypeRestApi) appbuilder.add_api(AvailableDomainsRestApi) appbuilder.add_api(CacheRestApi) appbuilder.add_api(ChartRestApi) appbuilder.add_api(ChartDataRestApi) appbuilder.add_api(CssTemplateRestApi) appbuilder.add_api(ThemeRestApi) appbuilder.add_api(CurrentUserRestApi) appbuilder.add_api(UserRestApi) appbuilder.add_api(DashboardFilterStateRestApi) appbuilder.add_api(DashboardPermalinkRestApi) appbuilder.add_api(DashboardRestApi) appbuilder.add_api(DatabaseRestApi) appbuilder.add_api(DatasetRestApi) appbuilder.add_api(DatasetColumnsRestApi) appbuilder.add_api(DatasetMetricRestApi) appbuilder.add_api(DatasourceRestApi) appbuilder.add_api(EmbeddedDashboardRestApi) appbuilder.add_api(ExploreRestApi) appbuilder.add_api(ExploreFormDataRestApi) appbuilder.add_api(ExplorePermalinkRestApi) appbuilder.add_api(ImportExportRestApi) appbuilder.add_api(QueryRestApi) appbuilder.add_api(ReportScheduleRestApi) appbuilder.add_api(ReportExecutionLogRestApi) appbuilder.add_api(RLSRestApi) appbuilder.add_api(SavedQueryRestApi) if feature_flag_manager.is_feature_enabled("SEMANTIC_LAYERS"): from superset.semantic_layers.api import ( SemanticLayerRestApi, SemanticViewRestApi, ) appbuilder.add_api(SemanticLayerRestApi) appbuilder.add_api(SemanticViewRestApi) appbuilder.add_api(TagRestApi) appbuilder.add_api(SqlLabRestApi) appbuilder.add_api(SqlLabPermalinkRestApi) appbuilder.add_api(SubjectRestApi) appbuilder.add_api(LogRestApi) if feature_flag_manager.is_feature_enabled("ENABLE_EXTENSIONS"): from superset.extensions.api import ExtensionsRestApi appbuilder.add_api(ExtensionsRestApi) if feature_flag_manager.is_feature_enabled("GLOBAL_TASK_FRAMEWORK"): from superset.tasks.api import TaskRestApi appbuilder.add_api(TaskRestApi) # # Setup regular views # app_root = current_app.config["APPLICATION_ROOT"] if app_root.endswith("/"): app_root = app_root.rstrip("/") # FAB renders this href verbatim in menus and does not apply # SCRIPT_NAME at render time, so the application_root has to be baked # in at registration. The previous hardcoded `/superset/welcome/` # collided with non-`/superset/` deployments AND with the new # `Superset.route_base = ""`. `app_root` is normalized above. appbuilder.add_link( "Home", label=_("Home"), href=f"{app_root}/welcome/", cond=lambda: bool(current_app.config["LOGO_TARGET_PATH"]), ) appbuilder.add_view( DatabaseView, "Databases", label=database_connections_menu_label(), icon="fa-database", category="Data", category_label=_("Data"), ) 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_link( "Datasets", label=_("Datasets"), href=f"{app_root}/tablemodelview/list/", icon="fa-table", category="", category_icon="", ) appbuilder.add_view( RolesListView, "List Roles", label=_("List Roles"), category="Security", category_label=_("Security"), menu_cond=lambda: bool( current_app.config.get("SUPERSET_SECURITY_VIEW_MENU", True) ), ) appbuilder.add_view( UserRegistrationsView, "User Registrations", label=_("User Registrations"), category="Security", category_label=_("Security"), menu_cond=lambda: bool(current_app.config["AUTH_USER_REGISTRATION"]), ) appbuilder.add_view( UsersListView, "List Users", label=_("List Users"), category="Security", category_label=_("Security"), menu_cond=lambda: bool( current_app.config.get("SUPERSET_SECURITY_VIEW_MENU", True) ), ) appbuilder.add_view( GroupsListView, "List Groups", label=_("List Groups"), category="Security", category_label=_("Security"), menu_cond=lambda: bool( current_app.config.get("SUPERSET_SECURITY_VIEW_MENU", True) ), ) 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="", menu_cond=lambda: feature_flag_manager.is_feature_enabled("CSS_TEMPLATES"), ) appbuilder.add_view( ThemeModelView, "Themes", href="/theme/list/", label=_("Themes"), icon="fa-palette", category="Manage", category_label=_("Manage"), category_icon="", ) appbuilder.add_view( ExtensionsView, "Extensions", label=_("Extensions"), category="Manage", category_label=_("Manage"), menu_cond=lambda: feature_flag_manager.is_feature_enabled( "ENABLE_EXTENSIONS" ), ) appbuilder.add_view( TaskModelView, "Tasks", label=_("Tasks"), icon="fa-clock-o", category="Manage", category_label=_("Manage"), menu_cond=lambda: feature_flag_manager.is_feature_enabled( "GLOBAL_TASK_FRAMEWORK" ), ) # # Setup views with no menu # appbuilder.add_view_no_menu(Api) appbuilder.add_view_no_menu(Dashboard) appbuilder.add_view_no_menu(Datasource) appbuilder.add_view_no_menu(DatasetEditor) appbuilder.add_view_no_menu(EmbeddedView) appbuilder.add_view_no_menu(ExploreView) appbuilder.add_view_no_menu(ExplorePermalinkView) appbuilder.add_view_no_menu(SavedQueryView) appbuilder.add_view_no_menu(SqllabView) appbuilder.add_view_no_menu(Superset) appbuilder.add_view_no_menu(TableModelView) appbuilder.add_view_no_menu(TableSchemaView) appbuilder.add_view_no_menu(TabStateView) appbuilder.add_view_no_menu(TaggedObjectsModelView) appbuilder.add_view_no_menu(TagView) appbuilder.add_view_no_menu(ReportView) appbuilder.add_view_no_menu(PwaManifestView) appbuilder.add_view_no_menu(RedirectView) appbuilder.add_view_no_menu(RoleRestAPI) appbuilder.add_view_no_menu(UserInfoView) # # Add links # appbuilder.add_link( "SQL Editor", label=_("SQL Lab"), href=f"{app_root}/sqllab/", category_icon="fa-flask", icon="fa-flask", category="SQL Lab", category_label=_("SQL"), ) appbuilder.add_link( "Saved Queries", label=_("Saved Queries"), href=f"{app_root}/savedqueryview/list/", icon="fa-save", category="SQL Lab", category_label=_("SQL"), ) appbuilder.add_link( "Query Search", label=_("Query History"), href=f"{app_root}/sqllab/history/", icon="fa-search", category_icon="fa-flask", category="SQL Lab", category_label=_("SQL Lab"), ) appbuilder.add_view( TagModelView, "Tags", label=_("Tags"), icon="", category_icon="", category="Manage", menu_cond=lambda: feature_flag_manager.is_feature_enabled("TAGGING_SYSTEM"), ) appbuilder.add_api(LogRestApi) appbuilder.add_api(UserRegistrationsRestAPI) appbuilder.add_view( ActionLogView, "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 # 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( AnnotationLayerView, "Annotation Layers", label=_("Annotation Layers"), href="AnnotationLayerView.list", icon="fa-comment", category_icon="", category="Manage", category_label=_("Manage"), ) appbuilder.add_view( RowLevelSecurityView, "Row Level Security", href="RowLevelSecurityView.list", label=_("Row Level Security"), category="Security", category_label=_("Security"), icon="fa-lock", ) def init_core_dependencies(self) -> None: """Initialize core dependency injection for direct import patterns.""" from superset.core.api.core_api_injection import ( initialize_core_api_dependencies, ) from superset.core.mcp.core_mcp_injection import ( initialize_core_mcp_dependencies, ) initialize_core_api_dependencies() initialize_core_mcp_dependencies() def init_all_dependencies_and_extensions(self) -> None: """ Initialize all core dependencies and extensions. Core dependencies are always initialized. Extensions are only initialized if the ENABLE_EXTENSIONS feature flag is enabled. """ # Always initialize core dependencies self.init_core_dependencies() # Conditionally initialize extensions based on feature flag if feature_flag_manager.is_feature_enabled("ENABLE_EXTENSIONS"): self.init_extensions() def init_extensions(self) -> None: from superset.extensions.utils import ( eager_import, get_extensions, install_in_memory_importer, ) try: extensions = get_extensions() except Exception: # pylint: disable=broad-except # noqa: S110 # If the db hasn't been initialized yet, an exception will be raised. # It's fine to ignore this, as in this case there are no extensions # present yet. return for extension in extensions.values(): if backend_files := extension.backend: install_in_memory_importer( backend_files, source_base_path=extension.source_base_path, ) backend = extension.manifest.backend if backend and backend.entrypoint: try: with extension_context(extension.manifest): eager_import(backend.entrypoint) except Exception: # pylint: disable=broad-except # Surface extension-initialization failures through the # configured logger (with traceback) so they reach log # aggregation, rather than being written to stdout. logger.exception( "Failed to initialize extension '%s'", extension.manifest.id, ) @staticmethod def _remove_continuum_write_listeners() -> None: """Detach SQLAlchemy-Continuum's own write listeners. ``make_versioned()`` runs unconditionally at import of ``superset.extensions`` and registers Continuum's mapper, session, and engine listeners — the ones that write shadow rows and ``version_transaction`` rows on every flush. Skipping only the custom baseline/change-record listeners would leave those running, so with the kill-switch off the shadow tables would silently keep accumulating, contradicting the documented contract. This is deliberately a *targeted subset* of ``sqlalchemy_continuum.remove_versioning()``: that helper also calls ``manager.reset()``, which clears ``version_class_map`` — and ``version_class()`` would then silently return the live model class, breaking the read-only ``/versions/`` endpoints this flag promises to keep working. Idempotent: guarded on a representative listener so repeated app initializations in one process (test fixtures) don't raise on double-removal. """ # pylint: disable=import-outside-toplevel import sqlalchemy as sa from sqlalchemy_continuum import versioning_manager # Enforce the master kill-switch first, unconditionally — before the # early return below. Every Continuum write listener checks # ``manager.options['versioning']`` before doing work (manager.py / # unit_of_work.py), so OFF must guarantee this is False even when the # core listeners are already detached (so any still-registered custom # listener, or a future Continuum listener this detach doesn't know to # remove, no-ops). ``version_class()`` reads ``version_class_map`` and # ignores this option, so the read-only ``/versions/`` endpoints are # unaffected. versioning_manager.options["versioning"] = False if not sa.event.contains( sa.orm.Mapper, "after_insert", versioning_manager.track_inserts ): return # listeners already detached by a prior init versioning_manager.remove_operations_tracking(sa.orm.Mapper) versioning_manager.remove_session_tracking(sa.orm.session.Session) sa.event.remove( sa.engine.Engine, "before_execute", versioning_manager.track_association_operations, ) sa.event.remove( sa.engine.Engine, "rollback", versioning_manager.clear_connection ) sa.event.remove( sa.engine.Engine, "set_connection_execution_options", versioning_manager.track_cloned_connections, ) # Verify the known write listeners are actually gone. A Continuum # upgrade that renamed a handler would make the removals above silently # miss, leaving capture half-on while we report "disabled"; surface # that rather than booting in a contradictory state. if sa.event.contains( sa.orm.Mapper, "after_insert", versioning_manager.track_inserts ): logger.warning( "versioning: Continuum write listeners still attached after " "detach; capture may not be fully disabled. This usually means " "the pinned sqlalchemy-continuum version changed how it " "registers listeners." ) @staticmethod def _add_continuum_write_listeners() -> None: """Re-attach Continuum's own write listeners — inverse of ``_remove_continuum_write_listeners``. ``make_versioned()`` attaches these at import, so on a normal boot they are already present and this is a no-op. It matters only when capture is toggled off->on within one process (multi-app / test reentrancy): an earlier OFF init detached Continuum's core shadow/transaction writers, and the ON path re-registering the custom baseline/change-record listeners + flipping ``options['versioning']`` back on is NOT enough on its own — without re-attaching these, no shadow rows would be written despite capture being reported "enabled". Mirrors ``make_versioned``'s registration exactly. Idempotent: guarded on a representative listener so a normal boot (or a repeat ON init) doesn't double-register. """ # pylint: disable=import-outside-toplevel import sqlalchemy as sa from sqlalchemy_continuum import versioning_manager if sa.event.contains( sa.orm.Mapper, "after_insert", versioning_manager.track_inserts ): return # already attached (normal boot, or already on) versioning_manager.track_operations(sa.orm.Mapper) versioning_manager.track_session(sa.orm.session.Session) sa.event.listen( sa.engine.Engine, "before_execute", versioning_manager.track_association_operations, ) sa.event.listen( sa.engine.Engine, "rollback", versioning_manager.clear_connection ) sa.event.listen( sa.engine.Engine, "set_connection_execution_options", versioning_manager.track_cloned_connections, ) def init_versioning(self) -> None: """Register SQLAlchemy-Continuum baseline and change-record listeners. Must be called after all versioned model classes have been imported so that VERSIONED_MODELS can be populated and configure_mappers() has run. ``ENABLE_VERSIONING_CAPTURE`` (ships default ``False``) gates the two before-flush listener registrations. The flag is operational, not feature: with it off the infrastructure is inert (no save writes shadow rows); flipping it on activates capture. The switch also lets an operator who observes a versioning-induced regression (e.g. a save-path slowdown attributable to the change-record listener) disable capture in ``superset_config.py`` and restart workers — a 30-second recovery instead of revert-and-redeploy. Shadow tables already created by the migration stay; they just stop accumulating new rows. The fallback here is ``False`` so that any app-factory path that does not load ``superset.config`` (some test factories, embedded use) stays inert by default rather than silently enabling capture. """ if not self.config.get("ENABLE_VERSIONING_CAPTURE", False): logger.warning( "versioning: ENABLE_VERSIONING_CAPTURE is False; " "skipping baseline + change-record listener registration " "and detaching Continuum's write listeners. Save-path " "capture is disabled; existing shadow tables and " "/versions/ endpoints continue to work read-only." ) self._remove_continuum_write_listeners() return # Symmetric with the OFF branch's ``options['versioning'] = False``: # re-assert it on here so capture is restored even if a prior app # init in the same process (multi-app / test reentrancy) flipped the # process-global Continuum option off. Without this, an OFF app # initialized before an ON app would leave the option False and the # baseline listener — which gates on it — would silently write no # baselines despite capture being "enabled". from sqlalchemy_continuum import versioning_manager versioning_manager.options["versioning"] = True # Re-attach Continuum's core write listeners in case a prior OFF init # in this process detached them. No-op on a normal boot, where # make_versioned() already attached them at import. Without this, an # off->on toggle in one process would leave the shadow/transaction # writers detached and capture silently dead despite the flag. self._add_continuum_write_listeners() from sqlalchemy.orm import Session # noqa: F401 from sqlalchemy_continuum import version_class from superset.connectors.sqla.models import SqlaTable from superset.models.dashboard import Dashboard from superset.models.slice import Slice from superset.versioning.baseline import ( register_baseline_listener, VERSIONED_MODELS, ) # Note: previously this block called ``configure_mappers()`` before # importing the snapshot modules, believing their Table declarations # needed ``version_transaction`` to exist. That's not actually the # case — the snapshot tables reference ``version_transaction.id`` # only at the DB level (via the migration); the SQLAlchemy Table # objects here intentionally declare ``transaction_id`` as a plain # ``BigInteger`` without a FK to avoid the resolution dependency. # Removing the global ``configure_mappers()`` avoids eagerly # resolving relationships in other unrelated models (notably # Flask-AppBuilder's AuditMixin on classes like Tag, whose # ``created_by`` primaryjoin only resolves under specific class # registry states in SQLAlchemy 1.4). from superset.versioning.changes import ( # noqa: E402 register_change_record_listener, ) # All versioned models — Dashboard / Slice / SqlaTable plus their # children (TableColumn / SqlMetric) and the dashboard_slices # M2M — go through Continuum's shadow tables. The JSON-snapshot # path that previously backed dataset / dashboard child diffs # has been removed (full-Continuum spike). for model_cls in (Dashboard, Slice, SqlaTable): try: version_class(model_cls) # ensure Continuum wired this model # Dedup guard: VERSIONED_MODELS is module-level state, and # test fixtures initialize multiple Superset apps per # process — without the check each re-init appends # duplicate entries. if model_cls not in VERSIONED_MODELS: VERSIONED_MODELS.append(model_cls) except Exception: # pylint: disable=broad-except # Continuum failed to wire versioning for this model. We # boot in degraded mode rather than failing startup, but a # silent skip would hide that change capture has stopped for # the model — so surface it at WARNING with the traceback. logger.warning( "Versioning is not wired for %s; change capture will be " "skipped for it. This usually means Continuum did not " "register a version class for the model.", model_cls.__name__, exc_info=True, ) register_baseline_listener() register_change_record_listener() # Retention pruning runs out-of-band as a scheduled Celery beat # task, shipped as a separate stacked PR. The previous # synchronous after_commit listener was retired so retention work # doesn't add latency to user saves. def init_app_in_ctx(self) -> None: """ Runs init logic in the context of the app """ self.configure_fab() self.configure_subjects() self.configure_url_map_converters() self.configure_data_sources() self.configure_auth_provider() self.configure_async_queries() self.configure_ssh_manager() self.configure_stats_manager() self.configure_task_manager() # Hook that provides administrators a handle on the Flask APP # after initialization if flask_app_mutator := self.config["FLASK_APP_MUTATOR"]: flask_app_mutator(self.superset_app) # Sync configuration to database (themes, etc.) # This can be called separately in multi-tenant environments self.superset_app.sync_config_to_db() self.init_views() self.init_all_dependencies_and_extensions() # Must run after all versioned models are imported and mappers configured. self.init_versioning() @staticmethod def _log_config_warning(message: str) -> None: top_banner = 80 * "-" + "\n" + 36 * " " + "WARNING\n" + 80 * "-" bottom_banner = 80 * "-" + "\n" + 80 * "-" logger.warning(top_banner) logger.warning(message) logger.warning(bottom_banner) def check_secret_key(self) -> None: secret_key = self.config["SECRET_KEY"] # A missing/empty SECRET_KEY is as insecure as the well-known default # placeholder: both are treated as insecure and handled identically. An # empty key is reachable when a deployment explicitly sets SECRET_KEY to # an empty value (the env fallback only substitutes the placeholder). if secret_key and secret_key != CHANGE_ME_SECRET_KEY: return if not secret_key: warning = ( "An empty SECRET_KEY was detected, please use superset_config.py " "to set a non-empty value.\n" "Use a strong complex alphanumeric string and use a tool to help" " you generate \n" "a sufficiently random sequence, ex: openssl rand -base64 42 \n" "For more info, see: https://superset.apache.org/docs/" "configuration/configuring-superset#specifying-a-secret_key" ) else: warning = ( "A Default SECRET_KEY was detected, please use superset_config.py " "to override it.\n" "Use a strong complex alphanumeric string and use a tool to help" " you generate \n" "a sufficiently random sequence, ex: openssl rand -base64 42 \n" "For more info, see: https://superset.apache.org/docs/" "configuration/configuring-superset#specifying-a-secret_key" ) if self.superset_app.debug or self.superset_app.config["TESTING"] or is_test(): logger.warning("Debug mode identified with insecure secret key") self._log_config_warning(warning) return self._log_config_warning(warning) logger.error("Refusing to start due to insecure SECRET_KEY") sys.exit(1) def check_guest_token_secret(self) -> None: """Refuse to start with default guest JWT secret when embedding is enabled.""" if not feature_flag_manager.is_feature_enabled("EMBEDDED_SUPERSET"): return if ( self.config.get("GUEST_TOKEN_JWT_SECRET") != CHANGE_ME_GUEST_TOKEN_JWT_SECRET ): return self._log_config_warning( "EMBEDDED_SUPERSET is enabled but GUEST_TOKEN_JWT_SECRET has not " "been changed from its default value.\n" "The default value is publicly known and must be replaced before " "running in production.\n" "Set a strong random value in superset_config.py:\n" " GUEST_TOKEN_JWT_SECRET = " "''" ) if self.superset_app.debug or self.superset_app.config["TESTING"] or is_test(): return logger.error( "Refusing to start: insecure GUEST_TOKEN_JWT_SECRET " "with EMBEDDED_SUPERSET enabled" ) sys.exit(1) def check_async_query_secret(self) -> None: """Refuse to start with the default async JWT secret when GAQ is enabled.""" if not feature_flag_manager.is_feature_enabled("GLOBAL_ASYNC_QUERIES"): return if ( self.config.get("GLOBAL_ASYNC_QUERIES_JWT_SECRET") != CHANGE_ME_GLOBAL_ASYNC_QUERIES_JWT_SECRET ): return self._log_config_warning( "GLOBAL_ASYNC_QUERIES is enabled but GLOBAL_ASYNC_QUERIES_JWT_SECRET " "has not been changed from its default value.\n" "The default value is publicly known and must be replaced before " "running in production.\n" "Set a strong random value (at least 32 bytes) in superset_config.py:\n" " GLOBAL_ASYNC_QUERIES_JWT_SECRET = " "''" ) if self.superset_app.debug or self.superset_app.config["TESTING"] or is_test(): return logger.error( "Refusing to start: insecure GLOBAL_ASYNC_QUERIES_JWT_SECRET " "with GLOBAL_ASYNC_QUERIES enabled" ) sys.exit(1) def configure_session(self) -> None: if self.config["SESSION_SERVER_SIDE"]: Session(self.superset_app) def register_request_handlers(self) -> None: """Register app-level request handlers""" from flask import request, Response from superset.security.password_change import ( register_password_change_enforcement, ) # Redirect users with a pending forced password change to the reset # page (no-op unless ENABLE_FORCE_PASSWORD_CHANGE is enabled). register_password_change_enforcement(self.superset_app) @self.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( { **self.superset_app.config["OVERRIDE_HTTP_HEADERS"], **self.superset_app.config["HTTP_HEADERS"], } ) for k, v in self.superset_app.config["DEFAULT_HTTP_HEADERS"].items(): if k not in response.headers: response.headers[k] = v # Allow service worker to control the root scope for PWA file handling if ( request.path.endswith("service-worker.js") and "Service-Worker-Allowed" not in response.headers ): response.headers["Service-Worker-Allowed"] = "/" return response @self.superset_app.after_request def cleanup_analytics_memory(response: Response) -> Response: """Force garbage collection after each request if feature flag enabled""" if feature_flag_manager.is_feature_enabled( "FORCE_GARBAGE_COLLECTION_AFTER_EVERY_REQUEST" ): import gc gc.collect() return response @self.superset_app.before_request def enforce_session_validity() -> Any: """Force logout of sessions invalidated by a per-user epoch.""" from superset.security.session_invalidation import ( enforce_session_validity as _enforce, ) return _enforce() # Stamp the per-user invalidation epoch when an account is disabled, # so outstanding sessions are terminated on their next request. from superset.security.session_invalidation import ( register_session_invalidation_events, ) register_session_invalidation_events(appbuilder.sm.user_model) @self.superset_app.context_processor def get_common_bootstrap_data() -> dict[str, Any]: # Import here to avoid circular imports from superset.utils import json from superset.views.base import common_bootstrap_payload def serialize_bootstrap_data() -> str: return json.dumps( {"common": common_bootstrap_payload()}, default=json.pessimistic_json_iso_dttm_ser, ) return {"bootstrap_data": serialize_bootstrap_data} def check_and_warn_database_connection(self) -> None: """Check database connection and warn if unavailable""" try: with self.superset_app.app_context(): # Simple connection test with db.engine.connect() as connection: connection.execute(text("SELECT 1")) except Exception: db_uri = self.database_uri if db_uri: try: safe_uri = make_url_safe(db_uri).render_as_string( hide_password=True ) except DatabaseInvalidError: safe_uri = "" else: safe_uri = "Not configured" print( f"{Fore.RED}ERROR: Cannot connect to database {safe_uri}\n" f"NOTE: Most CLI commands require a database{Style.RESET_ALL}" ) def init_app(self) -> None: """ Main entry point which will delegate to other methods in order to fully init the app """ self.pre_init() self.check_secret_key() self.configure_session() # 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.check_guest_token_secret() self.check_async_query_secret() self.configure_db_encrypt() self.setup_db() # Check database connection and warn if unavailable self.check_and_warn_database_connection() 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() self.set_db_default_isolation() self.configure_sqlglot_dialects() with self.superset_app.app_context(): self.init_app_in_ctx() # Registered outside ``init_app_in_ctx`` because the SQLAlchemy # event hook attaches to the ``Session`` *class* (a process-wide # global), not to a Session instance — it has no dependency on # the Flask app context. ``setup_db()`` ran earlier in # ``init_app``, so the ``Session`` import has already been # initialised by the time we get here. self.setup_soft_delete_listener() self.post_init() def set_db_default_isolation(self) -> None: # This block sets the default isolation level for mysql to READ COMMITTED if not # specified in the config. You can set your isolation in the config by using # SQLALCHEMY_ENGINE_OPTIONS eng_options = self.config["SQLALCHEMY_ENGINE_OPTIONS"] or {} isolation_level = eng_options.get("isolation_level") set_isolation_level_to = None if not isolation_level: backend = make_url_safe(self.database_uri).get_backend_name() if backend in ("mysql", "postgresql"): set_isolation_level_to = "READ COMMITTED" if set_isolation_level_to: logger.debug( "Setting database isolation level to %s", set_isolation_level_to, ) with self.superset_app.app_context(): db.engine.execution_options(isolation_level=set_isolation_level_to) def configure_auth_provider(self) -> None: machine_auth_provider_factory.init_app(self.superset_app) def configure_ssh_manager(self) -> None: ssh_manager_factory.init_app(self.superset_app) def configure_stats_manager(self) -> None: stats_logger_manager.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"]) # todo(hughhhh): fully remove the datasource config register for module_name, class_names in module_datasource_map.items(): class_names = [str(s) for s in class_names] __import__(module_name, fromlist=class_names) 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_sqlglot_dialects(self) -> None: extensions = self.config["SQLGLOT_DIALECTS_EXTENSIONS"] if callable(extensions): extensions = extensions() SQLGLOT_DIALECTS.update(extensions) @transaction() 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( # pylint: disable=broad-exception-raised """Your CUSTOM_SECURITY_MANAGER must now extend SupersetSecurityManager, not FAB's security manager. See [4565] in UPDATING.md""" ) appbuilder.indexview = SupersetIndexView appbuilder.security_manager_class = custom_sm appbuilder.init_app(self.superset_app, db.session) def configure_subjects(self) -> None: from superset.subjects.hooks import register_subject_hooks register_subject_hooks() 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: # noqa: C901 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( 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(self.superset_app.wsgi_app) if self.config["UPLOAD_FOLDER"]: with contextlib.suppress(OSError): os.makedirs(self.config["UPLOAD_FOLDER"]) for middleware in self.config["ADDITIONAL_MIDDLEWARE"]: self.superset_app.wsgi_app = middleware(self.superset_app.wsgi_app) # Flask-Compress Compress(self.superset_app) # Talisman talisman_enabled = self.config["TALISMAN_ENABLED"] talisman_config = ( self.config["TALISMAN_DEV_CONFIG"] if self.superset_app.debug or self.config["DEBUG"] else self.config["TALISMAN_CONFIG"] ) csp_warning = self.config["CONTENT_SECURITY_POLICY_WARNING"] if talisman_enabled: talisman.init_app(self.superset_app, **talisman_config) show_csp_warning = False if ( csp_warning and not self.superset_app.debug and ( not talisman_enabled or not talisman_config or not talisman_config.get("content_security_policy") ) ): show_csp_warning = True if show_csp_warning: logger.warning( "We haven't found any Content Security Policy (CSP) defined in " "the configurations. Please make sure to configure CSP using the " "TALISMAN_ENABLED and TALISMAN_CONFIG keys or any other external " "software. Failing to configure CSP have serious security implications. " # noqa: E501 "Check https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP for more " "information. You can disable this warning using the " "CONTENT_SECURITY_POLICY_WARNING key." ) 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 setup_soft_delete_listener(self) -> None: """Register the global soft-delete filter on the SQLAlchemy Session. Must be called after ``setup_db()`` so the Session class is available. Uses the ``do_orm_execute`` + ``with_loader_criteria`` pattern recommended by SQLAlchemy maintainer Mike Bayer for soft deletion in SQLAlchemy 1.4+: https://github.com/sqlalchemy/sqlalchemy/issues/7973#issuecomment-1112561295 """ from sqlalchemy import event from sqlalchemy.orm import Session from superset.models.helpers import _add_soft_delete_filter if not event.contains(Session, "do_orm_execute", _add_soft_delete_filter): event.listen(Session, "do_orm_execute", _add_soft_delete_filter) 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"): # In production, check_async_query_secret() already aborts startup when # the default secret is present, so this branch is never reached with it. # In debug/testing the check only warns, so skip async-query init here to # avoid AsyncQueryManager.init_app() hard-failing on the too-short default # secret and crashing startup despite the warn-only intent. if ( self.config.get("GLOBAL_ASYNC_QUERIES_JWT_SECRET") == CHANGE_ME_GLOBAL_ASYNC_QUERIES_JWT_SECRET ): return async_query_manager_factory.init_app(self.superset_app) def configure_task_manager(self) -> None: """Initialize the TaskManager for GTF realtime notifications.""" if feature_flag_manager.is_feature_enabled("GLOBAL_TASK_FRAMEWORK"): from superset.tasks.manager import TaskManager TaskManager.init_app(self.superset_app) def register_blueprints(self) -> None: # Register custom blueprints from config 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(url_for("Superset.welcome")) @expose("/lang/") @safe def patch_flask_locale(self, locale: str) -> FlaskResponse: """ Change user's locale and redirect back to the previous page. Overrides FAB's babel.views.LocaleView so we can use the request Referrer as the redirect target, in case our previous page was actually served by the frontend (and thus not added to the session's page_history stack). """ if locale not in self.appbuilder.bm.languages: abort(404, description="Locale not supported.") session["locale"] = locale refresh() self.update_redirect() if redirect_to := request.headers.get("Referer"): return redirect(get_safe_redirect(redirect_to)) return redirect(self.get_redirect())