mirror of
https://github.com/apache/superset.git
synced 2026-04-07 10:31:50 +00:00
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:
committed by
GitHub
parent
c4eb7de6de
commit
e06427d1ef
@@ -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
|
||||
|
||||
8
docs/static/feature-flags.json
vendored
8
docs/static/feature-flags.json
vendored
@@ -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,
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user