mirror of
https://github.com/apache/superset.git
synced 2026-04-20 00:24:38 +00:00
fix: dataset update with invalid SQL query (#35543)
This commit is contained in:
committed by
GitHub
parent
441e043bff
commit
50a5854b25
@@ -41,6 +41,7 @@ import {
|
||||
VizType,
|
||||
FeatureFlag,
|
||||
isFeatureEnabled,
|
||||
getClientErrorObject,
|
||||
} from '@superset-ui/core';
|
||||
import { extendedDayjs as dayjs } from '@superset-ui/core/utils/dates';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
@@ -227,39 +228,50 @@ export const SaveDatasetModal = ({
|
||||
}
|
||||
setLoading(true);
|
||||
|
||||
const [, key] = await Promise.all([
|
||||
updateDataset(
|
||||
datasource?.dbId,
|
||||
datasetToOverwrite?.datasetid,
|
||||
datasource?.sql,
|
||||
datasource?.columns?.map(
|
||||
(d: { column_name: string; type: string; is_dttm: boolean }) => ({
|
||||
column_name: d.column_name,
|
||||
type: d.type,
|
||||
is_dttm: d.is_dttm,
|
||||
}),
|
||||
try {
|
||||
const [, key] = await Promise.all([
|
||||
updateDataset(
|
||||
datasource?.dbId,
|
||||
datasetToOverwrite?.datasetid,
|
||||
datasource?.sql,
|
||||
datasource?.columns?.map(
|
||||
(d: { column_name: string; type: string; is_dttm: boolean }) => ({
|
||||
column_name: d.column_name,
|
||||
type: d.type,
|
||||
is_dttm: d.is_dttm,
|
||||
}),
|
||||
),
|
||||
datasetToOverwrite?.owners?.map((o: DatasetOwner) => o.id),
|
||||
true,
|
||||
),
|
||||
datasetToOverwrite?.owners?.map((o: DatasetOwner) => o.id),
|
||||
true,
|
||||
),
|
||||
postFormData(datasetToOverwrite.datasetid, 'table', {
|
||||
...formDataWithDefaults,
|
||||
datasource: `${datasetToOverwrite.datasetid}__table`,
|
||||
...(defaultVizType === VizType.Table && {
|
||||
all_columns: datasource?.columns?.map(column => column.column_name),
|
||||
postFormData(datasetToOverwrite.datasetid, 'table', {
|
||||
...formDataWithDefaults,
|
||||
datasource: `${datasetToOverwrite.datasetid}__table`,
|
||||
...(defaultVizType === VizType.Table && {
|
||||
all_columns: datasource?.columns?.map(column => column.column_name),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
]);
|
||||
setLoading(false);
|
||||
]);
|
||||
setLoading(false);
|
||||
|
||||
const url = mountExploreUrl(null, {
|
||||
[URL_PARAMS.formDataKey.name]: key,
|
||||
});
|
||||
createWindow(url);
|
||||
const url = mountExploreUrl(null, {
|
||||
[URL_PARAMS.formDataKey.name]: key,
|
||||
});
|
||||
createWindow(url);
|
||||
|
||||
setShouldOverwriteDataset(false);
|
||||
setDatasetName(getDefaultDatasetName());
|
||||
onHide();
|
||||
setShouldOverwriteDataset(false);
|
||||
setDatasetName(getDefaultDatasetName());
|
||||
onHide();
|
||||
} catch (error) {
|
||||
setLoading(false);
|
||||
getClientErrorObject(error).then((e: { error: string }) => {
|
||||
dispatch(
|
||||
addDangerToast(
|
||||
e.error || t('An error occurred while overwriting the dataset'),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const loadDatasetOverwriteOptions = useCallback(
|
||||
|
||||
@@ -33,6 +33,7 @@ from superset.commands.dataset.exceptions import (
|
||||
DatasetColumnNotFoundValidationError,
|
||||
DatasetColumnsDuplicateValidationError,
|
||||
DatasetColumnsExistsValidationError,
|
||||
DatasetDataAccessIsNotAllowed,
|
||||
DatasetExistsValidationError,
|
||||
DatasetForbiddenError,
|
||||
DatasetInvalidError,
|
||||
@@ -46,7 +47,7 @@ from superset.commands.dataset.exceptions import (
|
||||
from superset.connectors.sqla.models import SqlaTable
|
||||
from superset.daos.dataset import DatasetDAO
|
||||
from superset.datasets.schemas import FolderSchema
|
||||
from superset.exceptions import SupersetSecurityException
|
||||
from superset.exceptions import SupersetParseError, SupersetSecurityException
|
||||
from superset.models.core import Database
|
||||
from superset.sql.parse import Table
|
||||
from superset.utils.decorators import on_error, transaction
|
||||
@@ -166,6 +167,38 @@ class UpdateDatasetCommand(UpdateMixin, BaseCommand):
|
||||
):
|
||||
exceptions.append(DatasetExistsValidationError(table))
|
||||
|
||||
self._validate_sql_access(db, catalog, schema, exceptions)
|
||||
|
||||
def _validate_sql_access(
|
||||
self,
|
||||
db: Database,
|
||||
catalog: str | None,
|
||||
schema: str | None,
|
||||
exceptions: list[ValidationError],
|
||||
) -> None:
|
||||
"""Validate SQL query access if SQL is being updated."""
|
||||
# we know we have a valid model
|
||||
self._model = cast(SqlaTable, self._model)
|
||||
|
||||
sql = self._properties.get("sql")
|
||||
if sql and sql != self._model.sql:
|
||||
try:
|
||||
security_manager.raise_for_access(
|
||||
database=db,
|
||||
sql=sql,
|
||||
catalog=catalog,
|
||||
schema=schema,
|
||||
)
|
||||
except SupersetSecurityException as ex:
|
||||
exceptions.append(DatasetDataAccessIsNotAllowed(ex.error.message))
|
||||
except SupersetParseError as ex:
|
||||
exceptions.append(
|
||||
ValidationError(
|
||||
f"Invalid SQL: {ex.error.message}",
|
||||
field_name="sql",
|
||||
)
|
||||
)
|
||||
|
||||
def _validate_semantics(self, exceptions: list[ValidationError]) -> None:
|
||||
# we know we have a valid model
|
||||
self._model = cast(SqlaTable, self._model)
|
||||
|
||||
@@ -70,6 +70,105 @@ def test_update_dataset_forbidden(mocker: MockerFixture) -> None:
|
||||
UpdateDatasetCommand(1, {"name": "test"}).run()
|
||||
|
||||
|
||||
def test_update_dataset_sql_authorized_schema(mocker: MockerFixture) -> None:
|
||||
"""
|
||||
Test that updating a dataset with SQL works when user has schema access.
|
||||
"""
|
||||
mock_dataset_dao = mocker.patch("superset.commands.dataset.update.DatasetDAO")
|
||||
mock_database = mocker.MagicMock()
|
||||
mock_database.id = 1
|
||||
mock_database.get_default_catalog.return_value = "catalog"
|
||||
mock_database.allow_multi_catalog = False
|
||||
|
||||
mock_dataset = mocker.MagicMock()
|
||||
mock_dataset.database = mock_database
|
||||
mock_dataset.catalog = "catalog"
|
||||
mock_dataset.schema = "public"
|
||||
mock_dataset.table_name = "test_table"
|
||||
mock_dataset.owners = [] # No owners to avoid ownership computation issues
|
||||
|
||||
mock_dataset_dao.find_by_id.return_value = mock_dataset
|
||||
mock_dataset_dao.get_database_by_id.return_value = mock_database
|
||||
mock_dataset_dao.validate_update_uniqueness.return_value = True
|
||||
mock_dataset_dao.update.return_value = mock_dataset
|
||||
|
||||
# Mock successful ownership check
|
||||
mocker.patch(
|
||||
"superset.commands.dataset.update.security_manager.raise_for_ownership",
|
||||
)
|
||||
|
||||
# Mock security manager methods for owner computation
|
||||
mocker.patch("superset.commands.utils.security_manager.is_admin", return_value=True)
|
||||
|
||||
# Mock security manager to allow access to the schema
|
||||
mocker.patch(
|
||||
"superset.commands.dataset.update.security_manager.raise_for_access",
|
||||
)
|
||||
|
||||
# Update dataset with SQL - should work when user has access
|
||||
result = UpdateDatasetCommand(
|
||||
1, {"sql": "SELECT * FROM public.allowed_table"}
|
||||
).run()
|
||||
|
||||
# Verify the update was called
|
||||
assert result == mock_dataset
|
||||
mock_dataset_dao.update.assert_called_once()
|
||||
|
||||
|
||||
def test_update_dataset_sql_unauthorized_schema(mocker: MockerFixture) -> None:
|
||||
"""
|
||||
Test that updating a dataset with SQL to an unauthorized schema raises an error.
|
||||
"""
|
||||
mock_dataset_dao = mocker.patch("superset.commands.dataset.update.DatasetDAO")
|
||||
mock_database = mocker.MagicMock()
|
||||
mock_database.id = 1
|
||||
mock_database.get_default_catalog.return_value = "catalog"
|
||||
mock_database.allow_multi_catalog = False
|
||||
|
||||
mock_dataset = mocker.MagicMock()
|
||||
mock_dataset.database = mock_database
|
||||
mock_dataset.catalog = "catalog"
|
||||
mock_dataset.schema = "public"
|
||||
mock_dataset.table_name = "test_table"
|
||||
mock_dataset.owners = [] # No owners to avoid ownership computation issues
|
||||
|
||||
mock_dataset_dao.find_by_id.return_value = mock_dataset
|
||||
mock_dataset_dao.get_database_by_id.return_value = mock_database
|
||||
mock_dataset_dao.validate_update_uniqueness.return_value = True
|
||||
|
||||
# Mock successful ownership check
|
||||
mocker.patch(
|
||||
"superset.commands.dataset.update.security_manager.raise_for_ownership",
|
||||
)
|
||||
|
||||
# Mock security manager methods for owner computation
|
||||
mocker.patch("superset.commands.utils.security_manager.is_admin", return_value=True)
|
||||
|
||||
# Mock security manager to raise error for SQL schema access
|
||||
mocker.patch(
|
||||
"superset.commands.dataset.update.security_manager.raise_for_access",
|
||||
side_effect=SupersetSecurityException(
|
||||
SupersetError(
|
||||
error_type=SupersetErrorType.MISSING_OWNERSHIP_ERROR,
|
||||
message="You don't have access to the 'restricted_schema' schema",
|
||||
level=ErrorLevel.ERROR,
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
# Try to update dataset with SQL querying an unauthorized schema
|
||||
with pytest.raises(DatasetInvalidError) as excinfo:
|
||||
UpdateDatasetCommand(
|
||||
1, {"sql": "SELECT * FROM restricted_schema.sensitive_table"}
|
||||
).run()
|
||||
|
||||
# Check that the appropriate error message is in the exceptions
|
||||
assert any(
|
||||
"You don't have access to the 'restricted_schema' schema" in str(exc)
|
||||
for exc in excinfo.value._exceptions
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("payload, exception, error_msg"),
|
||||
[
|
||||
|
||||
Reference in New Issue
Block a user