This is an automated email from the ASF dual-hosted git repository.
maximebeauchemin pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git
The following commit(s) were added to refs/heads/master by this push:
new abc2d46fed refactor: remove obsolete Flask flash messaging system
(#35237)
abc2d46fed is described below
commit abc2d46fedf9ac0978d075e80941d3745b417470
Author: Maxime Beauchemin <[email protected]>
AuthorDate: Thu Sep 25 00:05:16 2025 -0700
refactor: remove obsolete Flask flash messaging system (#35237)
Co-authored-by: Claude <[email protected]>
---
.../spec/fixtures/mockDashboardInfo.js | 1 -
.../FlashProvider/FlashProvider.test.tsx | 65 ----------------------
.../src/components/FlashProvider/index.tsx | 51 -----------------
.../src/components/FlashProvider/types.ts | 20 -------
superset-frontend/src/components/index.ts | 1 -
superset-frontend/src/constants.ts | 1 -
superset-frontend/src/dashboard/actions/hydrate.js | 1 -
.../src/embedded/EmbeddedContextProviders.tsx | 38 ++++++-------
superset-frontend/src/explore/types.ts | 1 -
superset-frontend/src/pages/Login/Login.test.tsx | 6 +-
superset-frontend/src/pages/Login/index.tsx | 28 ++++++++--
.../src/theme/tests/ThemeController.test.ts | 2 -
superset-frontend/src/types/bootstrapTypes.ts | 2 -
.../src/views/RootContextProviders.tsx | 42 +++++++-------
superset/views/auth.py | 7 +--
superset/views/base.py | 12 +---
superset/views/core.py | 61 +++-----------------
superset/views/utils.py | 8 +--
.../dashboards/security/security_rbac_tests.py | 12 ++--
19 files changed, 85 insertions(+), 274 deletions(-)
diff --git a/superset-frontend/spec/fixtures/mockDashboardInfo.js
b/superset-frontend/spec/fixtures/mockDashboardInfo.js
index a046a554d0..31328e18ab 100644
--- a/superset-frontend/spec/fixtures/mockDashboardInfo.js
+++ b/superset-frontend/spec/fixtures/mockDashboardInfo.js
@@ -49,7 +49,6 @@ export default {
dash_edit_perm: true,
dash_save_perm: true,
common: {
- flash_messages: [],
conf: { SUPERSET_WEBSERVER_TIMEOUT: 60 },
},
filterBarOrientation: FilterBarOrientation.Vertical,
diff --git
a/superset-frontend/src/components/FlashProvider/FlashProvider.test.tsx
b/superset-frontend/src/components/FlashProvider/FlashProvider.test.tsx
deleted file mode 100644
index 6b93e4009d..0000000000
--- a/superset-frontend/src/components/FlashProvider/FlashProvider.test.tsx
+++ /dev/null
@@ -1,65 +0,0 @@
-/**
- * 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.
- */
-
-import { render, screen } from 'spec/helpers/testing-library';
-import { Provider } from 'react-redux';
-import { store } from 'src/views/store';
-import type { FlashMessage } from './types';
-import { FlashProvider } from '.';
-
-test('Rerendering correctly with default props', () => {
- const messages: FlashMessage[] = [];
- render(
- <FlashProvider messages={messages}>
- <div data-test="my-component">My Component</div>
- </FlashProvider>,
- { store },
- );
- expect(screen.getByTestId('my-component')).toBeInTheDocument();
-});
-
-test('messages should only be inserted in the State when the component is
mounted', () => {
- const messages: FlashMessage[] = [
- ['info', 'teste message 01'],
- ['info', 'teste message 02'],
- ];
- expect(store.getState().messageToasts).toEqual([]);
- const { rerender } = render(
- <Provider store={store}>
- <FlashProvider messages={messages}>
- <div data-teste="my-component">My Component</div>
- </FlashProvider>
- </Provider>,
- );
- const fistRender = store.getState().messageToasts;
- expect(fistRender).toHaveLength(2);
- expect(fistRender[1].text).toBe(messages[0][1]);
- expect(fistRender[0].text).toBe(messages[1][1]);
-
- rerender(
- <Provider store={store}>
- <FlashProvider messages={[...messages, ['info', 'teste message 03']]}>
- <div data-teste="my-component">My Component</div>
- </FlashProvider>
- </Provider>,
- );
-
- const secondRender = store.getState().messageToasts;
- expect(secondRender).toEqual(fistRender);
-});
diff --git a/superset-frontend/src/components/FlashProvider/index.tsx
b/superset-frontend/src/components/FlashProvider/index.tsx
deleted file mode 100644
index d6c07d4652..0000000000
--- a/superset-frontend/src/components/FlashProvider/index.tsx
+++ /dev/null
@@ -1,51 +0,0 @@
-/**
- * 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.
- */
-import { useToasts } from 'src/components/MessageToasts/withToasts';
-import { useComponentDidMount } from '@superset-ui/core';
-import type { FlashMessage } from './types';
-
-interface Props {
- children: JSX.Element;
- messages: FlashMessage[];
-}
-
-const flashObj = {
- info: 'addInfoToast',
- alert: 'addDangerToast',
- danger: 'addDangerToast',
- warning: 'addWarningToast',
- success: 'addSuccessToast',
-};
-
-export function FlashProvider({ children, messages }: Props) {
- const toasts = useToasts();
- useComponentDidMount(() => {
- messages.forEach(message => {
- const [type, text] = message;
- const flash = flashObj[type];
- const toast = toasts[flash as keyof typeof toasts];
- if (toast) {
- toast(text);
- }
- });
- });
- return children;
-}
-
-export type { FlashMessage };
diff --git a/superset-frontend/src/components/FlashProvider/types.ts
b/superset-frontend/src/components/FlashProvider/types.ts
deleted file mode 100644
index 8716eb985b..0000000000
--- a/superset-frontend/src/components/FlashProvider/types.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-/**
- * 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.
- */
-type FlashMessageType = 'info' | 'alert' | 'danger' | 'warning' | 'success';
-export type FlashMessage = [FlashMessageType, string];
diff --git a/superset-frontend/src/components/index.ts
b/superset-frontend/src/components/index.ts
index 09d50bb315..558f080261 100644
--- a/superset-frontend/src/components/index.ts
+++ b/superset-frontend/src/components/index.ts
@@ -38,7 +38,6 @@ export * from './ErrorMessage';
export { ImportModal, type ImportModelsModalProps } from './ImportModal';
export { ErrorBoundary, type ErrorBoundaryProps } from './ErrorBoundary';
export * from './GenericLink';
-export { FlashProvider, type FlashMessage } from './FlashProvider';
export { GridTable, type TableProps } from './GridTable';
export * from './Tag';
export * from './TagsList';
diff --git a/superset-frontend/src/constants.ts
b/superset-frontend/src/constants.ts
index 4ea7c276d9..faf3012906 100644
--- a/superset-frontend/src/constants.ts
+++ b/superset-frontend/src/constants.ts
@@ -122,7 +122,6 @@ export const RESERVED_DASHBOARD_URL_PARAMS: string[] = [
export const DEFAULT_COMMON_BOOTSTRAP_DATA: CommonBootstrapData = {
application_root: '/',
static_assets_prefix: '',
- flash_messages: [],
conf: {},
locale: 'en',
feature_flags: {},
diff --git a/superset-frontend/src/dashboard/actions/hydrate.js
b/superset-frontend/src/dashboard/actions/hydrate.js
index 99a74ddbd1..fe41d8aded 100644
--- a/superset-frontend/src/dashboard/actions/hydrate.js
+++ b/superset-frontend/src/dashboard/actions/hydrate.js
@@ -274,7 +274,6 @@ export const hydrateDashboard =
superset_can_csv: findPermission('can_csv', 'Superset', roles),
common: {
// legacy, please use state.common instead
- flash_messages: common?.flash_messages,
conf: common?.conf,
},
filterBarOrientation:
diff --git a/superset-frontend/src/embedded/EmbeddedContextProviders.tsx
b/superset-frontend/src/embedded/EmbeddedContextProviders.tsx
index 207a1d320d..34d1070e86 100644
--- a/superset-frontend/src/embedded/EmbeddedContextProviders.tsx
+++ b/superset-frontend/src/embedded/EmbeddedContextProviders.tsx
@@ -22,13 +22,12 @@ import { Provider as ReduxProvider } from 'react-redux';
import { QueryParamProvider } from 'use-query-params';
import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
-import { FlashProvider, DynamicPluginProvider } from 'src/components';
+import { DynamicPluginProvider } from 'src/components';
import { EmbeddedUiConfigProvider } from 'src/components/UiConfigContext';
import { SupersetThemeProvider } from 'src/theme/ThemeProvider';
import { ThemeController } from 'src/theme/ThemeController';
import type { ThemeStorage } from '@superset-ui/core';
import { store } from 'src/views/store';
-import getBootstrapData from 'src/utils/getBootstrapData';
/**
* In-memory implementation of ThemeStorage interface for embedded contexts.
@@ -56,7 +55,6 @@ const themeController = new ThemeController({
export const getThemeController = (): ThemeController => themeController;
-const { common } = getBootstrapData();
const extensionsRegistry = getExtensionsRegistry();
export const EmbeddedContextProviders: React.FC = ({ children }) => {
@@ -68,24 +66,22 @@ export const EmbeddedContextProviders: React.FC = ({
children }) => {
<SupersetThemeProvider themeController={themeController}>
<ReduxProvider store={store}>
<DndProvider backend={HTML5Backend}>
- <FlashProvider messages={common.flash_messages}>
- <EmbeddedUiConfigProvider>
- <DynamicPluginProvider>
- <QueryParamProvider
- ReactRouterRoute={Route}
- stringifyOptions={{ encode: false }}
- >
- {RootContextProviderExtension ? (
- <RootContextProviderExtension>
- {children}
- </RootContextProviderExtension>
- ) : (
- children
- )}
- </QueryParamProvider>
- </DynamicPluginProvider>
- </EmbeddedUiConfigProvider>
- </FlashProvider>
+ <EmbeddedUiConfigProvider>
+ <DynamicPluginProvider>
+ <QueryParamProvider
+ ReactRouterRoute={Route}
+ stringifyOptions={{ encode: false }}
+ >
+ {RootContextProviderExtension ? (
+ <RootContextProviderExtension>
+ {children}
+ </RootContextProviderExtension>
+ ) : (
+ children
+ )}
+ </QueryParamProvider>
+ </DynamicPluginProvider>
+ </EmbeddedUiConfigProvider>
</DndProvider>
</ReduxProvider>
</SupersetThemeProvider>
diff --git a/superset-frontend/src/explore/types.ts
b/superset-frontend/src/explore/types.ts
index f5a04a3f1d..fb2ce66596 100644
--- a/superset-frontend/src/explore/types.ts
+++ b/superset-frontend/src/explore/types.ts
@@ -98,7 +98,6 @@ export interface ExploreResponsePayload {
export interface ExplorePageState {
user: UserWithPermissionsAndRoles;
common: {
- flash_messages: string[];
conf: JsonObject;
locale: string;
};
diff --git a/superset-frontend/src/pages/Login/Login.test.tsx
b/superset-frontend/src/pages/Login/Login.test.tsx
index efe51be80c..6673dfdd99 100644
--- a/superset-frontend/src/pages/Login/Login.test.tsx
+++ b/superset-frontend/src/pages/Login/Login.test.tsx
@@ -33,7 +33,7 @@ jest.mock('src/utils/getBootstrapData', () => ({
}));
test('should render login form elements', () => {
- render(<Login />);
+ render(<Login />, { useRedux: true });
expect(screen.getByTestId('login-form')).toBeInTheDocument();
expect(screen.getByTestId('username-input')).toBeInTheDocument();
expect(screen.getByTestId('password-input')).toBeInTheDocument();
@@ -42,13 +42,13 @@ test('should render login form elements', () => {
});
test('should render username and password labels', () => {
- render(<Login />);
+ render(<Login />, { useRedux: true });
expect(screen.getByText('Username:')).toBeInTheDocument();
expect(screen.getByText('Password:')).toBeInTheDocument();
});
test('should render form instruction text', () => {
- render(<Login />);
+ render(<Login />, { useRedux: true });
expect(
screen.getByText('Enter your login and password below:'),
).toBeInTheDocument();
diff --git a/superset-frontend/src/pages/Login/index.tsx
b/superset-frontend/src/pages/Login/index.tsx
index 8c56a4c8c9..9bb10dfd1d 100644
--- a/superset-frontend/src/pages/Login/index.tsx
+++ b/superset-frontend/src/pages/Login/index.tsx
@@ -27,8 +27,10 @@ import {
Typography,
Icons,
} from '@superset-ui/core/components';
-import { useState } from 'react';
+import { useState, useEffect } from 'react';
import { capitalize } from 'lodash/fp';
+import { addDangerToast } from 'src/components/MessageToasts/actions';
+import { useDispatch } from 'react-redux';
import getBootstrapData from 'src/utils/getBootstrapData';
type OAuthProvider = {
@@ -77,6 +79,7 @@ const StyledLabel = styled(Typography.Text)`
export default function Login() {
const [form] = Form.useForm<LoginForm>();
const [loading, setLoading] = useState(false);
+ const dispatch = useDispatch();
const bootstrapData = getBootstrapData();
@@ -85,11 +88,28 @@ export default function Login() {
const authRegistration: boolean =
bootstrapData.common.conf.AUTH_USER_REGISTRATION;
+ // TODO: This is a temporary solution for showing login errors after form
submission.
+ // Should be replaced with proper SPA-style authentication (JSON API with
error responses)
+ // when Flask-AppBuilder is updated or we implement a custom login endpoint.
+ useEffect(() => {
+ const loginAttempted = sessionStorage.getItem('login_attempted');
+
+ if (loginAttempted === 'true') {
+ sessionStorage.removeItem('login_attempted');
+ dispatch(addDangerToast(t('Invalid username or password')));
+ // Clear password field for security
+ form.setFieldsValue({ password: '' });
+ }
+ }, [dispatch, form]);
+
const onFinish = (values: LoginForm) => {
setLoading(true);
- SupersetClient.postForm('/login/', values, '').finally(() => {
- setLoading(false);
- });
+
+ // Mark that we're attempting login (for error detection after redirect)
+ sessionStorage.setItem('login_attempted', 'true');
+
+ // Use standard form submission for Flask-AppBuilder compatibility
+ SupersetClient.postForm('/login/', values, '');
};
const getAuthIconElement = (
diff --git a/superset-frontend/src/theme/tests/ThemeController.test.ts
b/superset-frontend/src/theme/tests/ThemeController.test.ts
index e0ed88aa52..c7276132f3 100644
--- a/superset-frontend/src/theme/tests/ThemeController.test.ts
+++ b/superset-frontend/src/theme/tests/ThemeController.test.ts
@@ -83,7 +83,6 @@ const createMockBootstrapData = (
common: {
application_root: '/',
static_assets_prefix: '/static/assets/',
- flash_messages: [],
conf: {},
locale: 'en',
feature_flags: {},
@@ -391,7 +390,6 @@ describe('ThemeController', () => {
common: {
application_root: '/',
static_assets_prefix: '/static/assets/',
- flash_messages: [],
conf: {},
locale: 'en',
feature_flags: {},
diff --git a/superset-frontend/src/types/bootstrapTypes.ts
b/superset-frontend/src/types/bootstrapTypes.ts
index abe32d6583..1d8357a56e 100644
--- a/superset-frontend/src/types/bootstrapTypes.ts
+++ b/superset-frontend/src/types/bootstrapTypes.ts
@@ -20,7 +20,6 @@ import { FormatLocaleDefinition } from 'd3-format';
import { TimeLocaleDefinition } from 'd3-time-format';
import { isPlainObject } from 'lodash';
import { Languages } from 'src/features/home/LanguagePicker';
-import type { FlashMessage } from 'src/components';
import type {
AnyThemeConfig,
ColorSchemeConfig,
@@ -154,7 +153,6 @@ export interface BootstrapThemeDataConfig {
export interface CommonBootstrapData {
application_root: string;
static_assets_prefix: string;
- flash_messages: FlashMessage[];
conf: JsonObject;
locale: Locale;
feature_flags: FeatureFlagMap;
diff --git a/superset-frontend/src/views/RootContextProviders.tsx
b/superset-frontend/src/views/RootContextProviders.tsx
index fbff76b64c..c10f04ae81 100644
--- a/superset-frontend/src/views/RootContextProviders.tsx
+++ b/superset-frontend/src/views/RootContextProviders.tsx
@@ -23,8 +23,7 @@ import { Provider as ReduxProvider } from 'react-redux';
import { QueryParamProvider } from 'use-query-params';
import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
-import getBootstrapData from 'src/utils/getBootstrapData';
-import { FlashProvider, DynamicPluginProvider } from 'src/components';
+import { DynamicPluginProvider } from 'src/components';
import { EmbeddedUiConfigProvider } from 'src/components/UiConfigContext';
import { SupersetThemeProvider } from 'src/theme/ThemeProvider';
import { ThemeController } from 'src/theme/ThemeController';
@@ -32,7 +31,6 @@ import { ExtensionsProvider } from
'src/extensions/ExtensionsContext';
import { store } from './store';
import '../preamble';
-const { common } = getBootstrapData();
const themeController = new ThemeController();
const extensionsRegistry = getExtensionsRegistry();
@@ -45,26 +43,24 @@ export const RootContextProviders: React.FC = ({ children
}) => {
<SupersetThemeProvider themeController={themeController}>
<ReduxProvider store={store}>
<DndProvider backend={HTML5Backend}>
- <FlashProvider messages={common.flash_messages}>
- <EmbeddedUiConfigProvider>
- <DynamicPluginProvider>
- <QueryParamProvider
- ReactRouterRoute={Route}
- stringifyOptions={{ encode: false }}
- >
- <ExtensionsProvider>
- {RootContextProviderExtension ? (
- <RootContextProviderExtension>
- {children}
- </RootContextProviderExtension>
- ) : (
- children
- )}
- </ExtensionsProvider>
- </QueryParamProvider>
- </DynamicPluginProvider>
- </EmbeddedUiConfigProvider>
- </FlashProvider>
+ <EmbeddedUiConfigProvider>
+ <DynamicPluginProvider>
+ <QueryParamProvider
+ ReactRouterRoute={Route}
+ stringifyOptions={{ encode: false }}
+ >
+ <ExtensionsProvider>
+ {RootContextProviderExtension ? (
+ <RootContextProviderExtension>
+ {children}
+ </RootContextProviderExtension>
+ ) : (
+ children
+ )}
+ </ExtensionsProvider>
+ </QueryParamProvider>
+ </DynamicPluginProvider>
+ </EmbeddedUiConfigProvider>
</DndProvider>
</ReduxProvider>
</SupersetThemeProvider>
diff --git a/superset/views/auth.py b/superset/views/auth.py
index c24e0ae362..123333a639 100644
--- a/superset/views/auth.py
+++ b/superset/views/auth.py
@@ -18,9 +18,8 @@
import logging
from typing import Optional
-from flask import flash, g, redirect
+from flask import g, redirect
from flask_appbuilder import expose
-from flask_appbuilder._compat import as_unicode
from flask_appbuilder.const import LOGMSG_ERR_SEC_NO_REGISTER_HASH
from flask_appbuilder.security.decorators import no_cache
from flask_appbuilder.security.views import AuthView, WerkzeugResponse
@@ -66,7 +65,7 @@ class SupersetRegisterUserView(BaseSupersetView):
reg = self.appbuilder.sm.find_register_user(activation_hash)
if not reg:
logger.error(LOGMSG_ERR_SEC_NO_REGISTER_HASH, activation_hash)
- flash(as_unicode(self.false_error_message), "danger")
+ logger.error("Registration activation failed: %s",
self.false_error_message)
return redirect(self.appbuilder.get_url_for_index)
if not self.appbuilder.sm.add_user(
username=reg.username,
@@ -78,7 +77,7 @@ class SupersetRegisterUserView(BaseSupersetView):
),
hashed_password=reg.password,
):
- flash(as_unicode(self.error_message), "danger")
+ logger.error("User registration failed: %s", self.error_message)
return redirect(self.appbuilder.get_url_for_index)
else:
self.appbuilder.sm.del_register_user(reg)
diff --git a/superset/views/base.py b/superset/views/base.py
index b07d63f5cb..b0ad2c2e64 100644
--- a/superset/views/base.py
+++ b/superset/views/base.py
@@ -27,9 +27,7 @@ from babel import Locale
from flask import (
abort,
current_app as app,
- flash,
g,
- get_flashed_messages,
redirect,
Response,
session,
@@ -499,10 +497,7 @@ def cached_common_bootstrap_data( # pylint:
disable=unused-argument
def common_bootstrap_payload() -> dict[str, Any]:
- return {
- **cached_common_bootstrap_data(utils.get_user_id(), get_locale()),
- "flash_messages": get_flashed_messages(with_categories=True),
- }
+ return cached_common_bootstrap_data(utils.get_user_id(), get_locale())
def get_spa_payload(extra_data: dict[str, Any] | None = None) -> dict[str,
Any]:
@@ -597,7 +592,7 @@ class DeleteMixin: # pylint: disable=too-few-public-methods
try:
self.pre_delete(item)
except Exception as ex: # pylint: disable=broad-except
- flash(str(ex), "danger")
+ logger.error("Pre-delete error: %s", str(ex))
else:
view_menu = security_manager.find_view_menu(item.get_perm())
pvs = (
@@ -617,7 +612,6 @@ class DeleteMixin: # pylint: disable=too-few-public-methods
db.session.commit() # pylint:
disable=consider-using-transaction
- flash(*self.datamodel.message)
self.update_redirect()
@action(
@@ -630,7 +624,7 @@ class DeleteMixin: # pylint: disable=too-few-public-methods
try:
self.pre_delete(item)
except Exception as ex: # pylint: disable=broad-except
- flash(str(ex), "danger")
+ logger.error("Pre-delete error: %s", str(ex))
else:
self._delete(item.id)
self.update_redirect()
diff --git a/superset/views/core.py b/superset/views/core.py
index d52a01f356..140c0775aa 100755
--- a/superset/views/core.py
+++ b/superset/views/core.py
@@ -28,7 +28,6 @@ from urllib import parse
from flask import (
abort,
current_app as app,
- flash,
g,
redirect,
request,
@@ -109,7 +108,6 @@ from superset.views.utils import (
get_form_data,
get_viz,
loads_request_json,
- redirect_with_flash,
sanitize_datasource_data,
)
from superset.viz import BaseViz
@@ -415,7 +413,6 @@ class Superset(BaseSupersetView):
initial_form_data = {}
- form_data_key = request.args.get("form_data_key")
if key is not None:
command = GetExplorePermalinkCommand(key)
try:
@@ -430,9 +427,10 @@ class Superset(BaseSupersetView):
_("Error: permalink state not found"), status=404
)
except (ChartNotFoundError, ExplorePermalinkGetFailedError) as ex:
- flash(__("Error: %(msg)s", msg=ex.message), "danger")
- return redirect(url_for("SliceModelView.list"))
- elif form_data_key:
+ return json_error_response(
+ __("Error: %(msg)s", msg=ex.message), status=404
+ )
+ elif form_data_key := request.args.get("form_data_key"):
parameters = CommandParameters(key=form_data_key)
value = GetFormDataCommand(parameters).run()
initial_form_data = json.loads(value) if value else {}
@@ -442,18 +440,8 @@ class Superset(BaseSupersetView):
dataset_id = request.args.get("dataset_id")
if slice_id:
initial_form_data["slice_id"] = slice_id
- if form_data_key:
- flash(
- _("Form data not found in cache, reverting to chart
metadata.")
- )
elif dataset_id:
initial_form_data["datasource"] = f"{dataset_id}__table"
- if form_data_key:
- flash(
- _(
- "Form data not found in cache, reverting to
dataset metadata." # noqa: E501
- )
- )
form_data, slc = get_form_data(
use_slice_data=True, initial_form_data=initial_form_data
@@ -626,13 +614,9 @@ class Superset(BaseSupersetView):
if action == "saveas" and slice_add_perm:
ChartDAO.create(slc)
db.session.commit() # pylint: disable=consider-using-transaction
- msg = _("Chart [{}] has been saved").format(slc.slice_name)
- flash(msg, "success")
elif action == "overwrite" and slice_overwrite_perm:
ChartDAO.update(slc)
db.session.commit() # pylint: disable=consider-using-transaction
- msg = _("Chart [{}] has been overwritten").format(slc.slice_name)
- flash(msg, "success")
# Adding slice to a dashboard if requested
dash: Dashboard | None = None
@@ -654,13 +638,6 @@ class Superset(BaseSupersetView):
_("You don't have the rights to alter this dashboard"),
status=403,
)
-
- flash(
- _("Chart [{}] was added to dashboard [{}]").format(
- slc.slice_name, dash.dashboard_title
- ),
- "success",
- )
elif new_dashboard_name:
# Creating and adding to a new dashboard
# check create dashboard permissions
@@ -675,12 +652,6 @@ class Superset(BaseSupersetView):
dashboard_title=request.args.get("new_dashboard_name"),
owners=[g.user] if g.user else [],
)
- flash(
- _(
- "Dashboard [{}] just got created and chart [{}] was added
to it"
- ).format(dash.dashboard_title, slc.slice_name),
- "success",
- )
if dash and slc not in dash.slices:
dash.slices.append(slc)
@@ -798,19 +769,9 @@ class Superset(BaseSupersetView):
try:
dashboard.raise_for_access()
- except SupersetSecurityException as ex:
- # anonymous users should get the login screen, others should go to
dashboard list # noqa: E501
- if g.user is None or g.user.is_anonymous:
- redirect_url =
f"{appbuilder.get_url_for_login}?next={request.url}"
- warn_msg = "Users must be logged in to view this dashboard."
- else:
- redirect_url = url_for("DashboardModelView.list")
- warn_msg = utils.error_msg_from_exception(ex)
- return redirect_with_flash(
- url=redirect_url,
- message=warn_msg,
- category="danger",
- )
+ except SupersetSecurityException:
+ # Return 404 to avoid revealing dashboard existence
+ return Response(status=404)
add_extra_log_payload(
dashboard_id=dashboard.id,
dashboard_version="v2",
@@ -841,12 +802,8 @@ class Superset(BaseSupersetView):
) -> FlaskResponse:
try:
value = GetDashboardPermalinkCommand(key).run()
- except DashboardPermalinkGetFailedError as ex:
- flash(__("Error: %(msg)s", msg=ex.message), "danger")
- return redirect(url_for("DashboardModelView.list"))
- except DashboardAccessDeniedError as ex:
- flash(__("Error: %(msg)s", msg=ex.message), "danger")
- return redirect(url_for("DashboardModelView.list"))
+ except (DashboardPermalinkGetFailedError, DashboardAccessDeniedError)
as ex:
+ return json_error_response(__("Error: %(msg)s", msg=ex.message),
status=404)
if not value:
return json_error_response(_("permalink state not found"),
status=404)
diff --git a/superset/views/utils.py b/superset/views/utils.py
index a431c307fb..cbe5ef0dcf 100644
--- a/superset/views/utils.py
+++ b/superset/views/utils.py
@@ -22,12 +22,11 @@ from typing import Any, Callable, DefaultDict, Optional,
Union
import msgpack
import pyarrow as pa
-from flask import current_app as app, flash, g, has_request_context, redirect,
request
+from flask import current_app as app, g, has_request_context, request
from flask_appbuilder.security.sqla import models as ab_models
from flask_appbuilder.security.sqla.models import User
from flask_babel import _
from sqlalchemy.exc import NoResultFound
-from werkzeug.wrappers.response import Response
from superset import dataframe, db, result_set, viz
from superset.common.db_query_status import QueryStatus
@@ -551,8 +550,3 @@ def get_cta_schema_name(
if not func:
return None
return func(database, user, schema, sql)
-
-
-def redirect_with_flash(url: str, message: str, category: str) -> Response:
- flash(message=message, category=category)
- return redirect(url)
diff --git a/tests/integration_tests/dashboards/security/security_rbac_tests.py
b/tests/integration_tests/dashboards/security/security_rbac_tests.py
index 4754854047..c6ea811636 100644
--- a/tests/integration_tests/dashboards/security/security_rbac_tests.py
+++ b/tests/integration_tests/dashboards/security/security_rbac_tests.py
@@ -108,7 +108,7 @@ class
TestDashboardRoleBasedSecurity(BaseTestDashboardSecurity):
# act
response = self.get_dashboard_view_response(dashboard_to_access)
- assert response.status_code == 302
+ assert response.status_code == 404
request_payload = get_query_context("birth_names")
rv = self.post_assert_metric(CHART_DATA_URI, request_payload, "data")
@@ -129,7 +129,7 @@ class
TestDashboardRoleBasedSecurity(BaseTestDashboardSecurity):
response = self.get_dashboard_view_response(dashboard_to_access)
# assert
- assert response.status_code == 302
+ assert response.status_code == 404
# post
revoke_access_to_dashboard(dashboard_to_access, new_role) # noqa: F405
@@ -147,9 +147,9 @@ class
TestDashboardRoleBasedSecurity(BaseTestDashboardSecurity):
dashboard = create_dashboard_to_db(published=True, slices=[slice])
self.login(GAMMA_USERNAME)
- # assert redirect on regular rbac access denied
+ # assert 404 on regular rbac access denied (prevents information
leakage)
response = self.get_dashboard_view_response(dashboard)
- assert response.status_code == 302
+ assert response.status_code == 404
request_payload = get_query_context("birth_names")
rv = self.post_assert_metric(CHART_DATA_URI, request_payload, "data")
@@ -221,7 +221,7 @@ class
TestDashboardRoleBasedSecurity(BaseTestDashboardSecurity):
response = self.get_dashboard_view_response(dashboard_to_access)
# assert
- assert response.status_code == 302
+ assert response.status_code == 404
@pytest.mark.usefixtures("public_role_like_gamma")
def
test_get_dashboard_view__public_user_with_dashboard_permission_can_not_access_draft(
# noqa: E501
@@ -234,7 +234,7 @@ class
TestDashboardRoleBasedSecurity(BaseTestDashboardSecurity):
response = self.get_dashboard_view_response(dashboard_to_access)
# assert
- assert response.status_code == 302
+ assert response.status_code == 404
# post
revoke_access_to_dashboard(dashboard_to_access, "Public") # noqa: F405