You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by mi...@apache.org on 2023/10/19 13:06:08 UTC

[superset] branch 3.0 updated (890bf59ce4 -> 293568ad5a)

This is an automated email from the ASF dual-hosted git repository.

michaelsmolina pushed a change to branch 3.0
in repository https://gitbox.apache.org/repos/asf/superset.git


    from 890bf59ce4 chore: Updates 3.0.1 CHANGELOG
     new d7cbdca081 fix(sqllab): Mistitled for new tab after rename (#25523)
     new 701ee30d1e fix(sqllab): template validation error within comments (#25626)
     new ec3bed709e fix: avoid 500 errors with SQLLAB_BACKEND_PERSISTENCE (#25553)
     new af1e71352a fix(import): Make sure query context is overwritten for overwriting imports (#25493)
     new 236aef8126 fix: permalink save/overwrites in explore (#25112)
     new b95ff2da23 fix(header navlinks): link navlinks to path prefix (#25495)
     new b0f229ea7e fix: improve upload ZIP file validation (#25658)
     new b380495516 fix: warning of nth-child (#23638)
     new 293568ad5a fix(dremio): Fixes issue with Dremio SQL generation for Charts with Series Limit (#25657)

The 9 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 .../src/ReactParallelCoordinates.jsx               |  2 +-
 .../legacy-preset-chart-nvd3/src/ReactNVD3.jsx     |  2 +-
 superset-frontend/src/SqlLab/actions/sqlLab.js     | 17 +++++--
 .../src/SqlLab/actions/sqlLab.test.js              | 22 ++++++---
 .../components/SaveDatasetActionButton/index.tsx   |  2 +-
 .../src/components/ButtonGroup/index.tsx           |  6 +--
 .../src/components/DropdownButton/index.tsx        |  2 +-
 .../components/DropdownSelectableIcon/index.tsx    |  2 +-
 .../src/explore/components/SaveModal.test.jsx      | 35 ++++++++++++-
 .../src/explore/components/SaveModal.tsx           | 23 ++++++---
 superset-frontend/src/features/home/Menu.test.tsx  |  6 ++-
 superset-frontend/src/features/home/Menu.tsx       | 30 +++++++++++-
 superset/charts/commands/importers/v1/__init__.py  |  2 +-
 superset/commands/importers/v1/assets.py           |  2 +-
 superset/commands/importers/v1/utils.py            |  2 +
 superset/config.py                                 |  5 ++
 .../dashboards/commands/importers/v1/__init__.py   |  2 +-
 superset/db_engine_specs/dremio.py                 |  2 -
 superset/sqllab/query_render.py                    |  4 +-
 superset/utils/core.py                             | 19 ++++++++
 superset/views/sql_lab/views.py                    |  6 ++-
 tests/integration_tests/core_tests.py              | 35 +++++++++++++
 tests/integration_tests/sqllab_tests.py            |  7 +++
 tests/unit_tests/utils/test_core.py                | 57 ++++++++++++++++++++++
 24 files changed, 254 insertions(+), 38 deletions(-)


[superset] 08/09: fix: warning of nth-child (#23638)

Posted by mi...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

michaelsmolina pushed a commit to branch 3.0
in repository https://gitbox.apache.org/repos/asf/superset.git

commit b380495516cf405fa7d80e1a5590ffb267ecc791
Author: JUST.in DO IT <ju...@airbnb.com>
AuthorDate: Tue Oct 17 15:33:07 2023 -0400

    fix: warning of nth-child (#23638)
    
    (cherry picked from commit 16cc089b198dcdebc2422845aa08d18233c6b3a4)
---
 .../src/ReactParallelCoordinates.jsx                                | 2 +-
 .../plugins/legacy-preset-chart-nvd3/src/ReactNVD3.jsx              | 2 +-
 .../src/SqlLab/components/SaveDatasetActionButton/index.tsx         | 2 +-
 superset-frontend/src/components/ButtonGroup/index.tsx              | 6 +++---
 superset-frontend/src/components/DropdownButton/index.tsx           | 2 +-
 superset-frontend/src/components/DropdownSelectableIcon/index.tsx   | 2 +-
 6 files changed, 8 insertions(+), 8 deletions(-)

diff --git a/superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/src/ReactParallelCoordinates.jsx b/superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/src/ReactParallelCoordinates.jsx
index 4a7675d555..7f30716057 100644
--- a/superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/src/ReactParallelCoordinates.jsx
+++ b/superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/src/ReactParallelCoordinates.jsx
@@ -106,7 +106,7 @@ export default styled(ParallelCoordinates)`
       height: 18px;
       margin: 0px;
     }
-    .parcoords .row:nth-child(odd) {
+    .parcoords .row:nth-of-type(odd) {
       background: ${addAlpha(theme.colors.grayscale.dark2, 0.05)};
     }
     .parcoords .header {
diff --git a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/ReactNVD3.jsx b/superset-frontend/plugins/legacy-preset-chart-nvd3/src/ReactNVD3.jsx
index 9a9962e8aa..f7f219a05d 100644
--- a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/ReactNVD3.jsx
+++ b/superset-frontend/plugins/legacy-preset-chart-nvd3/src/ReactNVD3.jsx
@@ -152,7 +152,7 @@ export default styled(NVD3)`
       white-space: nowrap;
       font-weight: ${({ theme }) => theme.typography.weights.bold};
     }
-    tbody tr:not(.tooltip-header) td:nth-child(2) {
+    tbody tr:not(.tooltip-header) td:nth-of-type(2) {
       word-break: break-word;
     }
   }
diff --git a/superset-frontend/src/SqlLab/components/SaveDatasetActionButton/index.tsx b/superset-frontend/src/SqlLab/components/SaveDatasetActionButton/index.tsx
index dbb25b138a..79a3bf0b8e 100644
--- a/superset-frontend/src/SqlLab/components/SaveDatasetActionButton/index.tsx
+++ b/superset-frontend/src/SqlLab/components/SaveDatasetActionButton/index.tsx
@@ -44,7 +44,7 @@ const SaveDatasetActionButton = ({
       font-weight: ${theme.gridUnit * 150};
       background-color: ${theme.colors.primary.light4};
       color: ${theme.colors.primary.dark1};
-      &:nth-child(2) {
+      &:nth-of-type(2) {
         &:before,
         &:hover:before {
           border-left: 2px solid ${theme.colors.primary.dark2};
diff --git a/superset-frontend/src/components/ButtonGroup/index.tsx b/superset-frontend/src/components/ButtonGroup/index.tsx
index 463a042066..78e29714cd 100644
--- a/superset-frontend/src/components/ButtonGroup/index.tsx
+++ b/superset-frontend/src/components/ButtonGroup/index.tsx
@@ -30,18 +30,18 @@ export default function ButtonGroup(props: ButtonGroupProps) {
       role="group"
       className={className}
       css={{
-        '& :nth-child(1):not(:nth-last-child(1))': {
+        '& :nth-of-type(1):not(:nth-last-of-type(1))': {
           borderTopRightRadius: 0,
           borderBottomRightRadius: 0,
           borderRight: 0,
           marginLeft: 0,
         },
-        '& :not(:nth-child(1)):not(:nth-last-child(1))': {
+        '& :not(:nth-of-type(1)):not(:nth-last-of-type(1))': {
           borderRadius: 0,
           borderRight: 0,
           marginLeft: 0,
         },
-        '& :nth-last-child(1):not(:nth-child(1))': {
+        '& :nth-last-of-type(1):not(:nth-of-type(1))': {
           borderTopLeftRadius: 0,
           borderBottomLeftRadius: 0,
           marginLeft: 0,
diff --git a/superset-frontend/src/components/DropdownButton/index.tsx b/superset-frontend/src/components/DropdownButton/index.tsx
index c6293f66a3..a35e66b5d7 100644
--- a/superset-frontend/src/components/DropdownButton/index.tsx
+++ b/superset-frontend/src/components/DropdownButton/index.tsx
@@ -42,7 +42,7 @@ const StyledDropdownButton = styled.div`
         background-color: ${({ theme }) => theme.colors.grayscale.light2};
         color: ${({ theme }) => theme.colors.grayscale.base};
       }
-      &:nth-child(2) {
+      &:nth-of-type(2) {
         margin: 0;
         border-radius: ${({ theme }) =>
           `0 ${theme.gridUnit}px ${theme.gridUnit}px 0`};
diff --git a/superset-frontend/src/components/DropdownSelectableIcon/index.tsx b/superset-frontend/src/components/DropdownSelectableIcon/index.tsx
index 582bc182e3..f668aa720d 100644
--- a/superset-frontend/src/components/DropdownSelectableIcon/index.tsx
+++ b/superset-frontend/src/components/DropdownSelectableIcon/index.tsx
@@ -46,7 +46,7 @@ const StyledDropdownButton = styled(
   button.ant-btn:first-of-type {
     display: none;
   }
-  > button.ant-btn:nth-child(2) {
+  > button.ant-btn:nth-of-type(2) {
     display: inline-flex;
     background-color: transparent !important;
     height: unset;


[superset] 09/09: fix(dremio): Fixes issue with Dremio SQL generation for Charts with Series Limit (#25657)

Posted by mi...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

michaelsmolina pushed a commit to branch 3.0
in repository https://gitbox.apache.org/repos/asf/superset.git

commit 293568ad5ac10d34cc59e66afe936a1cb02ab48f
Author: OskarNS <so...@gmail.com>
AuthorDate: Wed Oct 18 21:54:27 2023 +0200

    fix(dremio): Fixes issue with Dremio SQL generation for Charts with Series Limit (#25657)
    
    (cherry picked from commit be8265794059d8bbe216a4cb22c7a3f6adf4bcb3)
---
 superset/db_engine_specs/dremio.py | 2 --
 superset/views/sql_lab/views.py    | 2 +-
 2 files changed, 1 insertion(+), 3 deletions(-)

diff --git a/superset/db_engine_specs/dremio.py b/superset/db_engine_specs/dremio.py
index 2288c52572..c96159f1b8 100644
--- a/superset/db_engine_specs/dremio.py
+++ b/superset/db_engine_specs/dremio.py
@@ -27,8 +27,6 @@ class DremioEngineSpec(BaseEngineSpec):
     engine = "dremio"
     engine_name = "Dremio"
 
-    allows_alias_in_select = False
-
     _time_grain_expressions = {
         None: "{col}",
         TimeGrain.SECOND: "DATE_TRUNC('second', {col})",
diff --git a/superset/views/sql_lab/views.py b/superset/views/sql_lab/views.py
index 08394eb2ba..068888353c 100644
--- a/superset/views/sql_lab/views.py
+++ b/superset/views/sql_lab/views.py
@@ -214,7 +214,7 @@ class TabStateView(BaseSupersetView):
 
     @has_access_api
     @expose("<int:tab_state_id>", methods=("PUT",))
-    def put(self, tab_state_id: int) -> FlaskResponse:  # pylint: disable=no-self-use
+    def put(self, tab_state_id: int) -> FlaskResponse:
         if _get_owner_id(tab_state_id) != get_user_id():
             return Response(status=403)
 


[superset] 01/09: fix(sqllab): Mistitled for new tab after rename (#25523)

Posted by mi...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

michaelsmolina pushed a commit to branch 3.0
in repository https://gitbox.apache.org/repos/asf/superset.git

commit d7cbdca081ed69a7fc740272d714a6deaa0f6bac
Author: JUST.in DO IT <ju...@airbnb.com>
AuthorDate: Fri Oct 13 11:17:45 2023 -0400

    fix(sqllab): Mistitled for new tab after rename (#25523)
    
    (cherry picked from commit a520124a78286aea0f9a7ad491d041bbca2c3596)
---
 superset-frontend/src/SqlLab/actions/sqlLab.js     | 17 ++++++++++++-----
 .../src/SqlLab/actions/sqlLab.test.js              | 22 ++++++++++++++++------
 2 files changed, 28 insertions(+), 11 deletions(-)

diff --git a/superset-frontend/src/SqlLab/actions/sqlLab.js b/superset-frontend/src/SqlLab/actions/sqlLab.js
index 5d9ecdacdf..fbfba6783e 100644
--- a/superset-frontend/src/SqlLab/actions/sqlLab.js
+++ b/superset-frontend/src/SqlLab/actions/sqlLab.js
@@ -596,7 +596,12 @@ export function addNewQueryEditor() {
           '-- Note: Unless you save your query, these tabs will NOT persist if you clear your cookies or change browsers.\n\n',
         );
 
-    const name = newQueryTabName(queryEditors || []);
+    const name = newQueryTabName(
+      queryEditors?.map(qe => ({
+        ...qe,
+        ...(qe.id === unsavedQueryEditor.id && unsavedQueryEditor),
+      })) || [],
+    );
 
     return dispatch(
       addQueryEditor({
@@ -614,10 +619,12 @@ export function addNewQueryEditor() {
 export function cloneQueryToNewTab(query, autorun) {
   return function (dispatch, getState) {
     const state = getState();
-    const { queryEditors, tabHistory } = state.sqlLab;
-    const sourceQueryEditor = queryEditors.find(
-      qe => qe.id === tabHistory[tabHistory.length - 1],
-    );
+    const { queryEditors, unsavedQueryEditor, tabHistory } = state.sqlLab;
+    const sourceQueryEditor = {
+      ...queryEditors.find(qe => qe.id === tabHistory[tabHistory.length - 1]),
+      ...(tabHistory[tabHistory.length - 1] === unsavedQueryEditor.id &&
+        unsavedQueryEditor),
+    };
     const queryEditor = {
       name: t('Copy of %s', sourceQueryEditor.name),
       dbId: query.dbId ? query.dbId : null,
diff --git a/superset-frontend/src/SqlLab/actions/sqlLab.test.js b/superset-frontend/src/SqlLab/actions/sqlLab.test.js
index fc94a44645..25f80aa1c3 100644
--- a/superset-frontend/src/SqlLab/actions/sqlLab.test.js
+++ b/superset-frontend/src/SqlLab/actions/sqlLab.test.js
@@ -389,8 +389,11 @@ describe('async actions', () => {
       const state = {
         sqlLab: {
           tabHistory: [id],
-          queryEditors: [{ id, name: 'Dummy query editor' }],
-          unsavedQueryEditor: {},
+          queryEditors: [{ id, name: 'out of updated title' }],
+          unsavedQueryEditor: {
+            id,
+            name: 'Dummy query editor',
+          },
         },
       };
       const store = mockStore(state);
@@ -444,16 +447,23 @@ describe('async actions', () => {
 
     describe('addNewQueryEditor', () => {
       it('creates new query editor with new tab name', () => {
-        const store = mockStore(initialState);
+        const store = mockStore({
+          ...initialState,
+          sqlLab: {
+            ...initialState.sqlLab,
+            unsavedQueryEditor: {
+              id: defaultQueryEditor.id,
+              name: 'Untitled Query 6',
+            },
+          },
+        });
         const expectedActions = [
           {
             type: actions.ADD_QUERY_EDITOR,
             queryEditor: {
               id: 'abcd',
               sql: expect.stringContaining('SELECT ...'),
-              name: `Untitled Query ${
-                store.getState().sqlLab.queryEditors.length + 1
-              }`,
+              name: `Untitled Query 7`,
               dbId: defaultQueryEditor.dbId,
               schema: defaultQueryEditor.schema,
               autorun: false,


[superset] 02/09: fix(sqllab): template validation error within comments (#25626)

Posted by mi...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

michaelsmolina pushed a commit to branch 3.0
in repository https://gitbox.apache.org/repos/asf/superset.git

commit 701ee30d1e06f93a1c35622203eac694b2fd23ef
Author: JUST.in DO IT <ju...@airbnb.com>
AuthorDate: Fri Oct 13 13:07:54 2023 -0400

    fix(sqllab): template validation error within comments (#25626)
    
    (cherry picked from commit b370c66308e1bc84031ed7aae855aa72c20fbd11)
---
 superset/sqllab/query_render.py         | 4 +++-
 tests/integration_tests/sqllab_tests.py | 7 +++++++
 2 files changed, 10 insertions(+), 1 deletion(-)

diff --git a/superset/sqllab/query_render.py b/superset/sqllab/query_render.py
index db1adf43ba..95111276fe 100644
--- a/superset/sqllab/query_render.py
+++ b/superset/sqllab/query_render.py
@@ -25,6 +25,7 @@ from jinja2.meta import find_undeclared_variables
 
 from superset import is_feature_enabled
 from superset.errors import SupersetErrorType
+from superset.sql_parse import ParsedQuery
 from superset.sqllab.commands.execute import SqlQueryRender
 from superset.sqllab.exceptions import SqlLabException
 from superset.utils import core as utils
@@ -57,8 +58,9 @@ class SqlQueryRenderImpl(SqlQueryRender):
                 database=query_model.database, query=query_model
             )
 
+            parsed_query = ParsedQuery(query_model.sql, strip_comments=True)
             rendered_query = sql_template_processor.process_template(
-                query_model.sql, **execution_context.template_params
+                parsed_query.stripped(), **execution_context.template_params
             )
             self._validate(execution_context, rendered_query, sql_template_processor)
             return rendered_query
diff --git a/tests/integration_tests/sqllab_tests.py b/tests/integration_tests/sqllab_tests.py
index fbab4d98d2..914c601610 100644
--- a/tests/integration_tests/sqllab_tests.py
+++ b/tests/integration_tests/sqllab_tests.py
@@ -514,6 +514,13 @@ class TestSqlLab(SupersetTestCase):
         )
         assert data["status"] == "success"
 
+        data = self.run_sql(
+            "SELECT * FROM birth_names WHERE state = '{{ state }}' -- blabblah {{ extra1 }} {{fake.fn()}}\nLIMIT 10",
+            "3",
+            template_params=json.dumps({"state": "CA"}),
+        )
+        assert data["status"] == "success"
+
         data = self.run_sql(
             "SELECT * FROM birth_names WHERE state = '{{ stat }}' LIMIT 10",
             "2",


[superset] 07/09: fix: improve upload ZIP file validation (#25658)

Posted by mi...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

michaelsmolina pushed a commit to branch 3.0
in repository https://gitbox.apache.org/repos/asf/superset.git

commit b0f229ea7efaee136f2b1761dadc36de8824296f
Author: Daniel Vaz Gaspar <da...@gmail.com>
AuthorDate: Tue Oct 17 18:28:09 2023 +0100

    fix: improve upload ZIP file validation (#25658)
---
 superset/commands/importers/v1/utils.py |  2 ++
 superset/config.py                      |  5 +++
 superset/utils/core.py                  | 19 +++++++++++
 tests/unit_tests/utils/test_core.py     | 57 +++++++++++++++++++++++++++++++++
 4 files changed, 83 insertions(+)

diff --git a/superset/commands/importers/v1/utils.py b/superset/commands/importers/v1/utils.py
index 8ca008b3e2..8cb0c1b553 100644
--- a/superset/commands/importers/v1/utils.py
+++ b/superset/commands/importers/v1/utils.py
@@ -26,6 +26,7 @@ from superset import db
 from superset.commands.importers.exceptions import IncorrectVersionError
 from superset.databases.ssh_tunnel.models import SSHTunnel
 from superset.models.core import Database
+from superset.utils.core import check_is_safe_zip
 
 METADATA_FILE_NAME = "metadata.yaml"
 IMPORT_VERSION = "1.0.0"
@@ -207,6 +208,7 @@ def is_valid_config(file_name: str) -> bool:
 
 
 def get_contents_from_bundle(bundle: ZipFile) -> dict[str, str]:
+    check_is_safe_zip(bundle)
     return {
         remove_root(file_name): bundle.read(file_name).decode()
         for file_name in bundle.namelist()
diff --git a/superset/config.py b/superset/config.py
index 4233799d0d..27f78832d1 100644
--- a/superset/config.py
+++ b/superset/config.py
@@ -1579,6 +1579,11 @@ WELCOME_PAGE_LAST_TAB: (
     Literal["examples", "all"] | tuple[str, list[dict[str, Any]]]
 ) = "all"
 
+# Max allowed size for a zipped file
+ZIPPED_FILE_MAX_SIZE = 100 * 1024 * 1024  # 100MB
+# Max allowed compression ratio for a zipped file
+ZIP_FILE_MAX_COMPRESS_RATIO = 200.0
+
 # Configuration for environment tag shown on the navbar. Setting 'text' to '' will hide the tag.
 # 'color' can either be a hex color code, or a dot-indexed theme color (e.g. error.base)
 ENVIRONMENT_TAG_CONFIG = {
diff --git a/superset/utils/core.py b/superset/utils/core.py
index 58df8b3c0f..9e3592d22b 100644
--- a/superset/utils/core.py
+++ b/superset/utils/core.py
@@ -1917,6 +1917,25 @@ def create_zip(files: dict[str, Any]) -> BytesIO:
     return buf
 
 
+def check_is_safe_zip(zip_file: ZipFile) -> None:
+    """
+    Checks whether a ZIP file is safe, raises SupersetException if not.
+
+    :param zip_file:
+    :return:
+    """
+    uncompress_size = 0
+    compress_size = 0
+    for zip_file_element in zip_file.infolist():
+        if zip_file_element.file_size > current_app.config["ZIPPED_FILE_MAX_SIZE"]:
+            raise SupersetException("Found file with size above allowed threshold")
+        uncompress_size += zip_file_element.file_size
+        compress_size += zip_file_element.compress_size
+    compress_ratio = uncompress_size / compress_size
+    if compress_ratio > current_app.config["ZIP_FILE_MAX_COMPRESS_RATIO"]:
+        raise SupersetException("Zip compress ratio above allowed threshold")
+
+
 def remove_extra_adhoc_filters(form_data: dict[str, Any]) -> None:
     """
     Remove filters from slice data that originate from a filter box or native filter
diff --git a/tests/unit_tests/utils/test_core.py b/tests/unit_tests/utils/test_core.py
index 568595517c..bd0cefeaf0 100644
--- a/tests/unit_tests/utils/test_core.py
+++ b/tests/unit_tests/utils/test_core.py
@@ -15,12 +15,16 @@
 # specific language governing permissions and limitations
 # under the License.
 import os
+from dataclasses import dataclass
 from typing import Any, Optional
+from unittest.mock import MagicMock
 
 import pytest
 
+from superset.exceptions import SupersetException
 from superset.utils.core import (
     cast_to_boolean,
+    check_is_safe_zip,
     is_test,
     parse_boolean_string,
     QueryObjectFilterClause,
@@ -41,6 +45,12 @@ EXTRA_FILTER: QueryObjectFilterClause = {
 }
 
 
+@dataclass
+class MockZipInfo:
+    file_size: int
+    compress_size: int
+
+
 @pytest.mark.parametrize(
     "original,expected",
     [
@@ -171,3 +181,50 @@ def test_other_values():
     assert cast_to_boolean([]) is False
     assert cast_to_boolean({}) is False
     assert cast_to_boolean(object()) is False
+
+
+def test_check_if_safe_zip_success(app_context: None) -> None:
+    """
+    Test if ZIP files are safe
+    """
+    ZipFile = MagicMock()
+    ZipFile.infolist.return_value = [
+        MockZipInfo(file_size=1000, compress_size=10),
+        MockZipInfo(file_size=1000, compress_size=10),
+        MockZipInfo(file_size=1000, compress_size=10),
+        MockZipInfo(file_size=1000, compress_size=10),
+        MockZipInfo(file_size=1000, compress_size=10),
+    ]
+    check_is_safe_zip(ZipFile)
+
+
+def test_check_if_safe_zip_high_rate(app_context: None) -> None:
+    """
+    Test if ZIP files is not highly compressed
+    """
+    ZipFile = MagicMock()
+    ZipFile.infolist.return_value = [
+        MockZipInfo(file_size=1000, compress_size=1),
+        MockZipInfo(file_size=1000, compress_size=1),
+        MockZipInfo(file_size=1000, compress_size=1),
+        MockZipInfo(file_size=1000, compress_size=1),
+        MockZipInfo(file_size=1000, compress_size=1),
+    ]
+    with pytest.raises(SupersetException):
+        check_is_safe_zip(ZipFile)
+
+
+def test_check_if_safe_zip_hidden_bomb(app_context: None) -> None:
+    """
+    Test if ZIP file does not contain a big file highly compressed
+    """
+    ZipFile = MagicMock()
+    ZipFile.infolist.return_value = [
+        MockZipInfo(file_size=1000, compress_size=100),
+        MockZipInfo(file_size=1000, compress_size=100),
+        MockZipInfo(file_size=1000, compress_size=100),
+        MockZipInfo(file_size=1000, compress_size=100),
+        MockZipInfo(file_size=1000 * (1024 * 1024), compress_size=100),
+    ]
+    with pytest.raises(SupersetException):
+        check_is_safe_zip(ZipFile)


[superset] 03/09: fix: avoid 500 errors with SQLLAB_BACKEND_PERSISTENCE (#25553)

Posted by mi...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

michaelsmolina pushed a commit to branch 3.0
in repository https://gitbox.apache.org/repos/asf/superset.git

commit ec3bed709e5660cc2832826a194abdf3cacb3499
Author: Igor Khrol <kh...@gmail.com>
AuthorDate: Fri Oct 13 22:30:19 2023 +0300

    fix: avoid 500 errors with SQLLAB_BACKEND_PERSISTENCE (#25553)
    
    (cherry picked from commit 99f79f5143c417497ffde326a8393ab60aa71e7e)
---
 superset/views/sql_lab/views.py       |  4 ++++
 tests/integration_tests/core_tests.py | 35 +++++++++++++++++++++++++++++++++++
 2 files changed, 39 insertions(+)

diff --git a/superset/views/sql_lab/views.py b/superset/views/sql_lab/views.py
index df7b36050e..08394eb2ba 100644
--- a/superset/views/sql_lab/views.py
+++ b/superset/views/sql_lab/views.py
@@ -219,6 +219,10 @@ class TabStateView(BaseSupersetView):
             return Response(status=403)
 
         fields = {k: json.loads(v) for k, v in request.form.to_dict().items()}
+        if client_id := fields.get("latest_query_id"):
+            query = db.session.query(Query).filter_by(client_id=client_id).one_or_none()
+            if not query:
+                return self.json_response({"error": "Bad request"}, status=400)
         db.session.query(TabState).filter_by(id=tab_state_id).update(fields)
         db.session.commit()
         return json_success(json.dumps(tab_state_id))
diff --git a/tests/integration_tests/core_tests.py b/tests/integration_tests/core_tests.py
index d9f998a066..191d9dc2d0 100644
--- a/tests/integration_tests/core_tests.py
+++ b/tests/integration_tests/core_tests.py
@@ -1197,6 +1197,41 @@ class TestCore(SupersetTestCase, InsertChartMixin):
 
         self.assertEqual(payload["label"], "Untitled Query foo")
 
+    def test_tabstate_update(self):
+        username = "admin"
+        self.login(username)
+        # create a tab
+        data = {
+            "queryEditor": json.dumps(
+                {
+                    "name": "Untitled Query foo",
+                    "dbId": 1,
+                    "schema": None,
+                    "autorun": False,
+                    "sql": "SELECT ...",
+                    "queryLimit": 1000,
+                }
+            )
+        }
+        resp = self.get_json_resp("/tabstateview/", data=data)
+        tab_state_id = resp["id"]
+        # update tab state with non-existing client_id
+        client_id = "asdfasdf"
+        data = {"sql": json.dumps("select 1"), "latest_query_id": json.dumps(client_id)}
+        response = self.client.put(f"/tabstateview/{tab_state_id}", data=data)
+        self.assertEqual(response.status_code, 400)
+        self.assertEqual(response.json["error"], "Bad request")
+        # generate query
+        db.session.add(Query(client_id=client_id, database_id=1))
+        db.session.commit()
+        # update tab state with a valid client_id
+        response = self.client.put(f"/tabstateview/{tab_state_id}", data=data)
+        self.assertEqual(response.status_code, 200)
+        # nulls should be ok too
+        data["latest_query_id"] = "null"
+        response = self.client.put(f"/tabstateview/{tab_state_id}", data=data)
+        self.assertEqual(response.status_code, 200)
+
     def test_virtual_table_explore_visibility(self):
         # test that default visibility it set to True
         database = superset.utils.database.get_example_database()


[superset] 04/09: fix(import): Make sure query context is overwritten for overwriting imports (#25493)

Posted by mi...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

michaelsmolina pushed a commit to branch 3.0
in repository https://gitbox.apache.org/repos/asf/superset.git

commit af1e71352ae0ce607b9882ec2e607b43904d256f
Author: Jack Fragassi <jf...@gmail.com>
AuthorDate: Mon Oct 16 09:49:55 2023 -0700

    fix(import): Make sure query context is overwritten for overwriting imports (#25493)
    
    (cherry picked from commit a0a0d8043fe7004134bf89a05e6b5f6ee41399e5)
---
 superset/charts/commands/importers/v1/__init__.py     | 2 +-
 superset/commands/importers/v1/assets.py              | 2 +-
 superset/dashboards/commands/importers/v1/__init__.py | 2 +-
 3 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/superset/charts/commands/importers/v1/__init__.py b/superset/charts/commands/importers/v1/__init__.py
index 2a9c691159..043018fa3b 100644
--- a/superset/charts/commands/importers/v1/__init__.py
+++ b/superset/charts/commands/importers/v1/__init__.py
@@ -95,6 +95,6 @@ class ImportChartsCommand(ImportModelsCommand):
                 config["params"].update({"datasource": dataset.uid})
 
                 if "query_context" in config:
-                    del config["query_context"]
+                    config["query_context"] = None
 
                 import_chart(session, config, overwrite=overwrite)
diff --git a/superset/commands/importers/v1/assets.py b/superset/commands/importers/v1/assets.py
index f0720d70b1..4c8971315c 100644
--- a/superset/commands/importers/v1/assets.py
+++ b/superset/commands/importers/v1/assets.py
@@ -117,7 +117,7 @@ class ImportAssetsCommand(BaseCommand):
                 dataset_uid = f"{dataset_dict['datasource_id']}__{dataset_dict['datasource_type']}"
                 config["params"].update({"datasource": dataset_uid})
                 if "query_context" in config:
-                    del config["query_context"]
+                    config["query_context"] = None
                 chart = import_chart(session, config, overwrite=True)
                 chart_ids[str(chart.uuid)] = chart.id
 
diff --git a/superset/dashboards/commands/importers/v1/__init__.py b/superset/dashboards/commands/importers/v1/__init__.py
index e86bddec9f..30e63da4e4 100644
--- a/superset/dashboards/commands/importers/v1/__init__.py
+++ b/superset/dashboards/commands/importers/v1/__init__.py
@@ -118,7 +118,7 @@ class ImportDashboardsCommand(ImportModelsCommand):
                 dataset_uid = f"{dataset_dict['datasource_id']}__{dataset_dict['datasource_type']}"
                 config["params"].update({"datasource": dataset_uid})
                 if "query_context" in config:
-                    del config["query_context"]
+                    config["query_context"] = None
 
                 chart = import_chart(session, config, overwrite=False)
                 chart_ids[str(chart.uuid)] = chart.id


[superset] 05/09: fix: permalink save/overwrites in explore (#25112)

Posted by mi...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

michaelsmolina pushed a commit to branch 3.0
in repository https://gitbox.apache.org/repos/asf/superset.git

commit 236aef81267786034ad0146fdab406a4d35503e8
Author: Hugh A. Miles II <hu...@gmail.com>
AuthorDate: Mon Oct 16 14:00:09 2023 -0400

    fix: permalink save/overwrites in explore (#25112)
    
    Co-authored-by: Elizabeth Thompson <es...@gmail.com>
    (cherry picked from commit e58a3aba545fd03f2af33b0075c4cacf09f776a3)
---
 .../src/explore/components/SaveModal.test.jsx      | 35 ++++++++++++++++++++--
 .../src/explore/components/SaveModal.tsx           | 23 +++++++++-----
 2 files changed, 48 insertions(+), 10 deletions(-)

diff --git a/superset-frontend/src/explore/components/SaveModal.test.jsx b/superset-frontend/src/explore/components/SaveModal.test.jsx
index bdb93e5429..29bb278269 100644
--- a/superset-frontend/src/explore/components/SaveModal.test.jsx
+++ b/superset-frontend/src/explore/components/SaveModal.test.jsx
@@ -27,7 +27,10 @@ import Button from 'src/components/Button';
 import fetchMock from 'fetch-mock';
 
 import * as saveModalActions from 'src/explore/actions/saveModalActions';
-import SaveModal, { StyledModal } from 'src/explore/components/SaveModal';
+import SaveModal, {
+  PureSaveModal,
+  StyledModal,
+} from 'src/explore/components/SaveModal';
 import { BrowserRouter } from 'react-router-dom';
 
 const middlewares = [thunk];
@@ -100,8 +103,12 @@ const queryDefaultProps = {
 };
 
 const fetchDashboardsEndpoint = `glob:*/dashboardasync/api/read?_flt_0_owners=${1}`;
+const fetchChartEndpoint = `glob:*/api/v1/chart/${1}*`;
 
-beforeAll(() => fetchMock.get(fetchDashboardsEndpoint, mockDashboardData));
+beforeAll(() => {
+  fetchMock.get(fetchDashboardsEndpoint, mockDashboardData);
+  fetchMock.get(fetchChartEndpoint, { id: 1, dashboards: [1] });
+});
 
 afterAll(() => fetchMock.restore());
 
@@ -226,3 +233,27 @@ test('set dataset name when chart source is query', () => {
   expect(wrapper.find('[data-test="new-dataset-name"]')).toExist();
   expect(wrapper.state().datasetName).toBe('test');
 });
+
+test('make sure slice_id in the URLSearchParams before the redirect', () => {
+  const myProps = {
+    ...defaultProps,
+    slice: { slice_id: 1, slice_name: 'title', owners: [1] },
+    actions: {
+      setFormData: jest.fn(),
+      updateSlice: jest.fn(() => Promise.resolve({ id: 1 })),
+      getSliceDashboards: jest.fn(),
+    },
+    user: { userId: 1 },
+    history: {
+      replace: jest.fn(),
+    },
+    dispatch: jest.fn(),
+  };
+
+  const saveModal = new PureSaveModal(myProps);
+  const result = saveModal.handleRedirect(
+    'https://example.com/?name=John&age=30',
+    { id: 1 },
+  );
+  expect(result.get('slice_id')).toEqual('1');
+});
diff --git a/superset-frontend/src/explore/components/SaveModal.tsx b/superset-frontend/src/explore/components/SaveModal.tsx
index d390e46388..ed5b244cd7 100644
--- a/superset-frontend/src/explore/components/SaveModal.tsx
+++ b/superset-frontend/src/explore/components/SaveModal.tsx
@@ -164,6 +164,17 @@ class SaveModal extends React.Component<SaveModalProps, SaveModalState> {
     this.props.dispatch(setSaveChartModalVisibility(false));
   }
 
+  handleRedirect = (windowLocationSearch: string, chart: any) => {
+    const searchParams = new URLSearchParams(windowLocationSearch);
+    searchParams.set('save_action', this.state.action);
+    if (this.state.action !== 'overwrite') {
+      searchParams.delete('form_data_key');
+    }
+
+    searchParams.set('slice_id', chart.id.toString());
+    return searchParams;
+  };
+
   async saveOrOverwrite(gotodash: boolean) {
     this.setState({ isLoading: true });
 
@@ -270,14 +281,7 @@ class SaveModal extends React.Component<SaveModalProps, SaveModalState> {
         return;
       }
 
-      const searchParams = new URLSearchParams(window.location.search);
-      searchParams.set('save_action', this.state.action);
-      if (this.state.action !== 'overwrite') {
-        searchParams.delete('form_data_key');
-      }
-      if (this.state.action === 'saveas') {
-        searchParams.set('slice_id', value.id.toString());
-      }
+      const searchParams = this.handleRedirect(window.location.search, value);
       this.props.history.replace(`/explore/?${searchParams.toString()}`);
 
       this.setState({ isLoading: false });
@@ -527,3 +531,6 @@ function mapStateToProps({
 }
 
 export default withRouter(connect(mapStateToProps)(SaveModal));
+
+// User for testing purposes need to revisit once we convert this to functional component
+export { SaveModal as PureSaveModal };


[superset] 06/09: fix(header navlinks): link navlinks to path prefix (#25495)

Posted by mi...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

michaelsmolina pushed a commit to branch 3.0
in repository https://gitbox.apache.org/repos/asf/superset.git

commit b95ff2da23a87b84b9a62af6433f53c98ccb49b8
Author: Jack <41...@users.noreply.github.com>
AuthorDate: Tue Oct 17 11:45:25 2023 -0500

    fix(header navlinks): link navlinks to path prefix (#25495)
    
    (cherry picked from commit 51c56dd2a0f52fa092862f8bc5833749f9adc1ba)
---
 superset-frontend/src/features/home/Menu.test.tsx |  6 ++++-
 superset-frontend/src/features/home/Menu.tsx      | 30 ++++++++++++++++++++++-
 2 files changed, 34 insertions(+), 2 deletions(-)

diff --git a/superset-frontend/src/features/home/Menu.test.tsx b/superset-frontend/src/features/home/Menu.test.tsx
index b40a5ab075..04a0a9876f 100644
--- a/superset-frontend/src/features/home/Menu.test.tsx
+++ b/superset-frontend/src/features/home/Menu.test.tsx
@@ -295,7 +295,11 @@ test('should render the environment tag', async () => {
   const {
     data: { environment_tag },
   } = mockedProps;
-  render(<Menu {...mockedProps} />, { useRedux: true, useQueryParams: true });
+  render(<Menu {...mockedProps} />, {
+    useRedux: true,
+    useQueryParams: true,
+    useRouter: true,
+  });
   expect(await screen.findByText(environment_tag.text)).toBeInTheDocument();
 });
 
diff --git a/superset-frontend/src/features/home/Menu.tsx b/superset-frontend/src/features/home/Menu.tsx
index 92766cfdda..56a2fd611e 100644
--- a/superset-frontend/src/features/home/Menu.tsx
+++ b/superset-frontend/src/features/home/Menu.tsx
@@ -24,7 +24,7 @@ import { getUrlParam } from 'src/utils/urlUtils';
 import { Row, Col, Grid } from 'src/components';
 import { MainNav as DropdownMenu, MenuMode } from 'src/components/Menu';
 import { Tooltip } from 'src/components/Tooltip';
-import { Link } from 'react-router-dom';
+import { Link, useLocation } from 'react-router-dom';
 import { GenericLink } from 'src/components/GenericLink/GenericLink';
 import Icons from 'src/components/Icons';
 import { useUiConfig } from 'src/components/UiConfigContext';
@@ -186,6 +186,33 @@ export function Menu({
     return () => window.removeEventListener('resize', windowResize);
   }, []);
 
+  enum paths {
+    EXPLORE = '/explore',
+    DASHBOARD = '/dashboard',
+    CHART = '/chart',
+    DATASETS = '/tablemodelview',
+  }
+
+  const defaultTabSelection: string[] = [];
+  const [activeTabs, setActiveTabs] = useState(defaultTabSelection);
+  const location = useLocation();
+  useEffect(() => {
+    const path = location.pathname;
+    switch (true) {
+      case path.startsWith(paths.DASHBOARD):
+        setActiveTabs(['Dashboards']);
+        break;
+      case path.startsWith(paths.CHART) || path.startsWith(paths.EXPLORE):
+        setActiveTabs(['Charts']);
+        break;
+      case path.startsWith(paths.DATASETS):
+        setActiveTabs(['Datasets']);
+        break;
+      default:
+        setActiveTabs(defaultTabSelection);
+    }
+  }, [location.pathname]);
+
   const standalone = getUrlParam(URL_PARAMS.standalone);
   if (standalone || uiConfig.hideNav) return <></>;
 
@@ -268,6 +295,7 @@ export function Menu({
             mode={showMenu}
             data-test="navbar-top"
             className="main-nav"
+            selectedKeys={activeTabs}
           >
             {menu.map((item, index) => {
               const props = {