feat(embedded): add feature flag to disable logout button in embedded contexts (#37537)

Co-authored-by: richard <richard@richards-MacBook-Pro-2.local>
This commit is contained in:
Richard Fogaca Nienkotter
2026-02-23 17:56:02 -03:00
committed by GitHub
parent c4eb7de6de
commit e06427d1ef
6 changed files with 138 additions and 10 deletions

View File

@@ -96,6 +96,24 @@ To enable this entry, add the following line to the `.env` file:
SUPERSET_FEATURE_EMBEDDED_SUPERSET=true
```
### Hiding the Logout Button in Embedded Contexts
When Superset is embedded in an application that manages authentication via SSO (OAuth2, SAML, or JWT), the logout button should be hidden since session management is handled by the parent application.
To hide the logout button in embedded contexts, add to `superset_config.py`:
```python
FEATURE_FLAGS = {
"DISABLE_EMBEDDED_SUPERSET_LOGOUT": True,
}
```
This flag only hides the logout button when Superset detects it is running inside an iframe. Users accessing Superset directly (not embedded) will still see the logout button regardless of this setting.
:::note
When embedding with SSO, also set `SESSION_COOKIE_SAMESITE = 'None'` and `SESSION_COOKIE_SECURE = True`. See [Security documentation](/docs/security/securing_superset) for details.
:::
## CSRF settings
Similarly, [flask-wtf](https://flask-wtf.readthedocs.io/en/0.15.x/config/) is used to manage

View File

@@ -261,6 +261,14 @@
"description": "Data panel closed by default in chart builder",
"category": "runtime_config"
},
{
"name": "DISABLE_EMBEDDED_SUPERSET_LOGOUT",
"default": false,
"lifecycle": "stable",
"description": "Hide the logout button in embedded contexts (e.g., when using SSO in iframes)",
"docs": "https://superset.apache.org/docs/configuration/networking-settings#hiding-the-logout-button-in-embedded-contexts",
"category": "runtime_config"
},
{
"name": "DRILL_BY",
"default": true,

View File

@@ -39,6 +39,7 @@ export enum FeatureFlag {
DatapanelClosedByDefault = 'DATAPANEL_CLOSED_BY_DEFAULT',
DatasetFolders = 'DATASET_FOLDERS',
DateRangeTimeshiftsEnabled = 'DATE_RANGE_TIMESHIFTS_ENABLED',
DisableEmbeddedSupersetLogout = 'DISABLE_EMBEDDED_SUPERSET_LOGOUT',
/** @deprecated */
DrillToDetail = 'DRILL_TO_DETAIL',
DrillBy = 'DRILL_BY',

View File

@@ -24,9 +24,26 @@ import {
userEvent,
waitFor,
} from 'spec/helpers/testing-library';
import { isFeatureEnabled, FeatureFlag } from '@superset-ui/core';
import { isEmbedded } from 'src/dashboard/util/isEmbedded';
import RightMenu from './RightMenu';
import { GlobalMenuDataOptions, RightMenuProps } from './types';
jest.mock('@superset-ui/core', () => ({
...jest.requireActual('@superset-ui/core'),
isFeatureEnabled: jest.fn(),
}));
const mockIsFeatureEnabled = isFeatureEnabled as jest.MockedFunction<
typeof isFeatureEnabled
>;
jest.mock('src/dashboard/util/isEmbedded', () => ({
isEmbedded: jest.fn(() => false),
}));
const mockIsEmbedded = isEmbedded as jest.MockedFunction<typeof isEmbedded>;
jest.mock('react-redux', () => ({
...jest.requireActual('react-redux'),
useSelector: jest.fn(),
@@ -160,6 +177,8 @@ const getDatabaseWithNameFilterMockUrl =
'glob:*api/v1/database/?q=(filters:!((col:database_name,opr:neq,value:examples)))';
beforeEach(async () => {
mockIsFeatureEnabled.mockReturnValue(false);
mockIsEmbedded.mockReturnValue(false);
useSelectorMock.mockReset();
fetchMock.get(
getDatabaseWithFileFiterMockUrl,
@@ -393,3 +412,67 @@ test('Logs out and clears local storage item redux', async () => {
expect(sessionStorage.getItem('login_attempted')).toBeNull();
});
});
test('shows logout button when not embedded', async () => {
mockIsEmbedded.mockReturnValue(false);
mockIsFeatureEnabled.mockReturnValue(false);
resetUseSelectorMock();
render(<RightMenu {...createProps()} />, {
useRedux: true,
useQueryParams: true,
useRouter: true,
useTheme: true,
});
userEvent.hover(await screen.findByText(/Settings/i));
expect(await screen.findByText('Logout')).toBeInTheDocument();
});
test('shows logout button when embedded but flag is disabled', async () => {
mockIsEmbedded.mockReturnValue(true);
mockIsFeatureEnabled.mockReturnValue(false);
resetUseSelectorMock();
render(<RightMenu {...createProps()} />, {
useRedux: true,
useQueryParams: true,
useRouter: true,
useTheme: true,
});
userEvent.hover(await screen.findByText(/Settings/i));
expect(await screen.findByText('Logout')).toBeInTheDocument();
});
test('shows logout button when not embedded even if flag is enabled', async () => {
mockIsEmbedded.mockReturnValue(false);
mockIsFeatureEnabled.mockImplementation(
(flag: FeatureFlag) => flag === FeatureFlag.DisableEmbeddedSupersetLogout,
);
resetUseSelectorMock();
render(<RightMenu {...createProps()} />, {
useRedux: true,
useQueryParams: true,
useRouter: true,
useTheme: true,
});
userEvent.hover(await screen.findByText(/Settings/i));
expect(await screen.findByText('Logout')).toBeInTheDocument();
});
test('hides logout button when embedded and flag is enabled', async () => {
mockIsEmbedded.mockReturnValue(true);
mockIsFeatureEnabled.mockImplementation(
(flag: FeatureFlag) => flag === FeatureFlag.DisableEmbeddedSupersetLogout,
);
resetUseSelectorMock();
render(<RightMenu {...createProps()} />, {
useRedux: true,
useQueryParams: true,
useRouter: true,
useTheme: true,
});
userEvent.hover(await screen.findByText(/Settings/i));
expect(screen.queryByText('Logout')).not.toBeInTheDocument();
});

View File

@@ -23,7 +23,12 @@ import { Link } from 'react-router-dom';
import { useQueryParams, BooleanParam } from 'use-query-params';
import { isEmpty } from 'lodash';
import { t } from '@apache-superset/core';
import { SupersetClient, getExtensionsRegistry } from '@superset-ui/core';
import {
SupersetClient,
getExtensionsRegistry,
isFeatureEnabled,
FeatureFlag,
} from '@superset-ui/core';
import { styled, css, SupersetTheme, useTheme } from '@apache-superset/core/ui';
import {
Tag,
@@ -35,6 +40,7 @@ import {
} from '@superset-ui/core/components';
import type { ItemType, MenuItem } from '@superset-ui/core/components/Menu';
import { ensureAppRoot, makeUrl } from 'src/utils/pathUtils';
import { isEmbedded } from 'src/dashboard/util/isEmbedded';
import { findPermission } from 'src/utils/findPermission';
import { isUserAdmin } from 'src/dashboard/util/permissionUtils';
import {
@@ -489,15 +495,22 @@ const RightMenu = ({
),
});
}
userItems.push({
key: 'logout',
label: (
<Typography.Link href={navbarRight.user_logout_url}>
{t('Logout')}
</Typography.Link>
),
onClick: handleLogout,
});
const showLogout =
!isEmbedded() ||
!isFeatureEnabled(FeatureFlag.DisableEmbeddedSupersetLogout);
if (showLogout) {
userItems.push({
key: 'logout',
label: (
<Typography.Link
href={ensureAppRoot(navbarRight.user_logout_url)}
>
{t('Logout')}
</Typography.Link>
),
onClick: handleLogout,
});
}
items.push({
type: 'group',

View File

@@ -714,6 +714,11 @@ DEFAULT_FEATURE_FLAGS: dict[str, bool] = {
# @lifecycle: stable
# @category: runtime_config
"DATAPANEL_CLOSED_BY_DEFAULT": False,
# Hide the logout button in embedded contexts (e.g., when using SSO in iframes)
# @lifecycle: stable
# @category: runtime_config
# @docs: https://superset.apache.org/docs/configuration/networking-settings#hiding-the-logout-button-in-embedded-contexts
"DISABLE_EMBEDDED_SUPERSET_LOGOUT": False,
# Enable drill-by functionality in charts
# @lifecycle: stable
# @category: runtime_config