diff --git a/docs/docs/configuration/networking-settings.mdx b/docs/docs/configuration/networking-settings.mdx index 74f13ff79863..59017fa9612a 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 6ca4c2ea8187..227d529c1db0 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 57bda77b7dbb..9770951342b5 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 851aaf2e224d..4c15c585ad6c 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 dcca12f03049..553908f2550e 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 423f6fb3bde9..23973d89fb14 100644 --- a/superset/config.py +++ b/superset/config.py @@ -714,6 +714,11 @@ class D3TimeFormat(TypedDict, total=False): # @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