Skip to content

Commit e06427d

Browse files
richardfogacarichard
andauthored
feat(embedded): add feature flag to disable logout button in embedded contexts (apache#37537)
Co-authored-by: richard <richard@richards-MacBook-Pro-2.local>
1 parent c4eb7de commit e06427d

File tree

6 files changed

+138
-10
lines changed

6 files changed

+138
-10
lines changed

docs/docs/configuration/networking-settings.mdx

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,24 @@ To enable this entry, add the following line to the `.env` file:
9696
SUPERSET_FEATURE_EMBEDDED_SUPERSET=true
9797
```
9898

99+
### Hiding the Logout Button in Embedded Contexts
100+
101+
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.
102+
103+
To hide the logout button in embedded contexts, add to `superset_config.py`:
104+
105+
```python
106+
FEATURE_FLAGS = {
107+
"DISABLE_EMBEDDED_SUPERSET_LOGOUT": True,
108+
}
109+
```
110+
111+
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.
112+
113+
:::note
114+
When embedding with SSO, also set `SESSION_COOKIE_SAMESITE = 'None'` and `SESSION_COOKIE_SECURE = True`. See [Security documentation](/docs/security/securing_superset) for details.
115+
:::
116+
99117
## CSRF settings
100118

101119
Similarly, [flask-wtf](https://flask-wtf.readthedocs.io/en/0.15.x/config/) is used to manage

docs/static/feature-flags.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,14 @@
261261
"description": "Data panel closed by default in chart builder",
262262
"category": "runtime_config"
263263
},
264+
{
265+
"name": "DISABLE_EMBEDDED_SUPERSET_LOGOUT",
266+
"default": false,
267+
"lifecycle": "stable",
268+
"description": "Hide the logout button in embedded contexts (e.g., when using SSO in iframes)",
269+
"docs": "https://superset.apache.org/docs/configuration/networking-settings#hiding-the-logout-button-in-embedded-contexts",
270+
"category": "runtime_config"
271+
},
264272
{
265273
"name": "DRILL_BY",
266274
"default": true,

superset-frontend/packages/superset-ui-core/src/utils/featureFlags.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ export enum FeatureFlag {
3939
DatapanelClosedByDefault = 'DATAPANEL_CLOSED_BY_DEFAULT',
4040
DatasetFolders = 'DATASET_FOLDERS',
4141
DateRangeTimeshiftsEnabled = 'DATE_RANGE_TIMESHIFTS_ENABLED',
42+
DisableEmbeddedSupersetLogout = 'DISABLE_EMBEDDED_SUPERSET_LOGOUT',
4243
/** @deprecated */
4344
DrillToDetail = 'DRILL_TO_DETAIL',
4445
DrillBy = 'DRILL_BY',

superset-frontend/src/features/home/RightMenu.test.tsx

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,26 @@ import {
2424
userEvent,
2525
waitFor,
2626
} from 'spec/helpers/testing-library';
27+
import { isFeatureEnabled, FeatureFlag } from '@superset-ui/core';
28+
import { isEmbedded } from 'src/dashboard/util/isEmbedded';
2729
import RightMenu from './RightMenu';
2830
import { GlobalMenuDataOptions, RightMenuProps } from './types';
2931

32+
jest.mock('@superset-ui/core', () => ({
33+
...jest.requireActual('@superset-ui/core'),
34+
isFeatureEnabled: jest.fn(),
35+
}));
36+
37+
const mockIsFeatureEnabled = isFeatureEnabled as jest.MockedFunction<
38+
typeof isFeatureEnabled
39+
>;
40+
41+
jest.mock('src/dashboard/util/isEmbedded', () => ({
42+
isEmbedded: jest.fn(() => false),
43+
}));
44+
45+
const mockIsEmbedded = isEmbedded as jest.MockedFunction<typeof isEmbedded>;
46+
3047
jest.mock('react-redux', () => ({
3148
...jest.requireActual('react-redux'),
3249
useSelector: jest.fn(),
@@ -160,6 +177,8 @@ const getDatabaseWithNameFilterMockUrl =
160177
'glob:*api/v1/database/?q=(filters:!((col:database_name,opr:neq,value:examples)))';
161178

162179
beforeEach(async () => {
180+
mockIsFeatureEnabled.mockReturnValue(false);
181+
mockIsEmbedded.mockReturnValue(false);
163182
useSelectorMock.mockReset();
164183
fetchMock.get(
165184
getDatabaseWithFileFiterMockUrl,
@@ -393,3 +412,67 @@ test('Logs out and clears local storage item redux', async () => {
393412
expect(sessionStorage.getItem('login_attempted')).toBeNull();
394413
});
395414
});
415+
416+
test('shows logout button when not embedded', async () => {
417+
mockIsEmbedded.mockReturnValue(false);
418+
mockIsFeatureEnabled.mockReturnValue(false);
419+
resetUseSelectorMock();
420+
render(<RightMenu {...createProps()} />, {
421+
useRedux: true,
422+
useQueryParams: true,
423+
useRouter: true,
424+
useTheme: true,
425+
});
426+
427+
userEvent.hover(await screen.findByText(/Settings/i));
428+
expect(await screen.findByText('Logout')).toBeInTheDocument();
429+
});
430+
431+
test('shows logout button when embedded but flag is disabled', async () => {
432+
mockIsEmbedded.mockReturnValue(true);
433+
mockIsFeatureEnabled.mockReturnValue(false);
434+
resetUseSelectorMock();
435+
render(<RightMenu {...createProps()} />, {
436+
useRedux: true,
437+
useQueryParams: true,
438+
useRouter: true,
439+
useTheme: true,
440+
});
441+
442+
userEvent.hover(await screen.findByText(/Settings/i));
443+
expect(await screen.findByText('Logout')).toBeInTheDocument();
444+
});
445+
446+
test('shows logout button when not embedded even if flag is enabled', async () => {
447+
mockIsEmbedded.mockReturnValue(false);
448+
mockIsFeatureEnabled.mockImplementation(
449+
(flag: FeatureFlag) => flag === FeatureFlag.DisableEmbeddedSupersetLogout,
450+
);
451+
resetUseSelectorMock();
452+
render(<RightMenu {...createProps()} />, {
453+
useRedux: true,
454+
useQueryParams: true,
455+
useRouter: true,
456+
useTheme: true,
457+
});
458+
459+
userEvent.hover(await screen.findByText(/Settings/i));
460+
expect(await screen.findByText('Logout')).toBeInTheDocument();
461+
});
462+
463+
test('hides logout button when embedded and flag is enabled', async () => {
464+
mockIsEmbedded.mockReturnValue(true);
465+
mockIsFeatureEnabled.mockImplementation(
466+
(flag: FeatureFlag) => flag === FeatureFlag.DisableEmbeddedSupersetLogout,
467+
);
468+
resetUseSelectorMock();
469+
render(<RightMenu {...createProps()} />, {
470+
useRedux: true,
471+
useQueryParams: true,
472+
useRouter: true,
473+
useTheme: true,
474+
});
475+
476+
userEvent.hover(await screen.findByText(/Settings/i));
477+
expect(screen.queryByText('Logout')).not.toBeInTheDocument();
478+
});

superset-frontend/src/features/home/RightMenu.tsx

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,12 @@ import { Link } from 'react-router-dom';
2323
import { useQueryParams, BooleanParam } from 'use-query-params';
2424
import { isEmpty } from 'lodash';
2525
import { t } from '@apache-superset/core';
26-
import { SupersetClient, getExtensionsRegistry } from '@superset-ui/core';
26+
import {
27+
SupersetClient,
28+
getExtensionsRegistry,
29+
isFeatureEnabled,
30+
FeatureFlag,
31+
} from '@superset-ui/core';
2732
import { styled, css, SupersetTheme, useTheme } from '@apache-superset/core/ui';
2833
import {
2934
Tag,
@@ -35,6 +40,7 @@ import {
3540
} from '@superset-ui/core/components';
3641
import type { ItemType, MenuItem } from '@superset-ui/core/components/Menu';
3742
import { ensureAppRoot, makeUrl } from 'src/utils/pathUtils';
43+
import { isEmbedded } from 'src/dashboard/util/isEmbedded';
3844
import { findPermission } from 'src/utils/findPermission';
3945
import { isUserAdmin } from 'src/dashboard/util/permissionUtils';
4046
import {
@@ -489,15 +495,22 @@ const RightMenu = ({
489495
),
490496
});
491497
}
492-
userItems.push({
493-
key: 'logout',
494-
label: (
495-
<Typography.Link href={navbarRight.user_logout_url}>
496-
{t('Logout')}
497-
</Typography.Link>
498-
),
499-
onClick: handleLogout,
500-
});
498+
const showLogout =
499+
!isEmbedded() ||
500+
!isFeatureEnabled(FeatureFlag.DisableEmbeddedSupersetLogout);
501+
if (showLogout) {
502+
userItems.push({
503+
key: 'logout',
504+
label: (
505+
<Typography.Link
506+
href={ensureAppRoot(navbarRight.user_logout_url)}
507+
>
508+
{t('Logout')}
509+
</Typography.Link>
510+
),
511+
onClick: handleLogout,
512+
});
513+
}
501514

502515
items.push({
503516
type: 'group',

superset/config.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -714,6 +714,11 @@ class D3TimeFormat(TypedDict, total=False):
714714
# @lifecycle: stable
715715
# @category: runtime_config
716716
"DATAPANEL_CLOSED_BY_DEFAULT": False,
717+
# Hide the logout button in embedded contexts (e.g., when using SSO in iframes)
718+
# @lifecycle: stable
719+
# @category: runtime_config
720+
# @docs: https://superset.apache.org/docs/configuration/networking-settings#hiding-the-logout-button-in-embedded-contexts
721+
"DISABLE_EMBEDDED_SUPERSET_LOGOUT": False,
717722
# Enable drill-by functionality in charts
718723
# @lifecycle: stable
719724
# @category: runtime_config

0 commit comments

Comments
 (0)