diff --git a/docs/docs/configuration/networking-settings.mdx b/docs/docs/configuration/networking-settings.mdx index 74f13ff7986..59017fa9612 100644 --- a/docs/docs/configuration/networking-settings.mdx +++ b/docs/docs/configuration/networking-settings.mdx @@ -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 diff --git a/docs/static/feature-flags.json b/docs/static/feature-flags.json index 6ca4c2ea818..227d529c1db 100644 --- a/docs/static/feature-flags.json +++ b/docs/static/feature-flags.json @@ -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, diff --git a/superset-frontend/packages/superset-ui-core/src/utils/featureFlags.ts b/superset-frontend/packages/superset-ui-core/src/utils/featureFlags.ts index 57bda77b7db..9770951342b 100644 --- a/superset-frontend/packages/superset-ui-core/src/utils/featureFlags.ts +++ b/superset-frontend/packages/superset-ui-core/src/utils/featureFlags.ts @@ -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', diff --git a/superset-frontend/src/features/home/RightMenu.test.tsx b/superset-frontend/src/features/home/RightMenu.test.tsx index 851aaf2e224..4c15c585ad6 100644 --- a/superset-frontend/src/features/home/RightMenu.test.tsx +++ b/superset-frontend/src/features/home/RightMenu.test.tsx @@ -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; + 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(, { + 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(, { + 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(, { + 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(, { + useRedux: true, + useQueryParams: true, + useRouter: true, + useTheme: true, + }); + + userEvent.hover(await screen.findByText(/Settings/i)); + expect(screen.queryByText('Logout')).not.toBeInTheDocument(); +}); diff --git a/superset-frontend/src/features/home/RightMenu.tsx b/superset-frontend/src/features/home/RightMenu.tsx index dcca12f0304..553908f2550 100644 --- a/superset-frontend/src/features/home/RightMenu.tsx +++ b/superset-frontend/src/features/home/RightMenu.tsx @@ -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: ( - - {t('Logout')} - - ), - onClick: handleLogout, - }); + const showLogout = + !isEmbedded() || + !isFeatureEnabled(FeatureFlag.DisableEmbeddedSupersetLogout); + if (showLogout) { + userItems.push({ + key: 'logout', + label: ( + + {t('Logout')} + + ), + onClick: handleLogout, + }); + } items.push({ type: 'group', diff --git a/superset/config.py b/superset/config.py index c4cae11114e..dfeafb3b261 100644 --- a/superset/config.py +++ b/superset/config.py @@ -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