diff --git a/docs/docs/security/security.mdx b/docs/docs/security/security.mdx index f8abdfcc6db..d6655477609 100644 --- a/docs/docs/security/security.mdx +++ b/docs/docs/security/security.mdx @@ -280,6 +280,49 @@ TALISMAN_CONFIG = { "content_security_policy": { ... ``` +#### Configuring Talisman in Superset + +Talisman settings in Superset can be modified using superset_config.py. If you need to adjust security policies, you can override the default configuration. + +Example: Overriding Talisman Configuration in superset_config.py for loading images form s3 or other external sources. + +```python +TALISMAN_CONFIG = { + "content_security_policy": { + "base-uri": ["'self'"], + "default-src": ["'self'"], + "img-src": [ + "'self'", + "blob:", + "data:", + "https://apachesuperset.gateway.scarf.sh", + "https://static.scarf.sh/", + # "https://cdn.brandfolder.io", # Uncomment when SLACK_ENABLE_AVATARS is True # noqa: E501 + "ows.terrestris.de", + "aws.s3.com", # Add Your Bucket or external data source + ], + "worker-src": ["'self'", "blob:"], + "connect-src": [ + "'self'", + "https://api.mapbox.com", + "https://events.mapbox.com", + ], + "object-src": "'none'", + "style-src": [ + "'self'", + "'unsafe-inline'", + ], + "script-src": ["'self'", "'strict-dynamic'"], + }, + "content_security_policy_nonce_in": ["script-src"], + "force_https": False, + "session_cookie_secure": False, +} +``` + +# For more information on setting up Talisman, please refer to +https://superset.apache.org/docs/configuration/networking-settings/#changing-flask-talisman-csp + ### Reporting Security Vulnerabilities Apache Software Foundation takes a rigorous standpoint in annihilating the security issues in its diff --git a/docs/src/styles/custom.css b/docs/src/styles/custom.css index 40ac1ad8e23..7f18f0d3abc 100644 --- a/docs/src/styles/custom.css +++ b/docs/src/styles/custom.css @@ -58,7 +58,6 @@ ul.dropdown__menu svg { --ifm-code-font-size: 95%; --ifm-menu-link-padding-vertical: 12px; --doc-sidebar-width: 350px !important; - --ifm-navbar-height: none; --ifm-font-family-base: Roboto; --ifm-footer-background-color: #173036; --ifm-footer-color: #87939a; diff --git a/superset-frontend/package-lock.json b/superset-frontend/package-lock.json index 51c51461543..b2194cfce19 100644 --- a/superset-frontend/package-lock.json +++ b/superset-frontend/package-lock.json @@ -3262,9 +3262,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.0.tgz", - "integrity": "sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==", + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.10.tgz", + "integrity": "sha512-2WJMeRQPHKSPemqk/awGrAiuFfzBmOIPXKizAsVhWH9YJqLZ0H+HS4c8loHGgW6utJ3E/ejXQUsiGaQy2NZ9Fw==", "license": "MIT", "dependencies": { "regenerator-runtime": "^0.14.0" @@ -21020,6 +21020,16 @@ "url": "https://github.com/fb55/domhandler?sponsor=1" } }, + "node_modules/dompurify": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.4.tgz", + "integrity": "sha512-ysFSFEDVduQpyhzAob/kkuJjf5zWkZD8/A9ywSp1byueyuCfHamrCBa14/Oc2iiB0e51B+NpxSl5gmzn+Ms/mg==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optional": true, + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, "node_modules/domutils": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", @@ -32642,30 +32652,23 @@ } }, "node_modules/jspdf": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/jspdf/-/jspdf-2.5.2.tgz", - "integrity": "sha512-myeX9c+p7znDWPk0eTrujCzNjT+CXdXyk7YmJq5nD5V7uLLKmSXnlQ/Jn/kuo3X09Op70Apm0rQSnFWyGK8uEQ==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/jspdf/-/jspdf-3.0.1.tgz", + "integrity": "sha512-qaGIxqxetdoNnFQQXxTKUD9/Z7AloLaw94fFsOiJMxbfYdBbrBuhWmbzI8TVjrw7s3jBY1PFHofBKMV/wZPapg==", "license": "MIT", "dependencies": { - "@babel/runtime": "^7.23.2", + "@babel/runtime": "^7.26.7", "atob": "^2.1.2", "btoa": "^1.2.1", "fflate": "^0.8.1" }, "optionalDependencies": { - "canvg": "^3.0.6", + "canvg": "^3.0.11", "core-js": "^3.6.0", - "dompurify": "^2.5.4", + "dompurify": "^3.2.4", "html2canvas": "^1.0.0-rc.5" } }, - "node_modules/jspdf/node_modules/dompurify": { - "version": "2.5.8", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.5.8.tgz", - "integrity": "sha512-o1vSNgrmYMQObbSSvF/1brBYEQPHhV1+gsmrusO7/GXtp1T9rCS8cXFqVxK/9crT1jA6Ccv+5MTSjBNqr7Sovw==", - "license": "(MPL-2.0 OR Apache-2.0)", - "optional": true - }, "node_modules/jsprim": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-2.0.2.tgz", @@ -54744,6 +54747,7 @@ "version": "0.20.3", "license": "Apache-2.0", "dependencies": { + "@types/react-redux": "^7.1.10", "d3-array": "^1.2.0", "dayjs": "^1.11.13", "lodash": "^4.17.21" diff --git a/superset-frontend/package.json b/superset-frontend/package.json index 53ace28ee61..9d0d7582c1a 100644 --- a/superset-frontend/package.json +++ b/superset-frontend/package.json @@ -383,7 +383,8 @@ "core-js": "^3.38.1", "d3-color": "^3.1.0", "puppeteer": "^22.4.1", - "underscore": "^1.13.7" + "underscore": "^1.13.7", + "jspdf": "^3.0.1" }, "readme": "ERROR: No README data found!", "scarfSettings": { diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Bubble/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Bubble/transformProps.ts index 1888383a523..49e51f511b3 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Bubble/transformProps.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Bubble/transformProps.ts @@ -201,7 +201,7 @@ export default function transformProps(chartProps: EchartsBubbleChartProps) { name: bubbleXAxisTitle, nameLocation: 'middle', nameTextStyle: { - fontWight: 'bolder', + fontWeight: 'bolder', }, nameGap: convertInteger(xAxisTitleMargin), type: xAxisType, @@ -219,7 +219,7 @@ export default function transformProps(chartProps: EchartsBubbleChartProps) { name: bubbleYAxisTitle, nameLocation: 'middle', nameTextStyle: { - fontWight: 'bolder', + fontWeight: 'bolder', }, nameGap: convertInteger(yAxisTitleMargin), min: yAxisMin, diff --git a/superset/config.py b/superset/config.py index 8eb78d9a46d..e0ebbb3f2ef 100644 --- a/superset/config.py +++ b/superset/config.py @@ -1616,6 +1616,9 @@ CONTENT_SECURITY_POLICY_WARNING = True TALISMAN_ENABLED = utils.cast_to_boolean(os.environ.get("TALISMAN_ENABLED", True)) # If you want Talisman, how do you want it configured?? +# For more information on setting up Talisman, please refer to +# https://superset.apache.org/docs/configuration/networking-settings/#changing-flask-talisman-csp + TALISMAN_CONFIG = { "content_security_policy": { "base-uri": ["'self'"], @@ -1626,7 +1629,7 @@ TALISMAN_CONFIG = { "data:", "https://apachesuperset.gateway.scarf.sh", "https://static.scarf.sh/", - # "https://avatars.slack-edge.com", # Uncomment when SLACK_ENABLE_AVATARS is True # noqa: E501 + # "https://cdn.brandfolder.io", # Uncomment when SLACK_ENABLE_AVATARS is True # noqa: E501 "ows.terrestris.de", ], "worker-src": ["'self'", "blob:"], @@ -1657,7 +1660,7 @@ TALISMAN_DEV_CONFIG = { "data:", "https://apachesuperset.gateway.scarf.sh", "https://static.scarf.sh/", - "https://avatars.slack-edge.com", + "https://cdn.brandfolder.io", "ows.terrestris.de", ], "worker-src": ["'self'", "blob:"], diff --git a/superset/models/helpers.py b/superset/models/helpers.py index 33c7c17d170..5dc97b602af 100644 --- a/superset/models/helpers.py +++ b/superset/models/helpers.py @@ -1380,7 +1380,7 @@ class ExploreMixin: # pylint: disable=too-many-public-methods if engine.dialect.identifier_preparer._double_percents: sql = sql.replace("%%", "%") - df = pd.read_sql_query(sql=sql, con=engine) + df = pd.read_sql_query(sql=self.text(sql), con=engine) # replace NaN with None to ensure it can be serialized to JSON df = df.replace({np.nan: None}) return df["column_values"].to_list() diff --git a/superset/queries/saved_queries/schemas.py b/superset/queries/saved_queries/schemas.py index 7c314e63911..152ddcd0870 100644 --- a/superset/queries/saved_queries/schemas.py +++ b/superset/queries/saved_queries/schemas.py @@ -39,6 +39,7 @@ get_export_ids_schema = {"type": "array", "items": {"type": "integer"}} class ImportV1SavedQuerySchema(Schema): + catalog = fields.String(allow_none=True, validate=Length(0, 128)) schema = fields.String(allow_none=True, validate=Length(0, 128)) label = fields.String(allow_none=True, validate=Length(0, 256)) description = fields.String(allow_none=True) diff --git a/superset/sql_lab.py b/superset/sql_lab.py index 7f4a0f1c991..c157896fcc6 100644 --- a/superset/sql_lab.py +++ b/superset/sql_lab.py @@ -193,7 +193,7 @@ def get_sql_results( # pylint: disable=too-many-arguments except Exception as ex: # pylint: disable=broad-except logger.debug("Query %d: %s", query_id, ex) stats_logger.incr("error_sqllab_unhandled") - query = get_query(query_id) + query = get_query(query_id=query_id) return handle_query_error(ex, query) @@ -423,7 +423,7 @@ def execute_sql_statements( # noqa: C901 # only asynchronous queries stats_logger.timing("sqllab.query.time_pending", now_as_float() - start_time) - query = get_query(query_id) + query = get_query(query_id=query_id) payload: dict[str, Any] = {"query_id": query_id} database = query.database db_engine_spec = database.db_engine_spec diff --git a/superset/utils/excel.py b/superset/utils/excel.py index d34446832a8..46e1a1f071a 100644 --- a/superset/utils/excel.py +++ b/superset/utils/excel.py @@ -56,10 +56,24 @@ def df_to_excel(df: pd.DataFrame, **kwargs: Any) -> Any: def apply_column_types( df: pd.DataFrame, column_types: list[GenericDataType] ) -> pd.DataFrame: + """ + Applies the column types to the dataframe to prepare for an excel export + + :param df: The dataframe to apply the column types to + :param column_types: The types of the columns + :return: The dataframe with the column types applied + """ for column, column_type in zip(df.columns, column_types, strict=False): if column_type == GenericDataType.NUMERIC: try: df[column] = pd.to_numeric(df[column]) + # if the number is too large, convert it to a string + # Excel does not support numbers larger than 10^15 + df[column] = df[column].apply( + lambda x: str(x) + if isinstance(x, (int, float)) and abs(x) > 10**15 + else x + ) except ValueError: df[column] = df[column].astype(str) elif pd.api.types.is_datetime64tz_dtype(df[column]): diff --git a/tests/integration_tests/fixtures/importexport.py b/tests/integration_tests/fixtures/importexport.py index 427d0d24a69..33aff24541c 100644 --- a/tests/integration_tests/fixtures/importexport.py +++ b/tests/integration_tests/fixtures/importexport.py @@ -689,4 +689,5 @@ saved_queries_config = { "uuid": "05b679b5-8eaf-452c-b874-a7a774cfa4e9", "version": "1.0.0", "database_uuid": "b8a1ccd3-779d-4ab7-8ad8-9ab119d7fe89", + "catalog": "default", } diff --git a/tests/unit_tests/models/helpers_test.py b/tests/unit_tests/models/helpers_test.py index cc106dbe649..4e8aa493be3 100644 --- a/tests/unit_tests/models/helpers_test.py +++ b/tests/unit_tests/models/helpers_test.py @@ -25,7 +25,7 @@ from unittest.mock import patch import pytest from pytest_mock import MockerFixture -from sqlalchemy import create_engine +from sqlalchemy import create_engine, text from sqlalchemy.orm.session import Session from sqlalchemy.pool import StaticPool @@ -201,10 +201,12 @@ def test_values_for_column_double_percents( ) # make sure final query has single percents with database.get_sqla_engine() as engine: - pd.read_sql_query.assert_called_with( - sql=( - "SELECT DISTINCT CASE WHEN b LIKE 'A%' THEN 'yes' ELSE 'nope' END " - "AS column_values \nFROM t\n LIMIT 10000 OFFSET 0" - ), - con=engine, + expected_sql = text( + "SELECT DISTINCT CASE WHEN b LIKE 'A%' THEN 'yes' ELSE 'nope' END " + "AS column_values \nFROM t\n LIMIT 10000 OFFSET 0" ) + called_sql = pd.read_sql_query.call_args.kwargs["sql"] + called_conn = pd.read_sql_query.call_args.kwargs["con"] + + assert called_sql.compare(expected_sql) is True + assert called_conn == engine diff --git a/tests/unit_tests/utils/excel_tests.py b/tests/unit_tests/utils/excel_tests.py index deb6d3d0b4e..b07fe7a4f0d 100644 --- a/tests/unit_tests/utils/excel_tests.py +++ b/tests/unit_tests/utils/excel_tests.py @@ -105,3 +105,27 @@ def test_column_data_types_with_failing_conversion(): assert not is_numeric_dtype(df["col1"]) assert not is_numeric_dtype(df["col2"]) assert not is_numeric_dtype(df["col3"]) + + +def test_column_data_types_with_large_numeric_values(): + df = pd.DataFrame( + { + "big_number": [ + 10**14, + 999999999999999, + 10**15 + 1, + 10**16, + 1100108628127863, + 2**54, + ], + } + ) + apply_column_types(df, [GenericDataType.NUMERIC]) + assert df["big_number"].tolist() == [ + 100000000000000, + 999999999999999, + "1000000000000001", + "10000000000000000", + "1100108628127863", + "18014398509481984", + ]