diff --git a/x-pack/plugins/data_enhanced/public/search/ui/connected_background_session_indicator/connected_background_session_indicator.test.tsx b/x-pack/plugins/data_enhanced/public/search/ui/connected_background_session_indicator/connected_background_session_indicator.test.tsx index e08773c6a8a76..6fa9abd0f1ab6 100644 --- a/x-pack/plugins/data_enhanced/public/search/ui/connected_background_session_indicator/connected_background_session_indicator.test.tsx +++ b/x-pack/plugins/data_enhanced/public/search/ui/connected_background_session_indicator/connected_background_session_indicator.test.tsx @@ -57,8 +57,35 @@ test('should show indicator in case there is an active search session', async () await waitFor(() => getByTestId('backgroundSessionIndicator')); }); +test('should be disabled when permissions are off', async () => { + const state$ = new BehaviorSubject(SessionState.Loading); + coreStart.application.currentAppId$ = new BehaviorSubject('discover'); + (coreStart.application.capabilities as any) = { + discover: { + storeSearchSession: false, + }, + }; + const BackgroundSessionIndicator = createConnectedBackgroundSessionIndicator({ + sessionService: { ...sessionService, state$ }, + application: coreStart.application, + timeFilter, + }); + + render(); + + await waitFor(() => screen.getByTestId('backgroundSessionIndicator')); + + expect(screen.getByTestId('backgroundSessionIndicator').querySelector('button')).toBeDisabled(); +}); + test('should be disabled during auto-refresh', async () => { const state$ = new BehaviorSubject(SessionState.Loading); + coreStart.application.currentAppId$ = new BehaviorSubject('discover'); + (coreStart.application.capabilities as any) = { + discover: { + storeSearchSession: true, + }, + }; const BackgroundSessionIndicator = createConnectedBackgroundSessionIndicator({ sessionService: { ...sessionService, state$ }, application: coreStart.application, diff --git a/x-pack/plugins/data_enhanced/public/search/ui/connected_background_session_indicator/connected_background_session_indicator.tsx b/x-pack/plugins/data_enhanced/public/search/ui/connected_background_session_indicator/connected_background_session_indicator.tsx index b80295d87d202..1469c96d7166e 100644 --- a/x-pack/plugins/data_enhanced/public/search/ui/connected_background_session_indicator/connected_background_session_indicator.tsx +++ b/x-pack/plugins/data_enhanced/public/search/ui/connected_background_session_indicator/connected_background_session_indicator.tsx @@ -29,12 +29,35 @@ export const createConnectedBackgroundSessionIndicator = ({ .getRefreshIntervalUpdate$() .pipe(map(isAutoRefreshEnabled), distinctUntilChanged()); + const getCapabilitiesByAppId = ( + capabilities: ApplicationStart['capabilities'], + appId?: string + ) => { + switch (appId) { + case 'dashboards': + return capabilities.dashboard; + case 'discover': + return capabilities.discover; + default: + return undefined; + } + }; + return () => { const state = useObservable(sessionService.state$.pipe(debounceTime(500))); const autoRefreshEnabled = useObservable(isAutoRefreshEnabled$, isAutoRefreshEnabled()); + const appId = useObservable(application.currentAppId$, undefined); + let disabled = false; let disabledReasonText: string = ''; + if (getCapabilitiesByAppId(application.capabilities, appId)?.storeSearchSession !== true) { + disabled = true; + disabledReasonText = i18n.translate('xpack.data.backgroundSessionIndicator.noCapability', { + defaultMessage: "You don't have permissions to send to background.", + }); + } + if (autoRefreshEnabled) { disabled = true; disabledReasonText = i18n.translate( diff --git a/x-pack/plugins/features/server/__snapshots__/oss_features.test.ts.snap b/x-pack/plugins/features/server/__snapshots__/oss_features.test.ts.snap index 680429a4f5946..b1d7a2d434968 100644 --- a/x-pack/plugins/features/server/__snapshots__/oss_features.test.ts.snap +++ b/x-pack/plugins/features/server/__snapshots__/oss_features.test.ts.snap @@ -73,6 +73,7 @@ Array [ "dashboard", "query", "url", + "background-session", ], "read": Array [ "index-pattern", @@ -83,7 +84,6 @@ Array [ "lens", "map", "tag", - "background-session", ], }, "ui": Array [ @@ -92,6 +92,7 @@ Array [ "showWriteControls", "saveQuery", "createShortUrl", + "storeSearchSession", ], }, "privilegeId": "all", @@ -205,8 +206,8 @@ Array [ "search", "query", "index-pattern", - "background-session", "url", + "background-session", ], "read": Array [], }, @@ -215,6 +216,7 @@ Array [ "save", "saveQuery", "createShortUrl", + "storeSearchSession", ], }, "privilegeId": "all", @@ -557,6 +559,7 @@ Array [ "dashboard", "query", "url", + "background-session", ], "read": Array [ "index-pattern", @@ -567,7 +570,6 @@ Array [ "lens", "map", "tag", - "background-session", ], }, "ui": Array [ @@ -576,6 +578,7 @@ Array [ "showWriteControls", "saveQuery", "createShortUrl", + "storeSearchSession", ], }, "privilegeId": "all", @@ -689,8 +692,8 @@ Array [ "search", "query", "index-pattern", - "background-session", "url", + "background-session", ], "read": Array [], }, @@ -699,6 +702,7 @@ Array [ "save", "saveQuery", "createShortUrl", + "storeSearchSession", ], }, "privilegeId": "all", diff --git a/x-pack/plugins/features/server/oss_features.ts b/x-pack/plugins/features/server/oss_features.ts index 209e26821aedd..c38fdf8b29d12 100644 --- a/x-pack/plugins/features/server/oss_features.ts +++ b/x-pack/plugins/features/server/oss_features.ts @@ -28,7 +28,7 @@ export const buildOSSFeatures = ({ savedObjectTypes, includeTimelion }: BuildOSS app: ['discover', 'kibana'], catalogue: ['discover'], savedObject: { - all: ['search', 'query', 'index-pattern', 'background-session'], + all: ['search', 'query', 'index-pattern'], read: [], }, ui: ['show', 'save', 'saveQuery'], @@ -71,6 +71,33 @@ export const buildOSSFeatures = ({ savedObjectTypes, includeTimelion }: BuildOSS }, ], }, + { + name: i18n.translate('xpack.features.ossFeatures.discoverSearchSessionsFeatureName', { + defaultMessage: 'Store Search Sessions', + }), + privilegeGroups: [ + { + groupType: 'independent', + privileges: [ + { + id: 'store_search_session', + name: i18n.translate( + 'xpack.features.ossFeatures.discoverStoreSearchSessionsPrivilegeName', + { + defaultMessage: 'Store Search Sessions', + } + ), + includeIn: 'all', + savedObject: { + all: ['background-session'], + read: [], + }, + ui: ['storeSearchSession'], + }, + ], + }, + ], + }, ], }, { @@ -156,7 +183,6 @@ export const buildOSSFeatures = ({ savedObjectTypes, includeTimelion }: BuildOSS 'lens', 'map', 'tag', - 'background-session', ], }, ui: ['createNew', 'show', 'showWriteControls', 'saveQuery'], @@ -210,6 +236,33 @@ export const buildOSSFeatures = ({ savedObjectTypes, includeTimelion }: BuildOSS }, ], }, + { + name: i18n.translate('xpack.features.ossFeatures.dashboardSearchSessionsFeatureName', { + defaultMessage: 'Store Search Sessions', + }), + privilegeGroups: [ + { + groupType: 'independent', + privileges: [ + { + id: 'store_search_session', + name: i18n.translate( + 'xpack.features.ossFeatures.dashboardStoreSearchSessionsPrivilegeName', + { + defaultMessage: 'Store Search Sessions', + } + ), + includeIn: 'all', + savedObject: { + all: ['background-session'], + read: [], + }, + ui: ['storeSearchSession'], + }, + ], + }, + ], + }, ], }, { diff --git a/x-pack/test/api_integration/apis/security/license_downgrade.ts b/x-pack/test/api_integration/apis/security/license_downgrade.ts index 1811f99977b60..a3229bca1f549 100644 --- a/x-pack/test/api_integration/apis/security/license_downgrade.ts +++ b/x-pack/test/api_integration/apis/security/license_downgrade.ts @@ -24,6 +24,7 @@ export default function ({ getService }: FtrProviderContext) { 'minimal_all', 'minimal_read', 'url_create', + 'store_search_session', ]; const trialPrivileges = await supertest .get('/api/security/privileges') diff --git a/x-pack/test/api_integration/apis/security/privileges.ts b/x-pack/test/api_integration/apis/security/privileges.ts index 843dd983adf85..f068dbe7febbd 100644 --- a/x-pack/test/api_integration/apis/security/privileges.ts +++ b/x-pack/test/api_integration/apis/security/privileges.ts @@ -20,9 +20,23 @@ export default function ({ getService }: FtrProviderContext) { // Roles are associated with these privileges, and we shouldn't be removing them in a minor version. const expected = { features: { - discover: ['all', 'read', 'minimal_all', 'minimal_read', 'url_create'], + discover: [ + 'all', + 'read', + 'minimal_all', + 'minimal_read', + 'url_create', + 'store_search_session', + ], visualize: ['all', 'read', 'minimal_all', 'minimal_read', 'url_create'], - dashboard: ['all', 'read', 'minimal_all', 'minimal_read', 'url_create'], + dashboard: [ + 'all', + 'read', + 'minimal_all', + 'minimal_read', + 'url_create', + 'store_search_session', + ], dev_tools: ['all', 'read'], advancedSettings: ['all', 'read'], indexPatterns: ['all', 'read'], diff --git a/x-pack/test/send_search_to_background_integration/config.ts b/x-pack/test/send_search_to_background_integration/config.ts index 7dd0915de3c33..957c183d04536 100644 --- a/x-pack/test/send_search_to_background_integration/config.ts +++ b/x-pack/test/send_search_to_background_integration/config.ts @@ -20,7 +20,10 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { reportName: 'X-Pack Background Search UI (Enabled WIP Feature)', }, - testFiles: [resolve(__dirname, './tests/apps/dashboard/async_search')], + testFiles: [ + resolve(__dirname, './tests/apps/dashboard/async_search'), + resolve(__dirname, './tests/apps/discover'), + ], kbnTestServer: { ...xpackFunctionalConfig.get('kbnTestServer'), diff --git a/x-pack/test/send_search_to_background_integration/tests/apps/dashboard/async_search/sessions_in_space.ts b/x-pack/test/send_search_to_background_integration/tests/apps/dashboard/async_search/sessions_in_space.ts index e2e0adf447afe..97fba9e3aef91 100644 --- a/x-pack/test/send_search_to_background_integration/tests/apps/dashboard/async_search/sessions_in_space.ts +++ b/x-pack/test/send_search_to_background_integration/tests/apps/dashboard/async_search/sessions_in_space.ts @@ -34,7 +34,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }, kibana: [ { - base: ['all'], + feature: { + dashboard: ['minimal_read', 'store_search_session'], + }, spaces: ['another-space'], }, ], @@ -54,6 +56,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); after(async () => { + await security.role.delete('data_analyst'); + await security.user.delete('analyst'); + await esArchiver.unload('dashboard/session_in_space'); await PageObjects.security.forceLogout(); }); diff --git a/x-pack/test/send_search_to_background_integration/tests/apps/discover/index.ts b/x-pack/test/send_search_to_background_integration/tests/apps/discover/index.ts new file mode 100644 index 0000000000000..b4fc74072ce00 --- /dev/null +++ b/x-pack/test/send_search_to_background_integration/tests/apps/discover/index.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ loadTestFile, getService }: FtrProviderContext) { + const kibanaServer = getService('kibanaServer'); + const esArchiver = getService('esArchiver'); + + describe('async search', function () { + this.tags('ciGroup3'); + + before(async () => { + await esArchiver.loadIfNeeded('logstash_functional'); + await kibanaServer.uiSettings.replace({ defaultIndex: 'logstash-*' }); + }); + + loadTestFile(require.resolve('./sessions_in_space')); + }); +} diff --git a/x-pack/test/send_search_to_background_integration/tests/apps/discover/sessions_in_space.ts b/x-pack/test/send_search_to_background_integration/tests/apps/discover/sessions_in_space.ts new file mode 100644 index 0000000000000..5c94a50e0a84d --- /dev/null +++ b/x-pack/test/send_search_to_background_integration/tests/apps/discover/sessions_in_space.ts @@ -0,0 +1,100 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const testSubjects = getService('testSubjects'); + const esArchiver = getService('esArchiver'); + const security = getService('security'); + const inspector = getService('inspector'); + const PageObjects = getPageObjects([ + 'common', + 'header', + 'discover', + 'visChart', + 'security', + 'timePicker', + ]); + const browser = getService('browser'); + const sendToBackground = getService('sendToBackground'); + + describe('discover in space', () => { + describe('Send to background in space', () => { + before(async () => { + await esArchiver.load('dashboard/session_in_space'); + + await security.role.create('data_analyst', { + elasticsearch: { + indices: [{ names: ['logstash-*'], privileges: ['all'] }], + }, + kibana: [ + { + feature: { + discover: ['all'], + }, + spaces: ['another-space'], + }, + ], + }); + + await security.user.create('analyst', { + password: 'analyst-password', + roles: ['data_analyst'], + full_name: 'test user', + }); + + await PageObjects.security.forceLogout(); + + await PageObjects.security.login('analyst', 'analyst-password', { + expectSpaceSelector: false, + }); + }); + + after(async () => { + await security.role.delete('data_analyst'); + await security.user.delete('analyst'); + + await esArchiver.unload('dashboard/session_in_space'); + await PageObjects.security.forceLogout(); + }); + + it('Saves and restores a session', async () => { + await PageObjects.common.navigateToApp('discover', { basePath: 's/another-space' }); + + await PageObjects.discover.selectIndexPattern('logstash-*'); + + await PageObjects.timePicker.setAbsoluteRange( + 'Sep 1, 2015 @ 00:00:00.000', + 'Oct 1, 2015 @ 00:00:00.000' + ); + + await PageObjects.discover.waitForDocTableLoadingComplete(); + + await sendToBackground.expectState('completed'); + await sendToBackground.save(); + await sendToBackground.expectState('backgroundCompleted'); + await inspector.open(); + + const savedSessionId = await ( + await testSubjects.find('inspectorRequestSearchSessionId') + ).getAttribute('data-search-session-id'); + await inspector.close(); + + // load URL to restore a saved session + const url = await browser.getCurrentUrl(); + const savedSessionURL = `${url}&searchSessionId=${savedSessionId}`; + await browser.get(savedSessionURL); + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.discover.waitForDocTableLoadingComplete(); + + // Check that session is restored + await sendToBackground.expectState('restored'); + await testSubjects.missingOrFail('embeddableErrorLabel'); + }); + }); + }); +}