From 4886819db0e4b7084ce10b91e4f12de762ca8018 Mon Sep 17 00:00:00 2001 From: Lukas Olson Date: Tue, 1 Dec 2020 16:07:19 -0700 Subject: [PATCH 01/14] [data.search] Move search method inside session service and add tests --- .../data/server/search/search_service.ts | 35 +++----- .../search/session/session_service.test.ts | 89 ++++++++++++++++--- .../server/search/session/session_service.ts | 37 ++++++-- 3 files changed, 122 insertions(+), 39 deletions(-) diff --git a/src/plugins/data/server/search/search_service.ts b/src/plugins/data/server/search/search_service.ts index a9539a8fd3c154..03cbcd534b15ff 100644 --- a/src/plugins/data/server/search/search_service.ts +++ b/src/plugins/data/server/search/search_service.ts @@ -17,7 +17,7 @@ * under the License. */ -import { BehaviorSubject, from, Observable } from 'rxjs'; +import { BehaviorSubject, Observable } from 'rxjs'; import { pick } from 'lodash'; import { CoreSetup, @@ -29,7 +29,7 @@ import { SharedGlobalConfig, StartServicesAccessor, } from 'src/core/server'; -import { catchError, first, map, switchMap } from 'rxjs/operators'; +import { catchError, first, map } from 'rxjs/operators'; import { BfetchServerSetup } from 'src/plugins/bfetch/server'; import { ExpressionsServerSetup } from 'src/plugins/expressions/server'; import { @@ -50,7 +50,11 @@ import { DataPluginStart } from '../plugin'; import { UsageCollectionSetup } from '../../../usage_collection/server'; import { registerUsageCollector } from './collectors/register'; import { usageProvider } from './collectors/usage'; -import { BACKGROUND_SESSION_TYPE, searchTelemetry } from '../saved_objects'; +import { + BACKGROUND_SESSION_TYPE, + backgroundSessionMapping, + searchTelemetry, +} from '../saved_objects'; import { IEsSearchRequest, IEsSearchResponse, @@ -73,8 +77,6 @@ import { aggShardDelay } from '../../common/search/aggs/buckets/shard_delay_fn'; import { ConfigSchema } from '../../config'; import { BackgroundSessionService, ISearchSessionClient } from './session'; import { registerSessionRoutes } from './routes/session'; -import { backgroundSessionMapping } from '../saved_objects'; -import { tapFirst } from '../../common/utils'; declare module 'src/core/server' { interface RequestHandlerContext { @@ -295,7 +297,7 @@ export class SearchService implements Plugin { SearchStrategyRequest extends IKibanaSearchRequest = IEsSearchRequest, SearchStrategyResponse extends IKibanaSearchResponse = IEsSearchResponse >( - searchRequest: SearchStrategyRequest, + request: SearchStrategyRequest, options: ISearchOptions, deps: SearchStrategyDependencies ) => { @@ -303,24 +305,9 @@ export class SearchService implements Plugin { options.strategy ); - // If this is a restored background search session, look up the ID using the provided sessionId - const getSearchRequest = async () => - !options.isRestore || searchRequest.id - ? searchRequest - : { - ...searchRequest, - id: await this.sessionService.getId(searchRequest, options, deps), - }; - - return from(getSearchRequest()).pipe( - switchMap((request) => strategy.search(request, options, deps)), - tapFirst((response) => { - if (searchRequest.id || !options.sessionId || !response.id || options.isRestore) return; - this.sessionService.trackId(searchRequest, response.id, options, { - savedObjectsClient: deps.savedObjectsClient, - }); - }) - ); + return options.sessionId + ? this.sessionService.search(strategy, request, options, deps) + : strategy.search(request, options, deps); }; private cancel = (id: string, options: ISearchOptions, deps: SearchStrategyDependencies) => { diff --git a/src/plugins/data/server/search/session/session_service.test.ts b/src/plugins/data/server/search/session/session_service.test.ts index 5ff6d4b932487c..167aa8c4099e04 100644 --- a/src/plugins/data/server/search/session/session_service.test.ts +++ b/src/plugins/data/server/search/session/session_service.test.ts @@ -17,7 +17,9 @@ * under the License. */ +import { of } from 'rxjs'; import type { SavedObject, SavedObjectsClientContract } from 'kibana/server'; +import type { SearchStrategyDependencies } from '../types'; import { savedObjectsClientMock } from '../../../../../core/server/mocks'; import { BackgroundSessionStatus } from '../../../common'; import { BACKGROUND_SESSION_TYPE } from '../../saved_objects'; @@ -28,6 +30,7 @@ describe('BackgroundSessionService', () => { let savedObjectsClient: jest.Mocked; let service: BackgroundSessionService; + const sessionId = 'd7170a35-7e2c-48d6-8dec-9a056721b489'; const mockSavedObject: SavedObject = { id: 'd7170a35-7e2c-48d6-8dec-9a056721b489', type: BACKGROUND_SESSION_TYPE, @@ -45,9 +48,13 @@ describe('BackgroundSessionService', () => { service = new BackgroundSessionService(); }); - it('save throws if `name` is not provided', () => { - const sessionId = 'd7170a35-7e2c-48d6-8dec-9a056721b489'; + it('search throws if `name` is not provided', () => { + expect(() => service.save(sessionId, {}, { savedObjectsClient })).rejects.toMatchInlineSnapshot( + `[Error: Name is required]` + ); + }); + it('save throws if `name` is not provided', () => { expect(() => service.save(sessionId, {}, { savedObjectsClient })).rejects.toMatchInlineSnapshot( `[Error: Name is required]` ); @@ -56,7 +63,6 @@ describe('BackgroundSessionService', () => { it('get calls saved objects client', async () => { savedObjectsClient.get.mockResolvedValue(mockSavedObject); - const sessionId = 'd7170a35-7e2c-48d6-8dec-9a056721b489'; const response = await service.get(sessionId, { savedObjectsClient }); expect(response).toBe(mockSavedObject); @@ -93,7 +99,6 @@ describe('BackgroundSessionService', () => { }; savedObjectsClient.update.mockResolvedValue(mockUpdateSavedObject); - const sessionId = 'd7170a35-7e2c-48d6-8dec-9a056721b489'; const attributes = { name: 'new_name' }; const response = await service.update(sessionId, attributes, { savedObjectsClient }); @@ -108,19 +113,87 @@ describe('BackgroundSessionService', () => { it('delete calls saved objects client', async () => { savedObjectsClient.delete.mockResolvedValue({}); - const sessionId = 'd7170a35-7e2c-48d6-8dec-9a056721b489'; const response = await service.delete(sessionId, { savedObjectsClient }); expect(response).toEqual({}); expect(savedObjectsClient.delete).toHaveBeenCalledWith(BACKGROUND_SESSION_TYPE, sessionId); }); + describe('search', () => { + const mockSearch = jest.fn().mockReturnValue(of({})); + const mockStrategy = { search: mockSearch }; + const mockDeps = {} as SearchStrategyDependencies; + + beforeEach(() => { + mockSearch.mockClear(); + }); + + it('searches using the original request if not restoring', async () => { + const searchRequest = { params: {} }; + const options = { sessionId, isStored: false, isRestore: false }; + + await service.search(mockStrategy, searchRequest, options, mockDeps).toPromise(); + + expect(mockSearch).toBeCalledWith(searchRequest, options, mockDeps); + }); + + it('searches using the original request if `id` is provided', async () => { + const searchId = 'FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0'; + const searchRequest = { id: searchId, params: {} }; + const options = { sessionId, isStored: true, isRestore: true }; + + await service.search(mockStrategy, searchRequest, options, mockDeps).toPromise(); + + expect(mockSearch).toBeCalledWith(searchRequest, options, mockDeps); + }); + + it('searches by looking up an `id` if restoring and `id` is not provided', async () => { + const searchRequest = { params: {} }; + const options = { sessionId, isStored: true, isRestore: true }; + const spyGetId = jest.spyOn(service, 'getId').mockResolvedValueOnce('my_id'); + + await service.search(mockStrategy, searchRequest, options, mockDeps).toPromise(); + + expect(mockSearch).toBeCalledWith({ ...searchRequest, id: 'my_id' }, options, mockDeps); + + spyGetId.mockRestore(); + }); + + it('calls `trackId` once if the response contains an `id` and not restoring', async () => { + const searchRequest = { params: {} }; + const options = { sessionId, isStored: false, isRestore: false }; + const spyTrackId = jest.spyOn(service, 'trackId').mockResolvedValue(); + mockSearch.mockReturnValueOnce(of({ id: 'my_id' }, { id: 'my_id' })); + + await service.search(mockStrategy, searchRequest, options, mockDeps).toPromise(); + + expect(spyTrackId).toBeCalledTimes(1); + expect(spyTrackId).toBeCalledWith(searchRequest, 'my_id', options, mockDeps); + + spyTrackId.mockRestore(); + }); + + it('does not call `trackId` if restoring', async () => { + const searchRequest = { params: {} }; + const options = { sessionId, isStored: true, isRestore: true }; + const spyGetId = jest.spyOn(service, 'getId').mockResolvedValueOnce('my_id'); + const spyTrackId = jest.spyOn(service, 'trackId').mockResolvedValue(); + mockSearch.mockReturnValueOnce(of({ id: 'my_id' })); + + await service.search(mockStrategy, searchRequest, options, mockDeps).toPromise(); + + expect(spyTrackId).not.toBeCalled(); + + spyGetId.mockRestore(); + spyTrackId.mockRestore(); + }); + }); + describe('trackId', () => { it('stores hash in memory when `isStored` is `false` for when `save` is called', async () => { const searchRequest = { params: {} }; const requestHash = createRequestHash(searchRequest.params); const searchId = 'FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0'; - const sessionId = 'd7170a35-7e2c-48d6-8dec-9a056721b489'; const isStored = false; const name = 'my saved background search session'; const appId = 'my_app_id'; @@ -164,7 +237,6 @@ describe('BackgroundSessionService', () => { const searchRequest = { params: {} }; const requestHash = createRequestHash(searchRequest.params); const searchId = 'FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0'; - const sessionId = 'd7170a35-7e2c-48d6-8dec-9a056721b489'; const isStored = true; await service.trackId( @@ -191,7 +263,6 @@ describe('BackgroundSessionService', () => { it('throws if there is not a saved object', () => { const searchRequest = { params: {} }; - const sessionId = 'd7170a35-7e2c-48d6-8dec-9a056721b489'; expect(() => service.getId(searchRequest, { sessionId, isStored: false }, { savedObjectsClient }) @@ -202,7 +273,6 @@ describe('BackgroundSessionService', () => { it('throws if not restoring a saved session', () => { const searchRequest = { params: {} }; - const sessionId = 'd7170a35-7e2c-48d6-8dec-9a056721b489'; expect(() => service.getId( @@ -219,7 +289,6 @@ describe('BackgroundSessionService', () => { const searchRequest = { params: {} }; const requestHash = createRequestHash(searchRequest.params); const searchId = 'FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0'; - const sessionId = 'd7170a35-7e2c-48d6-8dec-9a056721b489'; const mockSession = { id: 'd7170a35-7e2c-48d6-8dec-9a056721b489', type: BACKGROUND_SESSION_TYPE, diff --git a/src/plugins/data/server/search/session/session_service.ts b/src/plugins/data/server/search/session/session_service.ts index b9a738413ede4f..d997af728b60cb 100644 --- a/src/plugins/data/server/search/session/session_service.ts +++ b/src/plugins/data/server/search/session/session_service.ts @@ -18,14 +18,19 @@ */ import { CoreStart, KibanaRequest, SavedObjectsClientContract } from 'kibana/server'; +import { from } from 'rxjs'; +import { switchMap } from 'rxjs/operators'; import { BackgroundSessionSavedObjectAttributes, + BackgroundSessionStatus, IKibanaSearchRequest, + IKibanaSearchResponse, ISearchOptions, SearchSessionFindOptions, - BackgroundSessionStatus, + tapFirst, } from '../../../common'; import { BACKGROUND_SESSION_TYPE } from '../../saved_objects'; +import { ISearchStrategy, SearchStrategyDependencies } from '../types'; import { createRequestHash } from './utils'; const DEFAULT_EXPIRATION = 7 * 24 * 60 * 60 * 1000; @@ -59,6 +64,32 @@ export class BackgroundSessionService { this.sessionSearchMap.clear(); }; + public search = ( + strategy: ISearchStrategy, + searchRequest: Request, + options: ISearchOptions, + deps: SearchStrategyDependencies + ) => { + // If this is a restored background search session, look up the ID using the provided sessionId + const getSearchRequest = async () => + !options.isRestore || searchRequest.id + ? searchRequest + : { + ...searchRequest, + id: await this.getId(searchRequest, options, deps), + }; + + return from(getSearchRequest()).pipe( + switchMap((request) => strategy.search(request, options, deps)), + tapFirst((response) => { + if (searchRequest.id || !options.sessionId || !response.id || options.isRestore) return; + this.trackId(searchRequest, response.id, options, { + savedObjectsClient: deps.savedObjectsClient, + }); + }) + ); + }; + // TODO: Generate the `userId` from the realm type/realm name/username public save = async ( sessionId: string, @@ -208,10 +239,6 @@ export class BackgroundSessionService { update: (sessionId: string, attributes: Partial) => this.update(sessionId, attributes, deps), delete: (sessionId: string) => this.delete(sessionId, deps), - trackId: (searchRequest: IKibanaSearchRequest, searchId: string, options: ISearchOptions) => - this.trackId(searchRequest, searchId, options, deps), - getId: (searchRequest: IKibanaSearchRequest, options: ISearchOptions) => - this.getId(searchRequest, options, deps), }; }; }; From 0de773085dc0ff83508ac33244c967a41052c250 Mon Sep 17 00:00:00 2001 From: Lukas Olson Date: Wed, 2 Dec 2020 14:31:22 -0700 Subject: [PATCH 02/14] Move background session service to data_enhanced plugin --- .../kibana-plugin-plugins-data-public.md | 2 +- ...blic.searchsessioninfoprovider.getname.md} | 2 +- ...essioninfoprovider.geturlgeneratordata.md} | 2 +- ...-data-public.searchsessioninfoprovider.md} | 6 +- ...server.isessionservice.asscopedprovider.md | 11 + ...gin-plugins-data-server.isessionservice.md | 18 + .../kibana-plugin-plugins-data-server.md | 2 + ...ata-server.sessionservice._constructor_.md | 13 + ...-server.sessionservice.asscopedprovider.md | 26 ++ ...ugin-plugins-data-server.sessionservice.md | 27 ++ ...ugins-data-server.sessionservice.search.md | 23 ++ src/plugins/data/common/search/index.ts | 1 - .../data/common/search/session/index.ts | 21 -- .../data/common/search/session/status.ts | 26 -- src/plugins/data/public/public.api.md | 1 + .../public/search/session/sessions_client.ts | 16 +- src/plugins/data/server/index.ts | 2 + .../data/server/saved_objects/index.ts | 1 - src/plugins/data/server/search/index.ts | 1 + src/plugins/data/server/search/mocks.ts | 8 +- .../data/server/search/search_service.ts | 37 +-- .../data/server/search/session/index.ts | 3 +- .../search/session/session_service.test.ts | 298 +---------------- .../server/search/session/session_service.ts | 238 +------------ .../search/session/types.ts | 35 +- .../data/server/search/session/utils.test.ts | 37 --- .../data/server/search/session/utils.ts | 30 -- src/plugins/data/server/search/types.ts | 2 + src/plugins/data/server/server.api.md | 68 ++-- src/plugins/embeddable/public/public.api.md | 1 + x-pack/plugins/data_enhanced/common/index.ts | 3 + .../data_enhanced/common/search/index.ts | 1 + .../common/search/session/index.ts | 8 + .../common/search/session/status.ts | 13 + .../common/search/session/types.ts | 31 ++ x-pack/plugins/data_enhanced/server/plugin.ts | 23 +- .../data_enhanced/server/routes/index.ts | 7 + .../server}/routes/session.test.ts | 25 +- .../data_enhanced/server}/routes/session.ts | 19 +- .../saved_objects/background_session.ts | 19 +- .../server/saved_objects/index.ts | 7 + .../data_enhanced/server/search/index.ts | 1 + .../server/search/session/index.ts | 7 + .../search/session/session_service.test.ts | 312 ++++++++++++++++++ .../server/search/session/session_service.ts | 225 +++++++++++++ .../server/search/session/utils.test.ts | 24 ++ .../server/search/session/utils.ts | 17 + 47 files changed, 925 insertions(+), 775 deletions(-) rename docs/development/plugins/data/public/{kibana-plugin-plugins-data-public.searchsessionrestorationinfoprovider.getname.md => kibana-plugin-plugins-data-public.searchsessioninfoprovider.getname.md} (83%) rename docs/development/plugins/data/public/{kibana-plugin-plugins-data-public.searchsessionrestorationinfoprovider.geturlgeneratordata.md => kibana-plugin-plugins-data-public.searchsessioninfoprovider.geturlgeneratordata.md} (82%) rename docs/development/plugins/data/public/{kibana-plugin-plugins-data-public.searchsessionrestorationinfoprovider.md => kibana-plugin-plugins-data-public.searchsessioninfoprovider.md} (84%) create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isessionservice.asscopedprovider.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isessionservice.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.sessionservice._constructor_.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.sessionservice.asscopedprovider.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.sessionservice.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.sessionservice.search.md delete mode 100644 src/plugins/data/common/search/session/index.ts delete mode 100644 src/plugins/data/common/search/session/status.ts rename src/plugins/data/{common => server}/search/session/types.ts (55%) delete mode 100644 src/plugins/data/server/search/session/utils.test.ts delete mode 100644 src/plugins/data/server/search/session/utils.ts create mode 100644 x-pack/plugins/data_enhanced/common/search/session/index.ts create mode 100644 x-pack/plugins/data_enhanced/common/search/session/status.ts create mode 100644 x-pack/plugins/data_enhanced/common/search/session/types.ts create mode 100644 x-pack/plugins/data_enhanced/server/routes/index.ts rename {src/plugins/data/server/search => x-pack/plugins/data_enhanced/server}/routes/session.test.ts (78%) rename {src/plugins/data/server/search => x-pack/plugins/data_enhanced/server}/routes/session.ts (85%) rename {src/plugins/data => x-pack/plugins/data_enhanced}/server/saved_objects/background_session.ts (51%) create mode 100644 x-pack/plugins/data_enhanced/server/saved_objects/index.ts create mode 100644 x-pack/plugins/data_enhanced/server/search/session/index.ts create mode 100644 x-pack/plugins/data_enhanced/server/search/session/session_service.test.ts create mode 100644 x-pack/plugins/data_enhanced/server/search/session/session_service.ts create mode 100644 x-pack/plugins/data_enhanced/server/search/session/utils.test.ts create mode 100644 x-pack/plugins/data_enhanced/server/search/session/utils.ts diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md index 9121b0aade4706..08ed14b92d24c7 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md @@ -89,7 +89,7 @@ | [SavedQueryService](./kibana-plugin-plugins-data-public.savedqueryservice.md) | | | [SearchError](./kibana-plugin-plugins-data-public.searcherror.md) | | | [SearchInterceptorDeps](./kibana-plugin-plugins-data-public.searchinterceptordeps.md) | | -| [SearchSessionInfoProvider](./kibana-plugin-plugins-data-public.searchSessionInfoprovider.md) | Provide info about current search session to be stored in backgroundSearch saved object | +| [SearchSessionInfoProvider](./kibana-plugin-plugins-data-public.searchsessioninfoprovider.md) | Provide info about current search session to be stored in backgroundSearch saved object | | [SearchSourceFields](./kibana-plugin-plugins-data-public.searchsourcefields.md) | search source fields | | [TabbedAggColumn](./kibana-plugin-plugins-data-public.tabbedaggcolumn.md) | \* | | [TabbedTable](./kibana-plugin-plugins-data-public.tabbedtable.md) | \* | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsessionrestorationinfoprovider.getname.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsessioninfoprovider.getname.md similarity index 83% rename from docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsessionrestorationinfoprovider.getname.md rename to docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsessioninfoprovider.getname.md index 0f0b616066dd66..2a5e1d2a3135fe 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsessionrestorationinfoprovider.getname.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsessioninfoprovider.getname.md @@ -1,6 +1,6 @@ -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchSessionInfoProvider](./kibana-plugin-plugins-data-public.searchSessionInfoprovider.md) > [getName](./kibana-plugin-plugins-data-public.searchSessionInfoprovider.getname.md) +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchSessionInfoProvider](./kibana-plugin-plugins-data-public.searchsessioninfoprovider.md) > [getName](./kibana-plugin-plugins-data-public.searchsessioninfoprovider.getname.md) ## SearchSessionInfoProvider.getName property diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsessionrestorationinfoprovider.geturlgeneratordata.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsessioninfoprovider.geturlgeneratordata.md similarity index 82% rename from docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsessionrestorationinfoprovider.geturlgeneratordata.md rename to docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsessioninfoprovider.geturlgeneratordata.md index 207adaf2bd50b6..01558ed3dddad1 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsessionrestorationinfoprovider.geturlgeneratordata.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsessioninfoprovider.geturlgeneratordata.md @@ -1,6 +1,6 @@ -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchSessionInfoProvider](./kibana-plugin-plugins-data-public.searchSessionInfoprovider.md) > [getUrlGeneratorData](./kibana-plugin-plugins-data-public.searchSessionInfoprovider.geturlgeneratordata.md) +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchSessionInfoProvider](./kibana-plugin-plugins-data-public.searchsessioninfoprovider.md) > [getUrlGeneratorData](./kibana-plugin-plugins-data-public.searchsessioninfoprovider.geturlgeneratordata.md) ## SearchSessionInfoProvider.getUrlGeneratorData property diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsessionrestorationinfoprovider.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsessioninfoprovider.md similarity index 84% rename from docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsessionrestorationinfoprovider.md rename to docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsessioninfoprovider.md index a3d294f5e33038..bcc4a5508eb59c 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsessionrestorationinfoprovider.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsessioninfoprovider.md @@ -1,6 +1,6 @@ -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchSessionInfoProvider](./kibana-plugin-plugins-data-public.searchSessionInfoprovider.md) +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchSessionInfoProvider](./kibana-plugin-plugins-data-public.searchsessioninfoprovider.md) ## SearchSessionInfoProvider interface @@ -16,6 +16,6 @@ export interface SearchSessionInfoProvider() => Promise<string> | User-facing name of the session. e.g. will be displayed in background sessions management list | -| [getUrlGeneratorData](./kibana-plugin-plugins-data-public.searchSessionInfoprovider.geturlgeneratordata.md) | () => Promise<{
urlGeneratorId: ID;
initialState: UrlGeneratorStateMapping[ID]['State'];
restoreState: UrlGeneratorStateMapping[ID]['State'];
}> | | +| [getName](./kibana-plugin-plugins-data-public.searchsessioninfoprovider.getname.md) | () => Promise<string> | User-facing name of the session. e.g. will be displayed in background sessions management list | +| [getUrlGeneratorData](./kibana-plugin-plugins-data-public.searchsessioninfoprovider.geturlgeneratordata.md) | () => Promise<{
urlGeneratorId: ID;
initialState: UrlGeneratorStateMapping[ID]['State'];
restoreState: UrlGeneratorStateMapping[ID]['State'];
}> | | diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isessionservice.asscopedprovider.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isessionservice.asscopedprovider.md new file mode 100644 index 00000000000000..d52b9b783919b4 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isessionservice.asscopedprovider.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [ISessionService](./kibana-plugin-plugins-data-server.isessionservice.md) > [asScopedProvider](./kibana-plugin-plugins-data-server.isessionservice.asscopedprovider.md) + +## ISessionService.asScopedProvider property + +Signature: + +```typescript +asScopedProvider: (core: CoreStart) => (request: KibanaRequest) => IScopedSessionService; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isessionservice.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isessionservice.md new file mode 100644 index 00000000000000..dcc7dfc8bb9465 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isessionservice.md @@ -0,0 +1,18 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [ISessionService](./kibana-plugin-plugins-data-server.isessionservice.md) + +## ISessionService interface + +Signature: + +```typescript +export interface ISessionService +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [asScopedProvider](./kibana-plugin-plugins-data-server.isessionservice.asscopedprovider.md) | (core: CoreStart) => (request: KibanaRequest) => IScopedSessionService | | + diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md index c85f294d162bcc..55504e01b6c9a3 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md @@ -14,6 +14,7 @@ | [IndexPatternsService](./kibana-plugin-plugins-data-server.indexpatternsservice.md) | | | [OptionedParamType](./kibana-plugin-plugins-data-server.optionedparamtype.md) | | | [Plugin](./kibana-plugin-plugins-data-server.plugin.md) | | +| [SessionService](./kibana-plugin-plugins-data-server.sessionservice.md) | The OSS session service. See data\_enhanced in X-Pack for the background session service. | ## Enumerations @@ -54,6 +55,7 @@ | [ISearchSetup](./kibana-plugin-plugins-data-server.isearchsetup.md) | | | [ISearchStart](./kibana-plugin-plugins-data-server.isearchstart.md) | | | [ISearchStrategy](./kibana-plugin-plugins-data-server.isearchstrategy.md) | Search strategy interface contains a search method that takes in a request and returns a promise that resolves to a response. | +| [ISessionService](./kibana-plugin-plugins-data-server.isessionservice.md) | | | [KueryNode](./kibana-plugin-plugins-data-server.kuerynode.md) | | | [OptionedValueProp](./kibana-plugin-plugins-data-server.optionedvalueprop.md) | | | [PluginSetup](./kibana-plugin-plugins-data-server.pluginsetup.md) | | diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.sessionservice._constructor_.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.sessionservice._constructor_.md new file mode 100644 index 00000000000000..73d658455a66f6 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.sessionservice._constructor_.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [SessionService](./kibana-plugin-plugins-data-server.sessionservice.md) > [(constructor)](./kibana-plugin-plugins-data-server.sessionservice._constructor_.md) + +## SessionService.(constructor) + +Constructs a new instance of the `SessionService` class + +Signature: + +```typescript +constructor(); +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.sessionservice.asscopedprovider.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.sessionservice.asscopedprovider.md new file mode 100644 index 00000000000000..f3af7fb0f61d99 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.sessionservice.asscopedprovider.md @@ -0,0 +1,26 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [SessionService](./kibana-plugin-plugins-data-server.sessionservice.md) > [asScopedProvider](./kibana-plugin-plugins-data-server.sessionservice.asscopedprovider.md) + +## SessionService.asScopedProvider() method + +Signature: + +```typescript +asScopedProvider(core: CoreStart): (request: KibanaRequest) => { + search: , Response_1 extends IKibanaSearchResponse>(strategy: ISearchStrategy, request: Request_1, options: import("../../../common").ISearchOptions, deps: import("../types").SearchStrategyDependencies) => import("rxjs").Observable; + }; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| core | CoreStart | | + +Returns: + +`(request: KibanaRequest) => { + search: , Response_1 extends IKibanaSearchResponse>(strategy: ISearchStrategy, request: Request_1, options: import("../../../common").ISearchOptions, deps: import("../types").SearchStrategyDependencies) => import("rxjs").Observable; + }` + diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.sessionservice.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.sessionservice.md new file mode 100644 index 00000000000000..2369b006445485 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.sessionservice.md @@ -0,0 +1,27 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [SessionService](./kibana-plugin-plugins-data-server.sessionservice.md) + +## SessionService class + +The OSS session service. See data\_enhanced in X-Pack for the background session service. + +Signature: + +```typescript +export declare class SessionService implements ISessionService +``` + +## Constructors + +| Constructor | Modifiers | Description | +| --- | --- | --- | +| [(constructor)()](./kibana-plugin-plugins-data-server.sessionservice._constructor_.md) | | Constructs a new instance of the SessionService class | + +## Methods + +| Method | Modifiers | Description | +| --- | --- | --- | +| [asScopedProvider(core)](./kibana-plugin-plugins-data-server.sessionservice.asscopedprovider.md) | | | +| [search(strategy, args)](./kibana-plugin-plugins-data-server.sessionservice.search.md) | | | + diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.sessionservice.search.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.sessionservice.search.md new file mode 100644 index 00000000000000..8f8620a6ed5ee9 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.sessionservice.search.md @@ -0,0 +1,23 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [SessionService](./kibana-plugin-plugins-data-server.sessionservice.md) > [search](./kibana-plugin-plugins-data-server.sessionservice.search.md) + +## SessionService.search() method + +Signature: + +```typescript +search(strategy: ISearchStrategy, ...args: Parameters['search']>): import("rxjs").Observable; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| strategy | ISearchStrategy<Request, Response> | | +| args | Parameters<ISearchStrategy<Request, Response>['search']> | | + +Returns: + +`import("rxjs").Observable` + diff --git a/src/plugins/data/common/search/index.ts b/src/plugins/data/common/search/index.ts index 01944d6e37aaf2..d5939b4ec9cbf6 100644 --- a/src/plugins/data/common/search/index.ts +++ b/src/plugins/data/common/search/index.ts @@ -23,5 +23,4 @@ export * from './expressions'; export * from './search_source'; export * from './tabify'; export * from './types'; -export * from './session'; export * from './utils'; diff --git a/src/plugins/data/common/search/session/index.ts b/src/plugins/data/common/search/session/index.ts deleted file mode 100644 index 0feb43f8f1d4b9..00000000000000 --- a/src/plugins/data/common/search/session/index.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export * from './status'; -export * from './types'; diff --git a/src/plugins/data/common/search/session/status.ts b/src/plugins/data/common/search/session/status.ts deleted file mode 100644 index 1f6b6eb3084bb5..00000000000000 --- a/src/plugins/data/common/search/session/status.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export enum BackgroundSessionStatus { - IN_PROGRESS = 'in_progress', - ERROR = 'error', - COMPLETE = 'complete', - CANCELLED = 'cancelled', - EXPIRED = 'expired', -} diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index ad1861cecea0be..cc4e32cbce1578 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -81,6 +81,7 @@ import { SavedObject as SavedObject_2 } from 'src/core/server'; import { SavedObject as SavedObject_3 } from 'src/core/public'; import { SavedObjectReference } from 'src/core/types'; import { SavedObjectsClientContract } from 'src/core/public'; +import { SavedObjectsFindOptions } from 'kibana/public'; import { SavedObjectsFindResponse } from 'kibana/server'; import { Search } from '@elastic/elasticsearch/api/requestParams'; import { SearchResponse } from 'elasticsearch'; diff --git a/src/plugins/data/public/search/session/sessions_client.ts b/src/plugins/data/public/search/session/sessions_client.ts index c19c5db0640946..38be647a37c7ac 100644 --- a/src/plugins/data/public/search/session/sessions_client.ts +++ b/src/plugins/data/public/search/session/sessions_client.ts @@ -18,9 +18,8 @@ */ import { PublicContract } from '@kbn/utility-types'; -import { HttpSetup } from 'kibana/public'; +import { HttpSetup, SavedObjectsFindOptions } from 'kibana/public'; import type { SavedObject, SavedObjectsFindResponse } from 'kibana/server'; -import { BackgroundSessionSavedObjectAttributes, SearchSessionFindOptions } from '../../../common'; export type ISessionsClient = PublicContract; export interface SessionsClientDeps { @@ -37,7 +36,7 @@ export class SessionsClient { this.http = deps.http; } - public get(sessionId: string): Promise> { + public get(sessionId: string): Promise { return this.http.get(`/internal/session/${encodeURIComponent(sessionId)}`); } @@ -55,7 +54,7 @@ export class SessionsClient { restoreState: Record; urlGeneratorId: string; sessionId: string; - }): Promise> { + }): Promise { return this.http.post(`/internal/session`, { body: JSON.stringify({ name, @@ -68,18 +67,13 @@ export class SessionsClient { }); } - public find( - options: SearchSessionFindOptions - ): Promise> { + public find(options: SavedObjectsFindOptions): Promise { return this.http!.post(`/internal/session`, { body: JSON.stringify(options), }); } - public update( - sessionId: string, - attributes: Partial - ): Promise> { + public update(sessionId: string, attributes: unknown): Promise { return this.http!.put(`/internal/session/${encodeURIComponent(sessionId)}`, { body: JSON.stringify(attributes), }); diff --git a/src/plugins/data/server/index.ts b/src/plugins/data/server/index.ts index a233447cdf438b..4a4e1df2cf45f4 100644 --- a/src/plugins/data/server/index.ts +++ b/src/plugins/data/server/index.ts @@ -242,6 +242,8 @@ export { searchUsageObserver, shimAbortSignal, SearchUsage, + SessionService, + ISessionService, } from './search'; // Search namespace diff --git a/src/plugins/data/server/saved_objects/index.ts b/src/plugins/data/server/saved_objects/index.ts index 7cd4d319e64171..077f9380823d08 100644 --- a/src/plugins/data/server/saved_objects/index.ts +++ b/src/plugins/data/server/saved_objects/index.ts @@ -20,4 +20,3 @@ export { querySavedObjectType } from './query'; export { indexPatternSavedObjectType } from './index_patterns'; export { kqlTelemetry } from './kql_telemetry'; export { searchTelemetry } from './search_telemetry'; -export { BACKGROUND_SESSION_TYPE, backgroundSessionMapping } from './background_session'; diff --git a/src/plugins/data/server/search/index.ts b/src/plugins/data/server/search/index.ts index 3001bbe3c2f381..f051e4c48223ce 100644 --- a/src/plugins/data/server/search/index.ts +++ b/src/plugins/data/server/search/index.ts @@ -22,3 +22,4 @@ export * from './es_search'; export { usageProvider, SearchUsage, searchUsageObserver } from './collectors'; export * from './aggs'; export { shimHitsTotal } from './routes'; +export * from './session'; diff --git a/src/plugins/data/server/search/mocks.ts b/src/plugins/data/server/search/mocks.ts index 290e94ee7cf991..6d214008e8b173 100644 --- a/src/plugins/data/server/search/mocks.ts +++ b/src/plugins/data/server/search/mocks.ts @@ -50,13 +50,7 @@ export function createSearchRequestHandlerContext(): jest.Mocked { private defaultSearchStrategyName: string = ES_SEARCH_STRATEGY; private searchStrategies: StrategyMap = {}; private coreStart?: CoreStart; - private sessionService: BackgroundSessionService = new BackgroundSessionService(); + private sessionService: ISessionService = new SessionService(); constructor( private initializerContext: PluginInitializerContext, @@ -131,7 +126,6 @@ export class SearchService implements Plugin { }; registerSearchRoute(router); registerMsearchRoute(router, routeDependencies); - registerSessionRoutes(router); core.getStartServices().then(([coreStart]) => { this.coreStart = coreStart; @@ -143,8 +137,6 @@ export class SearchService implements Plugin { return { ...search, session }; }); - core.savedObjects.registerType(backgroundSessionMapping); - this.registerSearchStrategy( ES_SEARCH_STRATEGY, esSearchStrategyProvider( @@ -219,6 +211,7 @@ export class SearchService implements Plugin { if (this.searchStrategies.hasOwnProperty(enhancements.defaultStrategy)) { this.defaultSearchStrategyName = enhancements.defaultStrategy; } + this.sessionService = enhancements.sessionService; }, aggs, registerSearchStrategy: this.registerSearchStrategy, @@ -279,7 +272,6 @@ export class SearchService implements Plugin { public stop() { this.aggsService.stop(); - this.sessionService.stop(); } private registerSearchStrategy = < @@ -297,6 +289,7 @@ export class SearchService implements Plugin { SearchStrategyRequest extends IKibanaSearchRequest = IEsSearchRequest, SearchStrategyResponse extends IKibanaSearchResponse = IEsSearchResponse >( + session: IScopedSessionService, request: SearchStrategyRequest, options: ISearchOptions, deps: SearchStrategyDependencies @@ -304,15 +297,11 @@ export class SearchService implements Plugin { const strategy = this.getSearchStrategy( options.strategy ); - - return options.sessionId - ? this.sessionService.search(strategy, request, options, deps) - : strategy.search(request, options, deps); + return session.search(strategy, request, options, deps); }; private cancel = (id: string, options: ISearchOptions, deps: SearchStrategyDependencies) => { const strategy = this.getSearchStrategy(options.strategy); - return strategy.cancel ? strategy.cancel(id, options, deps) : Promise.resolve(); }; @@ -330,18 +319,20 @@ export class SearchService implements Plugin { return strategy; }; - private asScopedProvider = ({ elasticsearch, savedObjects, uiSettings }: CoreStart) => { + private asScopedProvider = (core: CoreStart) => { + const { elasticsearch, savedObjects, uiSettings } = core; + const getSessionAsScoped = this.sessionService.asScopedProvider(core); return (request: KibanaRequest): ISearchClient => { - const savedObjectsClient = savedObjects.getScopedClient(request, { - includedHiddenTypes: [BACKGROUND_SESSION_TYPE], - }); + const scopedSession = getSessionAsScoped(request); + const savedObjectsClient = savedObjects.getScopedClient(request); const deps = { savedObjectsClient, esClient: elasticsearch.client.asScoped(request), uiSettingsClient: uiSettings.asScopedToClient(savedObjectsClient), }; return { - search: (searchRequest, options = {}) => this.search(searchRequest, options, deps), + search: (searchRequest, options = {}) => + this.search(scopedSession, searchRequest, options, deps), cancel: (id, options = {}) => this.cancel(id, options, deps), }; }; diff --git a/src/plugins/data/server/search/session/index.ts b/src/plugins/data/server/search/session/index.ts index 11b5b16a02b561..0966a1e4a18ec5 100644 --- a/src/plugins/data/server/search/session/index.ts +++ b/src/plugins/data/server/search/session/index.ts @@ -17,4 +17,5 @@ * under the License. */ -export { BackgroundSessionService, ISearchSessionClient } from './session_service'; +export * from './session_service'; +export * from './types'; diff --git a/src/plugins/data/server/search/session/session_service.test.ts b/src/plugins/data/server/search/session/session_service.test.ts index 167aa8c4099e04..3dbe01105aca4e 100644 --- a/src/plugins/data/server/search/session/session_service.test.ts +++ b/src/plugins/data/server/search/session/session_service.test.ts @@ -18,297 +18,21 @@ */ import { of } from 'rxjs'; -import type { SavedObject, SavedObjectsClientContract } from 'kibana/server'; -import type { SearchStrategyDependencies } from '../types'; -import { savedObjectsClientMock } from '../../../../../core/server/mocks'; -import { BackgroundSessionStatus } from '../../../common'; -import { BACKGROUND_SESSION_TYPE } from '../../saved_objects'; -import { BackgroundSessionService } from './session_service'; -import { createRequestHash } from './utils'; +import { SearchStrategyDependencies } from '../types'; +import { SessionService } from './session_service'; -describe('BackgroundSessionService', () => { - let savedObjectsClient: jest.Mocked; - let service: BackgroundSessionService; - - const sessionId = 'd7170a35-7e2c-48d6-8dec-9a056721b489'; - const mockSavedObject: SavedObject = { - id: 'd7170a35-7e2c-48d6-8dec-9a056721b489', - type: BACKGROUND_SESSION_TYPE, - attributes: { - name: 'my_name', - appId: 'my_app_id', - urlGeneratorId: 'my_url_generator_id', - idMapping: {}, - }, - references: [], - }; - - beforeEach(() => { - savedObjectsClient = savedObjectsClientMock.create(); - service = new BackgroundSessionService(); - }); - - it('search throws if `name` is not provided', () => { - expect(() => service.save(sessionId, {}, { savedObjectsClient })).rejects.toMatchInlineSnapshot( - `[Error: Name is required]` - ); - }); - - it('save throws if `name` is not provided', () => { - expect(() => service.save(sessionId, {}, { savedObjectsClient })).rejects.toMatchInlineSnapshot( - `[Error: Name is required]` - ); - }); - - it('get calls saved objects client', async () => { - savedObjectsClient.get.mockResolvedValue(mockSavedObject); - - const response = await service.get(sessionId, { savedObjectsClient }); - - expect(response).toBe(mockSavedObject); - expect(savedObjectsClient.get).toHaveBeenCalledWith(BACKGROUND_SESSION_TYPE, sessionId); - }); - - it('find calls saved objects client', async () => { - const mockFindSavedObject = { - ...mockSavedObject, - score: 1, - }; - const mockResponse = { - saved_objects: [mockFindSavedObject], - total: 1, - per_page: 1, - page: 0, - }; - savedObjectsClient.find.mockResolvedValue(mockResponse); - - const options = { page: 0, perPage: 5 }; - const response = await service.find(options, { savedObjectsClient }); - - expect(response).toBe(mockResponse); - expect(savedObjectsClient.find).toHaveBeenCalledWith({ - ...options, - type: BACKGROUND_SESSION_TYPE, - }); - }); - - it('update calls saved objects client', async () => { - const mockUpdateSavedObject = { - ...mockSavedObject, - attributes: {}, - }; - savedObjectsClient.update.mockResolvedValue(mockUpdateSavedObject); - - const attributes = { name: 'new_name' }; - const response = await service.update(sessionId, attributes, { savedObjectsClient }); - - expect(response).toBe(mockUpdateSavedObject); - expect(savedObjectsClient.update).toHaveBeenCalledWith( - BACKGROUND_SESSION_TYPE, - sessionId, - attributes - ); - }); - - it('delete calls saved objects client', async () => { - savedObjectsClient.delete.mockResolvedValue({}); - - const response = await service.delete(sessionId, { savedObjectsClient }); - - expect(response).toEqual({}); - expect(savedObjectsClient.delete).toHaveBeenCalledWith(BACKGROUND_SESSION_TYPE, sessionId); - }); - - describe('search', () => { +describe('SessionService', () => { + it('search invokes `strategy.search`', async () => { + const service = new SessionService(); const mockSearch = jest.fn().mockReturnValue(of({})); const mockStrategy = { search: mockSearch }; - const mockDeps = {} as SearchStrategyDependencies; - - beforeEach(() => { - mockSearch.mockClear(); - }); - - it('searches using the original request if not restoring', async () => { - const searchRequest = { params: {} }; - const options = { sessionId, isStored: false, isRestore: false }; - - await service.search(mockStrategy, searchRequest, options, mockDeps).toPromise(); - - expect(mockSearch).toBeCalledWith(searchRequest, options, mockDeps); - }); - - it('searches using the original request if `id` is provided', async () => { - const searchId = 'FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0'; - const searchRequest = { id: searchId, params: {} }; - const options = { sessionId, isStored: true, isRestore: true }; - - await service.search(mockStrategy, searchRequest, options, mockDeps).toPromise(); - - expect(mockSearch).toBeCalledWith(searchRequest, options, mockDeps); - }); - - it('searches by looking up an `id` if restoring and `id` is not provided', async () => { - const searchRequest = { params: {} }; - const options = { sessionId, isStored: true, isRestore: true }; - const spyGetId = jest.spyOn(service, 'getId').mockResolvedValueOnce('my_id'); - - await service.search(mockStrategy, searchRequest, options, mockDeps).toPromise(); - - expect(mockSearch).toBeCalledWith({ ...searchRequest, id: 'my_id' }, options, mockDeps); - - spyGetId.mockRestore(); - }); - - it('calls `trackId` once if the response contains an `id` and not restoring', async () => { - const searchRequest = { params: {} }; - const options = { sessionId, isStored: false, isRestore: false }; - const spyTrackId = jest.spyOn(service, 'trackId').mockResolvedValue(); - mockSearch.mockReturnValueOnce(of({ id: 'my_id' }, { id: 'my_id' })); - - await service.search(mockStrategy, searchRequest, options, mockDeps).toPromise(); - - expect(spyTrackId).toBeCalledTimes(1); - expect(spyTrackId).toBeCalledWith(searchRequest, 'my_id', options, mockDeps); - - spyTrackId.mockRestore(); - }); - - it('does not call `trackId` if restoring', async () => { - const searchRequest = { params: {} }; - const options = { sessionId, isStored: true, isRestore: true }; - const spyGetId = jest.spyOn(service, 'getId').mockResolvedValueOnce('my_id'); - const spyTrackId = jest.spyOn(service, 'trackId').mockResolvedValue(); - mockSearch.mockReturnValueOnce(of({ id: 'my_id' })); - - await service.search(mockStrategy, searchRequest, options, mockDeps).toPromise(); - - expect(spyTrackId).not.toBeCalled(); - - spyGetId.mockRestore(); - spyTrackId.mockRestore(); - }); - }); - - describe('trackId', () => { - it('stores hash in memory when `isStored` is `false` for when `save` is called', async () => { - const searchRequest = { params: {} }; - const requestHash = createRequestHash(searchRequest.params); - const searchId = 'FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0'; - const isStored = false; - const name = 'my saved background search session'; - const appId = 'my_app_id'; - const urlGeneratorId = 'my_url_generator_id'; - const created = new Date().toISOString(); - const expires = new Date().toISOString(); - - await service.trackId( - searchRequest, - searchId, - { sessionId, isStored }, - { savedObjectsClient } - ); - - expect(savedObjectsClient.update).not.toHaveBeenCalled(); - - await service.save( - sessionId, - { name, created, expires, appId, urlGeneratorId }, - { savedObjectsClient } - ); - - expect(savedObjectsClient.create).toHaveBeenCalledWith( - BACKGROUND_SESSION_TYPE, - { - name, - created, - expires, - initialState: {}, - restoreState: {}, - status: BackgroundSessionStatus.IN_PROGRESS, - idMapping: { [requestHash]: searchId }, - appId, - urlGeneratorId, - }, - { id: sessionId } - ); - }); - - it('updates saved object when `isStored` is `true`', async () => { - const searchRequest = { params: {} }; - const requestHash = createRequestHash(searchRequest.params); - const searchId = 'FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0'; - const isStored = true; - - await service.trackId( - searchRequest, - searchId, - { sessionId, isStored }, - { savedObjectsClient } - ); - - expect(savedObjectsClient.update).toHaveBeenCalledWith(BACKGROUND_SESSION_TYPE, sessionId, { - idMapping: { [requestHash]: searchId }, - }); - }); - }); - - describe('getId', () => { - it('throws if `sessionId` is not provided', () => { - const searchRequest = { params: {} }; - - expect(() => - service.getId(searchRequest, {}, { savedObjectsClient }) - ).rejects.toMatchInlineSnapshot(`[Error: Session ID is required]`); - }); - - it('throws if there is not a saved object', () => { - const searchRequest = { params: {} }; - - expect(() => - service.getId(searchRequest, { sessionId, isStored: false }, { savedObjectsClient }) - ).rejects.toMatchInlineSnapshot( - `[Error: Cannot get search ID from a session that is not stored]` - ); - }); - - it('throws if not restoring a saved session', () => { - const searchRequest = { params: {} }; - - expect(() => - service.getId( - searchRequest, - { sessionId, isStored: true, isRestore: false }, - { savedObjectsClient } - ) - ).rejects.toMatchInlineSnapshot( - `[Error: Get search ID is only supported when restoring a session]` - ); - }); - - it('returns the search ID from the saved object ID mapping', async () => { - const searchRequest = { params: {} }; - const requestHash = createRequestHash(searchRequest.params); - const searchId = 'FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0'; - const mockSession = { - id: 'd7170a35-7e2c-48d6-8dec-9a056721b489', - type: BACKGROUND_SESSION_TYPE, - attributes: { - name: 'my_name', - appId: 'my_app_id', - urlGeneratorId: 'my_url_generator_id', - idMapping: { [requestHash]: searchId }, - }, - references: [], - }; - savedObjectsClient.get.mockResolvedValue(mockSession); + const mockRequest = { id: 'bar' }; + const mockOptions = { sessionId: '1234' }; + const mockDeps = { savedObjectsClient: {} } as SearchStrategyDependencies; - const id = await service.getId( - searchRequest, - { sessionId, isStored: true, isRestore: true }, - { savedObjectsClient } - ); + await service.search(mockStrategy, mockRequest, mockOptions, mockDeps); - expect(id).toBe(searchId); - }); + expect(mockSearch).toHaveBeenCalled(); + expect(mockSearch).toHaveBeenCalledWith(mockRequest, mockOptions, mockDeps); }); }); diff --git a/src/plugins/data/server/search/session/session_service.ts b/src/plugins/data/server/search/session/session_service.ts index d997af728b60cb..15021436d8821a 100644 --- a/src/plugins/data/server/search/session/session_service.ts +++ b/src/plugins/data/server/search/session/session_service.ts @@ -17,229 +17,27 @@ * under the License. */ -import { CoreStart, KibanaRequest, SavedObjectsClientContract } from 'kibana/server'; -import { from } from 'rxjs'; -import { switchMap } from 'rxjs/operators'; -import { - BackgroundSessionSavedObjectAttributes, - BackgroundSessionStatus, - IKibanaSearchRequest, - IKibanaSearchResponse, - ISearchOptions, - SearchSessionFindOptions, - tapFirst, -} from '../../../common'; -import { BACKGROUND_SESSION_TYPE } from '../../saved_objects'; -import { ISearchStrategy, SearchStrategyDependencies } from '../types'; -import { createRequestHash } from './utils'; - -const DEFAULT_EXPIRATION = 7 * 24 * 60 * 60 * 1000; - -export interface BackgroundSessionDependencies { - savedObjectsClient: SavedObjectsClientContract; -} - -export type ISearchSessionClient = ReturnType< - ReturnType ->; - -export class BackgroundSessionService { - /** - * Map of sessionId to { [requestHash]: searchId } - * @private - */ - private sessionSearchMap = new Map>(); +import { CoreStart, KibanaRequest } from 'kibana/server'; +import { IKibanaSearchRequest, IKibanaSearchResponse } from '../../../common'; +import { ISearchStrategy } from '../types'; +import { ISessionService } from './types'; +/** + * The OSS session service. See data_enhanced in X-Pack for the background session service. + */ +export class SessionService implements ISessionService { constructor() {} - public setup = () => {}; - - public start = (core: CoreStart) => { - return { - asScoped: this.asScopedProvider(core), - }; - }; - - public stop = () => { - this.sessionSearchMap.clear(); - }; - - public search = ( + public search( strategy: ISearchStrategy, - searchRequest: Request, - options: ISearchOptions, - deps: SearchStrategyDependencies - ) => { - // If this is a restored background search session, look up the ID using the provided sessionId - const getSearchRequest = async () => - !options.isRestore || searchRequest.id - ? searchRequest - : { - ...searchRequest, - id: await this.getId(searchRequest, options, deps), - }; - - return from(getSearchRequest()).pipe( - switchMap((request) => strategy.search(request, options, deps)), - tapFirst((response) => { - if (searchRequest.id || !options.sessionId || !response.id || options.isRestore) return; - this.trackId(searchRequest, response.id, options, { - savedObjectsClient: deps.savedObjectsClient, - }); - }) - ); - }; - - // TODO: Generate the `userId` from the realm type/realm name/username - public save = async ( - sessionId: string, - { - name, - appId, - created = new Date().toISOString(), - expires = new Date(Date.now() + DEFAULT_EXPIRATION).toISOString(), - status = BackgroundSessionStatus.IN_PROGRESS, - urlGeneratorId, - initialState = {}, - restoreState = {}, - }: Partial, - { savedObjectsClient }: BackgroundSessionDependencies - ) => { - if (!name) throw new Error('Name is required'); - if (!appId) throw new Error('AppId is required'); - if (!urlGeneratorId) throw new Error('UrlGeneratorId is required'); - - // Get the mapping of request hash/search ID for this session - const searchMap = this.sessionSearchMap.get(sessionId) ?? new Map(); - const idMapping = Object.fromEntries(searchMap.entries()); - const attributes = { - name, - created, - expires, - status, - initialState, - restoreState, - idMapping, - urlGeneratorId, - appId, - }; - const session = await savedObjectsClient.create( - BACKGROUND_SESSION_TYPE, - attributes, - { id: sessionId } - ); - - // Clear out the entries for this session ID so they don't get saved next time - this.sessionSearchMap.delete(sessionId); - - return session; - }; - - // TODO: Throw an error if this session doesn't belong to this user - public get = (sessionId: string, { savedObjectsClient }: BackgroundSessionDependencies) => { - return savedObjectsClient.get( - BACKGROUND_SESSION_TYPE, - sessionId - ); - }; - - // TODO: Throw an error if this session doesn't belong to this user - public find = ( - options: SearchSessionFindOptions, - { savedObjectsClient }: BackgroundSessionDependencies - ) => { - return savedObjectsClient.find({ - ...options, - type: BACKGROUND_SESSION_TYPE, + ...args: Parameters['search']> + ) { + return strategy.search(...args); + } + + public asScopedProvider(core: CoreStart) { + return (request: KibanaRequest) => ({ + search: this.search, }); - }; - - // TODO: Throw an error if this session doesn't belong to this user - public update = ( - sessionId: string, - attributes: Partial, - { savedObjectsClient }: BackgroundSessionDependencies - ) => { - return savedObjectsClient.update( - BACKGROUND_SESSION_TYPE, - sessionId, - attributes - ); - }; - - // TODO: Throw an error if this session doesn't belong to this user - public delete = (sessionId: string, { savedObjectsClient }: BackgroundSessionDependencies) => { - return savedObjectsClient.delete(BACKGROUND_SESSION_TYPE, sessionId); - }; - - /** - * Tracks the given search request/search ID in the saved session (if it exists). Otherwise, just - * store it in memory until a saved session exists. - * @internal - */ - public trackId = async ( - searchRequest: IKibanaSearchRequest, - searchId: string, - { sessionId, isStored }: ISearchOptions, - deps: BackgroundSessionDependencies - ) => { - if (!sessionId || !searchId) return; - const requestHash = createRequestHash(searchRequest.params); - - // If there is already a saved object for this session, update it to include this request/ID. - // Otherwise, just update the in-memory mapping for this session for when the session is saved. - if (isStored) { - const attributes = { idMapping: { [requestHash]: searchId } }; - await this.update(sessionId, attributes, deps); - } else { - const map = this.sessionSearchMap.get(sessionId) ?? new Map(); - map.set(requestHash, searchId); - this.sessionSearchMap.set(sessionId, map); - } - }; - - /** - * Look up an existing search ID that matches the given request in the given session so that the - * request can continue rather than restart. - * @internal - */ - public getId = async ( - searchRequest: IKibanaSearchRequest, - { sessionId, isStored, isRestore }: ISearchOptions, - deps: BackgroundSessionDependencies - ) => { - if (!sessionId) { - throw new Error('Session ID is required'); - } else if (!isStored) { - throw new Error('Cannot get search ID from a session that is not stored'); - } else if (!isRestore) { - throw new Error('Get search ID is only supported when restoring a session'); - } - - const session = await this.get(sessionId, deps); - const requestHash = createRequestHash(searchRequest.params); - if (!session.attributes.idMapping.hasOwnProperty(requestHash)) { - throw new Error('No search ID in this session matching the given search request'); - } - - return session.attributes.idMapping[requestHash]; - }; - - public asScopedProvider = ({ savedObjects }: CoreStart) => { - return (request: KibanaRequest) => { - const savedObjectsClient = savedObjects.getScopedClient(request, { - includedHiddenTypes: [BACKGROUND_SESSION_TYPE], - }); - const deps = { savedObjectsClient }; - return { - save: (sessionId: string, attributes: Partial) => - this.save(sessionId, attributes, deps), - get: (sessionId: string) => this.get(sessionId, deps), - find: (options: SearchSessionFindOptions) => this.find(options, deps), - update: (sessionId: string, attributes: Partial) => - this.update(sessionId, attributes, deps), - delete: (sessionId: string) => this.delete(sessionId, deps), - }; - }; - }; + } } diff --git a/src/plugins/data/common/search/session/types.ts b/src/plugins/data/server/search/session/types.ts similarity index 55% rename from src/plugins/data/common/search/session/types.ts rename to src/plugins/data/server/search/session/types.ts index 50ca3ca390ece0..5e179b99952fea 100644 --- a/src/plugins/data/common/search/session/types.ts +++ b/src/plugins/data/server/search/session/types.ts @@ -17,28 +17,19 @@ * under the License. */ -export interface BackgroundSessionSavedObjectAttributes { - /** - * User-facing session name to be displayed in session management - */ - name: string; - /** - * App that created the session. e.g 'discover' - */ - appId: string; - created: string; - expires: string; - status: string; - urlGeneratorId: string; - initialState: Record; - restoreState: Record; - idMapping: Record; +import { Observable } from 'rxjs'; +import { CoreStart, KibanaRequest } from 'kibana/server'; +import { ISearchStrategy } from '../types'; +import { IKibanaSearchRequest, IKibanaSearchResponse } from '../../../common/search'; + +export interface IScopedSessionService { + search: ( + strategy: ISearchStrategy, + ...args: Parameters['search']> + ) => Observable; + [prop: string]: any; } -export interface SearchSessionFindOptions { - page?: number; - perPage?: number; - sortField?: string; - sortOrder?: string; - filter?: string; +export interface ISessionService { + asScopedProvider: (core: CoreStart) => (request: KibanaRequest) => IScopedSessionService; } diff --git a/src/plugins/data/server/search/session/utils.test.ts b/src/plugins/data/server/search/session/utils.test.ts deleted file mode 100644 index d190f892a7f848..00000000000000 --- a/src/plugins/data/server/search/session/utils.test.ts +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { createRequestHash } from './utils'; - -describe('data/search/session utils', () => { - describe('createRequestHash', () => { - it('ignores `preference`', () => { - const request = { - foo: 'bar', - }; - - const withPreference = { - ...request, - preference: 1234, - }; - - expect(createRequestHash(request)).toEqual(createRequestHash(withPreference)); - }); - }); -}); diff --git a/src/plugins/data/server/search/session/utils.ts b/src/plugins/data/server/search/session/utils.ts deleted file mode 100644 index c3332f80b6e3fa..00000000000000 --- a/src/plugins/data/server/search/session/utils.ts +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { createHash } from 'crypto'; - -/** - * Generate the hash for this request so that, in the future, this hash can be used to look up - * existing search IDs for this request. Ignores the `preference` parameter since it generally won't - * match from one request to another identical request. - */ -export function createRequestHash(keys: Record) { - const { preference, ...params } = keys; - return createHash(`sha256`).update(JSON.stringify(params)).digest('hex'); -} diff --git a/src/plugins/data/server/search/types.ts b/src/plugins/data/server/search/types.ts index ebce02014c2a4e..db8b8ac72d0e56 100644 --- a/src/plugins/data/server/search/types.ts +++ b/src/plugins/data/server/search/types.ts @@ -34,9 +34,11 @@ import { import { AggsSetup, AggsStart } from './aggs'; import { SearchUsage } from './collectors'; import { IEsSearchRequest, IEsSearchResponse } from './es_search'; +import { ISessionService } from './session'; export interface SearchEnhancements { defaultStrategy: string; + sessionService: ISessionService; } export interface SearchStrategyDependencies { diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md index 86ec784834ace6..cb71e5fa0460a7 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -13,8 +13,8 @@ import { BfetchServerSetup } from 'src/plugins/bfetch/server'; import { ConfigDeprecationProvider } from '@kbn/config'; import { CoreSetup } from 'src/core/server'; import { CoreSetup as CoreSetup_2 } from 'kibana/server'; -import { CoreStart } from 'src/core/server'; -import { CoreStart as CoreStart_2 } from 'kibana/server'; +import { CoreStart } from 'kibana/server'; +import { CoreStart as CoreStart_2 } from 'src/core/server'; import { Datatable } from 'src/plugins/expressions'; import { Datatable as Datatable_2 } from 'src/plugins/expressions/common'; import { DatatableColumn } from 'src/plugins/expressions'; @@ -36,7 +36,8 @@ import { ISearchOptions as ISearchOptions_2 } from 'src/plugins/data/public'; import { ISearchSource } from 'src/plugins/data/public'; import { IUiSettingsClient } from 'src/core/server'; import { IUiSettingsClient as IUiSettingsClient_3 } from 'kibana/server'; -import { KibanaRequest } from 'src/core/server'; +import { KibanaRequest } from 'kibana/server'; +import { KibanaRequest as KibanaRequest_2 } from 'src/core/server'; import { LegacyAPICaller } from 'src/core/server'; import { Logger } from 'src/core/server'; import { Logger as Logger_2 } from 'kibana/server'; @@ -738,7 +739,7 @@ export class IndexPatternsService implements Plugin_3 Promise; }; } @@ -781,11 +782,11 @@ export interface ISearchStart ISearchClient; + asScoped: (request: KibanaRequest_2) => ISearchClient; getSearchStrategy: (name?: string) => ISearchStrategy; // (undocumented) searchSource: { - asScoped: (request: KibanaRequest) => Promise; + asScoped: (request: KibanaRequest_2) => Promise; }; } @@ -799,6 +800,16 @@ export interface ISearchStrategy Observable; } +// Warning: (ae-missing-release-tag) "ISessionService" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export interface ISessionService { + // Warning: (ae-forgotten-export) The symbol "IScopedSessionService" needs to be exported by the entry point index.d.ts + // + // (undocumented) + asScopedProvider: (core: CoreStart) => (request: KibanaRequest) => IScopedSessionService; +} + // @public (undocumented) export enum KBN_FIELD_TYPES { // (undocumented) @@ -957,7 +968,7 @@ export class Plugin implements Plugin_2 Promise; }; @@ -1091,6 +1102,19 @@ export function searchUsageObserver(logger: Logger_2, usage?: SearchUsage): { error(): void; }; +// Warning: (ae-missing-release-tag) "SessionService" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public +export class SessionService implements ISessionService { + constructor(); + // (undocumented) + asScopedProvider(core: CoreStart): (request: KibanaRequest) => { + search: , Response_1 extends IKibanaSearchResponse>(strategy: ISearchStrategy, request: Request_1, options: import("../../../common").ISearchOptions, deps: import("../types").SearchStrategyDependencies) => import("rxjs").Observable; + }; + // (undocumented) + search(strategy: ISearchStrategy, ...args: Parameters['search']>): import("rxjs").Observable; +} + // @internal export const shimAbortSignal: (promise: TransportRequestPromise, signal?: AbortSignal | undefined) => TransportRequestPromise; @@ -1236,23 +1260,23 @@ export function usageProvider(core: CoreSetup_2): SearchUsage; // src/plugins/data/server/index.ts:111:26 - (ae-forgotten-export) The symbol "TruncateFormat" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index.ts:137:27 - (ae-forgotten-export) The symbol "isFilterable" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index.ts:137:27 - (ae-forgotten-export) The symbol "isNestedField" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:248:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:248:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:248:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:248:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:250:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:251:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:260:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:261:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:262:1 - (ae-forgotten-export) The symbol "Ipv4Address" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:266:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:267:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:271:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:274:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:275:1 - (ae-forgotten-export) The symbol "calcAutoIntervalLessThan" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:250:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:250:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:250:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:250:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:252:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:253:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:262:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:263:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:264:1 - (ae-forgotten-export) The symbol "Ipv4Address" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:268:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:269:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:273:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:276:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:277:1 - (ae-forgotten-export) The symbol "calcAutoIntervalLessThan" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index_patterns/index_patterns_service.ts:58:14 - (ae-forgotten-export) The symbol "IndexPatternsService" needs to be exported by the entry point index.d.ts // src/plugins/data/server/plugin.ts:90:74 - (ae-forgotten-export) The symbol "DataEnhancements" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/search/types.ts:104:5 - (ae-forgotten-export) The symbol "ISearchStartSearchSource" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/search/types.ts:106:5 - (ae-forgotten-export) The symbol "ISearchStartSearchSource" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/src/plugins/embeddable/public/public.api.md b/src/plugins/embeddable/public/public.api.md index 534ab0f331e87d..6099a2d5f73b4f 100644 --- a/src/plugins/embeddable/public/public.api.md +++ b/src/plugins/embeddable/public/public.api.md @@ -73,6 +73,7 @@ import { SavedObjectAttributes } from 'kibana/server'; import { SavedObjectAttributes as SavedObjectAttributes_2 } from 'src/core/public'; import { SavedObjectAttributes as SavedObjectAttributes_3 } from 'kibana/public'; import { SavedObjectsClientContract as SavedObjectsClientContract_3 } from 'src/core/public'; +import { SavedObjectsFindOptions as SavedObjectsFindOptions_3 } from 'kibana/public'; import { SavedObjectsFindResponse as SavedObjectsFindResponse_2 } from 'kibana/server'; import { Search } from '@elastic/elasticsearch/api/requestParams'; import { SearchResponse } from 'elasticsearch'; diff --git a/x-pack/plugins/data_enhanced/common/index.ts b/x-pack/plugins/data_enhanced/common/index.ts index dd1a2d39ab5d14..7b14a723d78777 100644 --- a/x-pack/plugins/data_enhanced/common/index.ts +++ b/x-pack/plugins/data_enhanced/common/index.ts @@ -12,4 +12,7 @@ export { EqlSearchStrategyResponse, IAsyncSearchOptions, pollSearch, + BackgroundSessionSavedObjectAttributes, + BackgroundSessionFindOptions, + BackgroundSessionStatus, } from './search'; diff --git a/x-pack/plugins/data_enhanced/common/search/index.ts b/x-pack/plugins/data_enhanced/common/search/index.ts index 34bb21cb91af14..5617a1780c269e 100644 --- a/x-pack/plugins/data_enhanced/common/search/index.ts +++ b/x-pack/plugins/data_enhanced/common/search/index.ts @@ -6,3 +6,4 @@ export * from './types'; export * from './poll_search'; +export * from './session'; diff --git a/x-pack/plugins/data_enhanced/common/search/session/index.ts b/x-pack/plugins/data_enhanced/common/search/session/index.ts new file mode 100644 index 00000000000000..ef7f3f1c7f2c4c --- /dev/null +++ b/x-pack/plugins/data_enhanced/common/search/session/index.ts @@ -0,0 +1,8 @@ +/* + * 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. + */ + +export * from './status'; +export * from './types'; diff --git a/x-pack/plugins/data_enhanced/common/search/session/status.ts b/x-pack/plugins/data_enhanced/common/search/session/status.ts new file mode 100644 index 00000000000000..a83dd389e4f13d --- /dev/null +++ b/x-pack/plugins/data_enhanced/common/search/session/status.ts @@ -0,0 +1,13 @@ +/* + * 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. + */ + +export enum BackgroundSessionStatus { + IN_PROGRESS = 'in_progress', + ERROR = 'error', + COMPLETE = 'complete', + CANCELLED = 'cancelled', + EXPIRED = 'expired', +} diff --git a/x-pack/plugins/data_enhanced/common/search/session/types.ts b/x-pack/plugins/data_enhanced/common/search/session/types.ts new file mode 100644 index 00000000000000..0b82c9160ea1a0 --- /dev/null +++ b/x-pack/plugins/data_enhanced/common/search/session/types.ts @@ -0,0 +1,31 @@ +/* + * 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. + */ + +export interface BackgroundSessionSavedObjectAttributes { + /** + * User-facing session name to be displayed in session management + */ + name: string; + /** + * App that created the session. e.g 'discover' + */ + appId: string; + created: string; + expires: string; + status: string; + urlGeneratorId: string; + initialState: Record; + restoreState: Record; + idMapping: Record; +} + +export interface BackgroundSessionFindOptions { + page?: number; + perPage?: number; + sortField?: string; + sortOrder?: string; + filter?: string; +} diff --git a/x-pack/plugins/data_enhanced/server/plugin.ts b/x-pack/plugins/data_enhanced/server/plugin.ts index ad21216bb7035f..956568fbe7632d 100644 --- a/x-pack/plugins/data_enhanced/server/plugin.ts +++ b/x-pack/plugins/data_enhanced/server/plugin.ts @@ -4,22 +4,22 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - PluginInitializerContext, - CoreSetup, - CoreStart, - Plugin, - Logger, -} from '../../../../src/core/server'; +import { CoreSetup, CoreStart, Logger, Plugin, PluginInitializerContext } from 'kibana/server'; import { PluginSetup as DataPluginSetup, PluginStart as DataPluginStart, usageProvider, } from '../../../../src/plugins/data/server'; -import { enhancedEsSearchStrategyProvider, eqlSearchStrategyProvider } from './search'; import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/server'; -import { getUiSettings } from './ui_settings'; import { ENHANCED_ES_SEARCH_STRATEGY, EQL_SEARCH_STRATEGY } from '../common'; +import { registerSessionRoutes } from './routes'; +import { backgroundSessionMapping } from './saved_objects'; +import { + BackgroundSessionService, + enhancedEsSearchStrategyProvider, + eqlSearchStrategyProvider, +} from './search'; +import { getUiSettings } from './ui_settings'; interface SetupDependencies { data: DataPluginSetup; @@ -37,6 +37,7 @@ export class EnhancedDataServerPlugin implements Plugin { let mockCoreSetup: MockedKeys>; diff --git a/src/plugins/data/server/search/routes/session.ts b/x-pack/plugins/data_enhanced/server/routes/session.ts similarity index 85% rename from src/plugins/data/server/search/routes/session.ts rename to x-pack/plugins/data_enhanced/server/routes/session.ts index f7dfc776565e08..b6f1187f497816 100644 --- a/src/plugins/data/server/search/routes/session.ts +++ b/x-pack/plugins/data_enhanced/server/routes/session.ts @@ -1,20 +1,7 @@ /* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. + * 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 { schema } from '@kbn/config-schema'; diff --git a/src/plugins/data/server/saved_objects/background_session.ts b/x-pack/plugins/data_enhanced/server/saved_objects/background_session.ts similarity index 51% rename from src/plugins/data/server/saved_objects/background_session.ts rename to x-pack/plugins/data_enhanced/server/saved_objects/background_session.ts index e81272628c091a..337c5f8ad974f5 100644 --- a/src/plugins/data/server/saved_objects/background_session.ts +++ b/x-pack/plugins/data_enhanced/server/saved_objects/background_session.ts @@ -1,20 +1,7 @@ /* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. + * 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 { SavedObjectsType } from 'kibana/server'; diff --git a/x-pack/plugins/data_enhanced/server/saved_objects/index.ts b/x-pack/plugins/data_enhanced/server/saved_objects/index.ts new file mode 100644 index 00000000000000..4e07fe7117eaaa --- /dev/null +++ b/x-pack/plugins/data_enhanced/server/saved_objects/index.ts @@ -0,0 +1,7 @@ +/* + * 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. + */ + +export * from './background_session'; diff --git a/x-pack/plugins/data_enhanced/server/search/index.ts b/x-pack/plugins/data_enhanced/server/search/index.ts index 64a28cea358e53..67369ef829752f 100644 --- a/x-pack/plugins/data_enhanced/server/search/index.ts +++ b/x-pack/plugins/data_enhanced/server/search/index.ts @@ -6,3 +6,4 @@ export { enhancedEsSearchStrategyProvider } from './es_search_strategy'; export { eqlSearchStrategyProvider } from './eql_search_strategy'; +export * from './session'; diff --git a/x-pack/plugins/data_enhanced/server/search/session/index.ts b/x-pack/plugins/data_enhanced/server/search/session/index.ts new file mode 100644 index 00000000000000..5b75885fb31dfa --- /dev/null +++ b/x-pack/plugins/data_enhanced/server/search/session/index.ts @@ -0,0 +1,7 @@ +/* + * 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. + */ + +export * from './session_service'; diff --git a/x-pack/plugins/data_enhanced/server/search/session/session_service.test.ts b/x-pack/plugins/data_enhanced/server/search/session/session_service.test.ts new file mode 100644 index 00000000000000..95dcb0592e23d5 --- /dev/null +++ b/x-pack/plugins/data_enhanced/server/search/session/session_service.test.ts @@ -0,0 +1,312 @@ +/* + * 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 { of } from 'rxjs'; +import type { SavedObject, SavedObjectsClientContract } from 'kibana/server'; +import type { SearchStrategyDependencies } from '../../../../../../src/plugins/data/server'; +import { savedObjectsClientMock } from '../../../../../../src/core/server/mocks'; +import { BackgroundSessionStatus } from '../../../common'; +import { BACKGROUND_SESSION_TYPE } from '../../saved_objects'; +import { BackgroundSessionDependencies, BackgroundSessionService } from './session_service'; +import { createRequestHash } from './utils'; + +describe('BackgroundSessionService', () => { + let savedObjectsClient: jest.Mocked; + let service: BackgroundSessionService; + + const sessionId = 'd7170a35-7e2c-48d6-8dec-9a056721b489'; + const mockSavedObject: SavedObject = { + id: 'd7170a35-7e2c-48d6-8dec-9a056721b489', + type: BACKGROUND_SESSION_TYPE, + attributes: { + name: 'my_name', + appId: 'my_app_id', + urlGeneratorId: 'my_url_generator_id', + idMapping: {}, + }, + references: [], + }; + + beforeEach(() => { + savedObjectsClient = savedObjectsClientMock.create(); + service = new BackgroundSessionService(); + }); + + it('search throws if `name` is not provided', () => { + expect(() => service.save(sessionId, {}, { savedObjectsClient })).rejects.toMatchInlineSnapshot( + `[Error: Name is required]` + ); + }); + + it('save throws if `name` is not provided', () => { + expect(() => service.save(sessionId, {}, { savedObjectsClient })).rejects.toMatchInlineSnapshot( + `[Error: Name is required]` + ); + }); + + it('get calls saved objects client', async () => { + savedObjectsClient.get.mockResolvedValue(mockSavedObject); + + const response = await service.get(sessionId, { savedObjectsClient }); + + expect(response).toBe(mockSavedObject); + expect(savedObjectsClient.get).toHaveBeenCalledWith(BACKGROUND_SESSION_TYPE, sessionId); + }); + + it('find calls saved objects client', async () => { + const mockFindSavedObject = { + ...mockSavedObject, + score: 1, + }; + const mockResponse = { + saved_objects: [mockFindSavedObject], + total: 1, + per_page: 1, + page: 0, + }; + savedObjectsClient.find.mockResolvedValue(mockResponse); + + const options = { page: 0, perPage: 5 }; + const response = await service.find(options, { savedObjectsClient }); + + expect(response).toBe(mockResponse); + expect(savedObjectsClient.find).toHaveBeenCalledWith({ + ...options, + type: BACKGROUND_SESSION_TYPE, + }); + }); + + it('update calls saved objects client', async () => { + const mockUpdateSavedObject = { + ...mockSavedObject, + attributes: {}, + }; + savedObjectsClient.update.mockResolvedValue(mockUpdateSavedObject); + + const attributes = { name: 'new_name' }; + const response = await service.update(sessionId, attributes, { savedObjectsClient }); + + expect(response).toBe(mockUpdateSavedObject); + expect(savedObjectsClient.update).toHaveBeenCalledWith( + BACKGROUND_SESSION_TYPE, + sessionId, + attributes + ); + }); + + it('delete calls saved objects client', async () => { + savedObjectsClient.delete.mockResolvedValue({}); + + const response = await service.delete(sessionId, { savedObjectsClient }); + + expect(response).toEqual({}); + expect(savedObjectsClient.delete).toHaveBeenCalledWith(BACKGROUND_SESSION_TYPE, sessionId); + }); + + describe('search', () => { + const mockSearch = jest.fn().mockReturnValue(of({})); + const mockStrategy = { search: mockSearch }; + const mockSearchDeps = {} as SearchStrategyDependencies; + const mockDeps = {} as BackgroundSessionDependencies; + + beforeEach(() => { + mockSearch.mockClear(); + }); + + it('searches using the original request if not restoring', async () => { + const searchRequest = { params: {} }; + const options = { sessionId, isStored: false, isRestore: false }; + + await service + .search(mockStrategy, searchRequest, options, mockSearchDeps, mockDeps) + .toPromise(); + + expect(mockSearch).toBeCalledWith(searchRequest, options, mockSearchDeps); + }); + + it('searches using the original request if `id` is provided', async () => { + const searchId = 'FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0'; + const searchRequest = { id: searchId, params: {} }; + const options = { sessionId, isStored: true, isRestore: true }; + + await service + .search(mockStrategy, searchRequest, options, mockSearchDeps, mockDeps) + .toPromise(); + + expect(mockSearch).toBeCalledWith(searchRequest, options, mockSearchDeps); + }); + + it('searches by looking up an `id` if restoring and `id` is not provided', async () => { + const searchRequest = { params: {} }; + const options = { sessionId, isStored: true, isRestore: true }; + const spyGetId = jest.spyOn(service, 'getId').mockResolvedValueOnce('my_id'); + + await service + .search(mockStrategy, searchRequest, options, mockSearchDeps, mockDeps) + .toPromise(); + + expect(mockSearch).toBeCalledWith({ ...searchRequest, id: 'my_id' }, options, mockSearchDeps); + + spyGetId.mockRestore(); + }); + + it('calls `trackId` once if the response contains an `id` and not restoring', async () => { + const searchRequest = { params: {} }; + const options = { sessionId, isStored: false, isRestore: false }; + const spyTrackId = jest.spyOn(service, 'trackId').mockResolvedValue(); + mockSearch.mockReturnValueOnce(of({ id: 'my_id' }, { id: 'my_id' })); + + await service + .search(mockStrategy, searchRequest, options, mockSearchDeps, mockDeps) + .toPromise(); + + expect(spyTrackId).toBeCalledTimes(1); + expect(spyTrackId).toBeCalledWith(searchRequest, 'my_id', options, {}); + + spyTrackId.mockRestore(); + }); + + it('does not call `trackId` if restoring', async () => { + const searchRequest = { params: {} }; + const options = { sessionId, isStored: true, isRestore: true }; + const spyGetId = jest.spyOn(service, 'getId').mockResolvedValueOnce('my_id'); + const spyTrackId = jest.spyOn(service, 'trackId').mockResolvedValue(); + mockSearch.mockReturnValueOnce(of({ id: 'my_id' })); + + await service + .search(mockStrategy, searchRequest, options, mockSearchDeps, mockDeps) + .toPromise(); + + expect(spyTrackId).not.toBeCalled(); + + spyGetId.mockRestore(); + spyTrackId.mockRestore(); + }); + }); + + describe('trackId', () => { + it('stores hash in memory when `isStored` is `false` for when `save` is called', async () => { + const searchRequest = { params: {} }; + const requestHash = createRequestHash(searchRequest.params); + const searchId = 'FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0'; + const isStored = false; + const name = 'my saved background search session'; + const appId = 'my_app_id'; + const urlGeneratorId = 'my_url_generator_id'; + const created = new Date().toISOString(); + const expires = new Date().toISOString(); + + await service.trackId( + searchRequest, + searchId, + { sessionId, isStored }, + { savedObjectsClient } + ); + + expect(savedObjectsClient.update).not.toHaveBeenCalled(); + + await service.save( + sessionId, + { name, created, expires, appId, urlGeneratorId }, + { savedObjectsClient } + ); + + expect(savedObjectsClient.create).toHaveBeenCalledWith( + BACKGROUND_SESSION_TYPE, + { + name, + created, + expires, + initialState: {}, + restoreState: {}, + status: BackgroundSessionStatus.IN_PROGRESS, + idMapping: { [requestHash]: searchId }, + appId, + urlGeneratorId, + }, + { id: sessionId } + ); + }); + + it('updates saved object when `isStored` is `true`', async () => { + const searchRequest = { params: {} }; + const requestHash = createRequestHash(searchRequest.params); + const searchId = 'FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0'; + const isStored = true; + + await service.trackId( + searchRequest, + searchId, + { sessionId, isStored }, + { savedObjectsClient } + ); + + expect(savedObjectsClient.update).toHaveBeenCalledWith(BACKGROUND_SESSION_TYPE, sessionId, { + idMapping: { [requestHash]: searchId }, + }); + }); + }); + + describe('getId', () => { + it('throws if `sessionId` is not provided', () => { + const searchRequest = { params: {} }; + + expect(() => + service.getId(searchRequest, {}, { savedObjectsClient }) + ).rejects.toMatchInlineSnapshot(`[Error: Session ID is required]`); + }); + + it('throws if there is not a saved object', () => { + const searchRequest = { params: {} }; + + expect(() => + service.getId(searchRequest, { sessionId, isStored: false }, { savedObjectsClient }) + ).rejects.toMatchInlineSnapshot( + `[Error: Cannot get search ID from a session that is not stored]` + ); + }); + + it('throws if not restoring a saved session', () => { + const searchRequest = { params: {} }; + + expect(() => + service.getId( + searchRequest, + { sessionId, isStored: true, isRestore: false }, + { savedObjectsClient } + ) + ).rejects.toMatchInlineSnapshot( + `[Error: Get search ID is only supported when restoring a session]` + ); + }); + + it('returns the search ID from the saved object ID mapping', async () => { + const searchRequest = { params: {} }; + const requestHash = createRequestHash(searchRequest.params); + const searchId = 'FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0'; + const mockSession = { + id: 'd7170a35-7e2c-48d6-8dec-9a056721b489', + type: BACKGROUND_SESSION_TYPE, + attributes: { + name: 'my_name', + appId: 'my_app_id', + urlGeneratorId: 'my_url_generator_id', + idMapping: { [requestHash]: searchId }, + }, + references: [], + }; + savedObjectsClient.get.mockResolvedValue(mockSession); + + const id = await service.getId( + searchRequest, + { sessionId, isStored: true, isRestore: true }, + { savedObjectsClient } + ); + + expect(id).toBe(searchId); + }); + }); +}); diff --git a/x-pack/plugins/data_enhanced/server/search/session/session_service.ts b/x-pack/plugins/data_enhanced/server/search/session/session_service.ts new file mode 100644 index 00000000000000..65a9e0901d738f --- /dev/null +++ b/x-pack/plugins/data_enhanced/server/search/session/session_service.ts @@ -0,0 +1,225 @@ +/* + * 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 { CoreStart, KibanaRequest, SavedObjectsClientContract } from 'kibana/server'; +import { from, Observable } from 'rxjs'; +import { switchMap } from 'rxjs/operators'; +import { + IKibanaSearchRequest, + IKibanaSearchResponse, + ISearchOptions, + tapFirst, +} from '../../../../../../src/plugins/data/common'; +import { + ISearchStrategy, + ISessionService, + SearchStrategyDependencies, +} from '../../../../../../src/plugins/data/server'; +import { + BackgroundSessionSavedObjectAttributes, + BackgroundSessionFindOptions, + BackgroundSessionStatus, +} from '../../../common'; +import { BACKGROUND_SESSION_TYPE } from '../../saved_objects'; +import { createRequestHash } from './utils'; + +const DEFAULT_EXPIRATION = 7 * 24 * 60 * 60 * 1000; + +export interface BackgroundSessionDependencies { + savedObjectsClient: SavedObjectsClientContract; +} + +export class BackgroundSessionService implements ISessionService { + /** + * Map of sessionId to { [requestHash]: searchId } + * @private + */ + private sessionSearchMap = new Map>(); + + constructor() {} + + public search( + strategy: ISearchStrategy, + searchRequest: Request, + options: ISearchOptions, + searchDeps: SearchStrategyDependencies, + deps: BackgroundSessionDependencies + ): Observable { + // If this is a restored background search session, look up the ID using the provided sessionId + const getSearchRequest = async () => + !options.isRestore || searchRequest.id + ? searchRequest + : { + ...searchRequest, + id: await this.getId(searchRequest, options, deps), + }; + + return from(getSearchRequest()).pipe( + switchMap((request) => strategy.search(request, options, searchDeps)), + tapFirst((response) => { + if (searchRequest.id || !options.sessionId || !response.id || options.isRestore) return; + this.trackId(searchRequest, response.id, options, deps); + }) + ); + } + + // TODO: Generate the `userId` from the realm type/realm name/username + public save = async ( + sessionId: string, + { + name, + appId, + created = new Date().toISOString(), + expires = new Date(Date.now() + DEFAULT_EXPIRATION).toISOString(), + status = BackgroundSessionStatus.IN_PROGRESS, + urlGeneratorId, + initialState = {}, + restoreState = {}, + }: Partial, + { savedObjectsClient }: BackgroundSessionDependencies + ) => { + if (!name) throw new Error('Name is required'); + if (!appId) throw new Error('AppId is required'); + if (!urlGeneratorId) throw new Error('UrlGeneratorId is required'); + + // Get the mapping of request hash/search ID for this session + const searchMap = this.sessionSearchMap.get(sessionId) ?? new Map(); + const idMapping = Object.fromEntries(searchMap.entries()); + const attributes = { + name, + created, + expires, + status, + initialState, + restoreState, + idMapping, + urlGeneratorId, + appId, + }; + const session = await savedObjectsClient.create( + BACKGROUND_SESSION_TYPE, + attributes, + { id: sessionId } + ); + + // Clear out the entries for this session ID so they don't get saved next time + this.sessionSearchMap.delete(sessionId); + + return session; + }; + + // TODO: Throw an error if this session doesn't belong to this user + public get = (sessionId: string, { savedObjectsClient }: BackgroundSessionDependencies) => { + return savedObjectsClient.get( + BACKGROUND_SESSION_TYPE, + sessionId + ); + }; + + // TODO: Throw an error if this session doesn't belong to this user + public find = ( + options: BackgroundSessionFindOptions, + { savedObjectsClient }: BackgroundSessionDependencies + ) => { + return savedObjectsClient.find({ + ...options, + type: BACKGROUND_SESSION_TYPE, + }); + }; + + // TODO: Throw an error if this session doesn't belong to this user + public update = ( + sessionId: string, + attributes: Partial, + { savedObjectsClient }: BackgroundSessionDependencies + ) => { + return savedObjectsClient.update( + BACKGROUND_SESSION_TYPE, + sessionId, + attributes + ); + }; + + // TODO: Throw an error if this session doesn't belong to this user + public delete = (sessionId: string, { savedObjectsClient }: BackgroundSessionDependencies) => { + return savedObjectsClient.delete(BACKGROUND_SESSION_TYPE, sessionId); + }; + + /** + * Tracks the given search request/search ID in the saved session (if it exists). Otherwise, just + * store it in memory until a saved session exists. + * @internal + */ + public trackId = async ( + searchRequest: IKibanaSearchRequest, + searchId: string, + { sessionId, isStored }: ISearchOptions, + deps: BackgroundSessionDependencies + ) => { + if (!sessionId || !searchId) return; + const requestHash = createRequestHash(searchRequest.params); + + // If there is already a saved object for this session, update it to include this request/ID. + // Otherwise, just update the in-memory mapping for this session for when the session is saved. + if (isStored) { + const attributes = { idMapping: { [requestHash]: searchId } }; + await this.update(sessionId, attributes, deps); + } else { + const map = this.sessionSearchMap.get(sessionId) ?? new Map(); + map.set(requestHash, searchId); + this.sessionSearchMap.set(sessionId, map); + } + }; + + /** + * Look up an existing search ID that matches the given request in the given session so that the + * request can continue rather than restart. + * @internal + */ + public getId = async ( + searchRequest: IKibanaSearchRequest, + { sessionId, isStored, isRestore }: ISearchOptions, + deps: BackgroundSessionDependencies + ) => { + if (!sessionId) { + throw new Error('Session ID is required'); + } else if (!isStored) { + throw new Error('Cannot get search ID from a session that is not stored'); + } else if (!isRestore) { + throw new Error('Get search ID is only supported when restoring a session'); + } + + const session = await this.get(sessionId, deps); + const requestHash = createRequestHash(searchRequest.params); + if (!session.attributes.idMapping.hasOwnProperty(requestHash)) { + throw new Error('No search ID in this session matching the given search request'); + } + + return session.attributes.idMapping[requestHash]; + }; + + public asScopedProvider = ({ savedObjects }: CoreStart) => { + return (request: KibanaRequest) => { + const savedObjectsClient = savedObjects.getScopedClient(request, { + includedHiddenTypes: [BACKGROUND_SESSION_TYPE], + }); + const deps = { savedObjectsClient }; + return { + search: ( + strategy: ISearchStrategy, + ...args: Parameters['search']> + ) => this.search(strategy, ...args, deps), + save: (sessionId: string, attributes: Partial) => + this.save(sessionId, attributes, deps), + get: (sessionId: string) => this.get(sessionId, deps), + find: (options: BackgroundSessionFindOptions) => this.find(options, deps), + update: (sessionId: string, attributes: Partial) => + this.update(sessionId, attributes, deps), + delete: (sessionId: string) => this.delete(sessionId, deps), + }; + }; + }; +} diff --git a/x-pack/plugins/data_enhanced/server/search/session/utils.test.ts b/x-pack/plugins/data_enhanced/server/search/session/utils.test.ts new file mode 100644 index 00000000000000..aae1819a9c3e18 --- /dev/null +++ b/x-pack/plugins/data_enhanced/server/search/session/utils.test.ts @@ -0,0 +1,24 @@ +/* + * 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 { createRequestHash } from './utils'; + +describe('data/search/session utils', () => { + describe('createRequestHash', () => { + it('ignores `preference`', () => { + const request = { + foo: 'bar', + }; + + const withPreference = { + ...request, + preference: 1234, + }; + + expect(createRequestHash(request)).toEqual(createRequestHash(withPreference)); + }); + }); +}); diff --git a/x-pack/plugins/data_enhanced/server/search/session/utils.ts b/x-pack/plugins/data_enhanced/server/search/session/utils.ts new file mode 100644 index 00000000000000..1c314c64f9be3c --- /dev/null +++ b/x-pack/plugins/data_enhanced/server/search/session/utils.ts @@ -0,0 +1,17 @@ +/* + * 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 { createHash } from 'crypto'; + +/** + * Generate the hash for this request so that, in the future, this hash can be used to look up + * existing search IDs for this request. Ignores the `preference` parameter since it generally won't + * match from one request to another identical request. + */ +export function createRequestHash(keys: Record) { + const { preference, ...params } = keys; + return createHash(`sha256`).update(JSON.stringify(params)).digest('hex'); +} From ea14d295cec2c85dbf2a83355327aff05209a538 Mon Sep 17 00:00:00 2001 From: Lukas Olson Date: Thu, 3 Dec 2020 14:23:28 -0700 Subject: [PATCH 03/14] Fix types --- src/plugins/data/server/search/mocks.ts | 15 ----------- .../data_enhanced/server/routes/mocks.ts | 26 +++++++++++++++++++ .../server/routes/session.test.ts | 6 ++--- 3 files changed, 29 insertions(+), 18 deletions(-) create mode 100644 x-pack/plugins/data_enhanced/server/routes/mocks.ts diff --git a/src/plugins/data/server/search/mocks.ts b/src/plugins/data/server/search/mocks.ts index 6d214008e8b173..4914726c85ef8b 100644 --- a/src/plugins/data/server/search/mocks.ts +++ b/src/plugins/data/server/search/mocks.ts @@ -17,8 +17,6 @@ * under the License. */ -import type { RequestHandlerContext } from 'src/core/server'; -import { coreMock } from '../../../../core/server/mocks'; import { ISearchSetup, ISearchStart } from './types'; import { searchAggsSetupMock, searchAggsStartMock } from './aggs/mocks'; import { searchSourceMock } from './search_source/mocks'; @@ -42,16 +40,3 @@ export function createSearchStartMock(): jest.Mocked { searchSource: searchSourceMock.createStartContract(), }; } - -export function createSearchRequestHandlerContext(): jest.Mocked { - return { - core: coreMock.createRequestHandlerContext(), - search: { - search: jest.fn(), - cancel: jest.fn(), - session: { - search: jest.fn(), - }, - }, - }; -} diff --git a/x-pack/plugins/data_enhanced/server/routes/mocks.ts b/x-pack/plugins/data_enhanced/server/routes/mocks.ts new file mode 100644 index 00000000000000..3e7b89ed2cca6a --- /dev/null +++ b/x-pack/plugins/data_enhanced/server/routes/mocks.ts @@ -0,0 +1,26 @@ +/* + * 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 { RequestHandlerContext } from 'kibana/server'; +import { coreMock } from '../../../../../src/core/server/mocks'; + +export function createSearchRequestHandlerContext() { + return ({ + core: coreMock.createRequestHandlerContext(), + search: { + search: jest.fn(), + cancel: jest.fn(), + session: { + search: jest.fn(), + save: jest.fn(), + get: jest.fn(), + find: jest.fn(), + delete: jest.fn(), + update: jest.fn(), + }, + }, + } as unknown) as jest.Mocked; +} diff --git a/x-pack/plugins/data_enhanced/server/routes/session.test.ts b/x-pack/plugins/data_enhanced/server/routes/session.test.ts index 7342e5cda4e0c1..313dfb1e0f1f07 100644 --- a/x-pack/plugins/data_enhanced/server/routes/session.test.ts +++ b/x-pack/plugins/data_enhanced/server/routes/session.test.ts @@ -6,10 +6,10 @@ import type { MockedKeys } from '@kbn/utility-types/jest'; import type { CoreSetup, RequestHandlerContext } from 'kibana/server'; -import { coreMock, httpServerMock } from 'src/core/server/mocks'; -import { registerSessionRoutes } from './session'; +import { coreMock, httpServerMock } from '../../../../../src/core/server/mocks'; import { PluginStart as DataPluginStart } from '../../../../../src/plugins/data/server'; -import { createSearchRequestHandlerContext } from '../../../../../src/plugins/data/server/search/mocks'; +import { createSearchRequestHandlerContext } from './mocks'; +import { registerSessionRoutes } from './session'; describe('registerSessionRoutes', () => { let mockCoreSetup: MockedKeys>; From d5a4bd0789a082d14f4785e23f66cb7536bfd8c6 Mon Sep 17 00:00:00 2001 From: Lukas Olson Date: Thu, 3 Dec 2020 16:59:21 -0700 Subject: [PATCH 04/14] [data.search] Add user information to background session service --- .../common/search/session/types.ts | 3 + x-pack/plugins/data_enhanced/kibana.json | 6 +- x-pack/plugins/data_enhanced/server/plugin.ts | 4 +- .../data_enhanced/server/routes/session.ts | 18 ++-- .../saved_objects/background_session.ts | 9 ++ .../server/search/session/session_service.ts | 101 ++++++++++++++---- 6 files changed, 112 insertions(+), 29 deletions(-) diff --git a/x-pack/plugins/data_enhanced/common/search/session/types.ts b/x-pack/plugins/data_enhanced/common/search/session/types.ts index 0b82c9160ea1a0..4f0e8b0b626ba3 100644 --- a/x-pack/plugins/data_enhanced/common/search/session/types.ts +++ b/x-pack/plugins/data_enhanced/common/search/session/types.ts @@ -5,6 +5,9 @@ */ export interface BackgroundSessionSavedObjectAttributes { + realmType?: string; + realmName?: string; + username?: string; /** * User-facing session name to be displayed in session management */ diff --git a/x-pack/plugins/data_enhanced/kibana.json b/x-pack/plugins/data_enhanced/kibana.json index eea0101ec4ed78..9196b6c3782dcb 100644 --- a/x-pack/plugins/data_enhanced/kibana.json +++ b/x-pack/plugins/data_enhanced/kibana.json @@ -10,7 +10,11 @@ "data", "features" ], - "optionalPlugins": ["kibanaUtils", "usageCollection"], + "optionalPlugins": [ + "kibanaUtils", + "usageCollection", + "security" + ], "server": true, "ui": true, "requiredBundles": ["kibanaUtils", "kibanaReact"] diff --git a/x-pack/plugins/data_enhanced/server/plugin.ts b/x-pack/plugins/data_enhanced/server/plugin.ts index 956568fbe7632d..5aaf721465eec3 100644 --- a/x-pack/plugins/data_enhanced/server/plugin.ts +++ b/x-pack/plugins/data_enhanced/server/plugin.ts @@ -11,6 +11,7 @@ import { usageProvider, } from '../../../../src/plugins/data/server'; import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/server'; +import { SecurityPluginSetup } from '../../security/server'; import { ENHANCED_ES_SEARCH_STRATEGY, EQL_SEARCH_STRATEGY } from '../common'; import { registerSessionRoutes } from './routes'; import { backgroundSessionMapping } from './saved_objects'; @@ -24,6 +25,7 @@ import { getUiSettings } from './ui_settings'; interface SetupDependencies { data: DataPluginSetup; usageCollection?: UsageCollectionSetup; + security?: SecurityPluginSetup; } export class EnhancedDataServerPlugin implements Plugin { @@ -61,7 +63,7 @@ export class EnhancedDataServerPlugin implements Plugin { const { id } = request.params; + const user = security?.authc.getCurrentUser(request); try { - const response = await context.search!.session.get(id); + const response = await context.search!.session.get(user, id); return res.ok({ body: response, @@ -107,8 +110,9 @@ export function registerSessionRoutes(router: IRouter): void { }, async (context, request, res) => { const { page, perPage, sortField, sortOrder, filter } = request.body; + const user = security?.authc.getCurrentUser(request); try { - const response = await context.search!.session.find({ + const response = await context.search!.session.find(user, { page, perPage, sortField, @@ -144,8 +148,9 @@ export function registerSessionRoutes(router: IRouter): void { }, async (context, request, res) => { const { id } = request.params; + const user = security?.authc.getCurrentUser(request); try { - await context.search!.session.delete(id); + await context.search!.session.delete(user, id); return res.ok(); } catch (err) { @@ -178,8 +183,9 @@ export function registerSessionRoutes(router: IRouter): void { async (context, request, res) => { const { id } = request.params; const { name, expires } = request.body; + const user = security?.authc.getCurrentUser(request); try { - const response = await context.search!.session.update(id, { name, expires }); + const response = await context.search!.session.update(user, id, { name, expires }); return res.ok({ body: response, diff --git a/x-pack/plugins/data_enhanced/server/saved_objects/background_session.ts b/x-pack/plugins/data_enhanced/server/saved_objects/background_session.ts index 337c5f8ad974f5..8fe1fad1884bd9 100644 --- a/x-pack/plugins/data_enhanced/server/saved_objects/background_session.ts +++ b/x-pack/plugins/data_enhanced/server/saved_objects/background_session.ts @@ -14,6 +14,15 @@ export const backgroundSessionMapping: SavedObjectsType = { hidden: true, mappings: { properties: { + realmType: { + type: 'keyword', + }, + realmName: { + type: 'keyword', + }, + username: { + type: 'keyword', + }, name: { type: 'keyword', }, diff --git a/x-pack/plugins/data_enhanced/server/search/session/session_service.ts b/x-pack/plugins/data_enhanced/server/search/session/session_service.ts index 65a9e0901d738f..1085f7ce147c82 100644 --- a/x-pack/plugins/data_enhanced/server/search/session/session_service.ts +++ b/x-pack/plugins/data_enhanced/server/search/session/session_service.ts @@ -4,9 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { CoreStart, KibanaRequest, SavedObjectsClientContract } from 'kibana/server'; +import { CoreStart, KibanaRequest, SavedObject, SavedObjectsClientContract } from 'kibana/server'; import { from, Observable } from 'rxjs'; import { switchMap } from 'rxjs/operators'; +import Boom from '@hapi/boom'; import { IKibanaSearchRequest, IKibanaSearchResponse, @@ -18,6 +19,7 @@ import { ISessionService, SearchStrategyDependencies, } from '../../../../../../src/plugins/data/server'; +import { AuthenticatedUser } from '../../../../security/common/model'; import { BackgroundSessionSavedObjectAttributes, BackgroundSessionFindOptions, @@ -66,8 +68,8 @@ export class BackgroundSessionService implements ISessionService { ); } - // TODO: Generate the `userId` from the realm type/realm name/username public save = async ( + user: AuthenticatedUser | null, sessionId: string, { name, @@ -85,10 +87,17 @@ export class BackgroundSessionService implements ISessionService { if (!appId) throw new Error('AppId is required'); if (!urlGeneratorId) throw new Error('UrlGeneratorId is required'); + const realmType = user?.authentication_realm.type; + const realmName = user?.authentication_realm.name; + const username = user?.username; + // Get the mapping of request hash/search ID for this session const searchMap = this.sessionSearchMap.get(sessionId) ?? new Map(); const idMapping = Object.fromEntries(searchMap.entries()); const attributes = { + realmType, + realmName, + username, name, created, expires, @@ -111,41 +120,63 @@ export class BackgroundSessionService implements ISessionService { return session; }; - // TODO: Throw an error if this session doesn't belong to this user - public get = (sessionId: string, { savedObjectsClient }: BackgroundSessionDependencies) => { - return savedObjectsClient.get( + public get = async ( + user: AuthenticatedUser | null, + sessionId: string, + { savedObjectsClient }: BackgroundSessionDependencies + ) => { + const session = await savedObjectsClient.get( BACKGROUND_SESSION_TYPE, sessionId ); + this.throwOnUserConflict(user, session); + return session; }; - // TODO: Throw an error if this session doesn't belong to this user - public find = ( + public find = async ( + user: AuthenticatedUser | null, options: BackgroundSessionFindOptions, { savedObjectsClient }: BackgroundSessionDependencies ) => { + const userFilters = + user === null + ? [] + : [ + `${BACKGROUND_SESSION_TYPE}.attributes.realmType: ${user.authentication_realm.type}`, + `${BACKGROUND_SESSION_TYPE}.attributes.realmName: ${user.authentication_realm.name}`, + `${BACKGROUND_SESSION_TYPE}.attributes.username: ${user.username}`, + ]; + const filter = userFilters.concat(options.filter ?? []).join(' and '); return savedObjectsClient.find({ ...options, + filter, type: BACKGROUND_SESSION_TYPE, }); }; - // TODO: Throw an error if this session doesn't belong to this user - public update = ( + public update = async ( + user: AuthenticatedUser | null, sessionId: string, attributes: Partial, - { savedObjectsClient }: BackgroundSessionDependencies + deps: BackgroundSessionDependencies ) => { - return savedObjectsClient.update( + const session = await this.get(user, sessionId, deps); + this.throwOnUserConflict(user, session); + return deps.savedObjectsClient.update( BACKGROUND_SESSION_TYPE, sessionId, attributes ); }; - // TODO: Throw an error if this session doesn't belong to this user - public delete = (sessionId: string, { savedObjectsClient }: BackgroundSessionDependencies) => { - return savedObjectsClient.delete(BACKGROUND_SESSION_TYPE, sessionId); + public delete = async ( + user: AuthenticatedUser | null, + sessionId: string, + deps: BackgroundSessionDependencies + ) => { + const session = await this.get(user, sessionId, deps); + this.throwOnUserConflict(user, session); + return deps.savedObjectsClient.delete(BACKGROUND_SESSION_TYPE, sessionId); }; /** @@ -212,14 +243,42 @@ export class BackgroundSessionService implements ISessionService { strategy: ISearchStrategy, ...args: Parameters['search']> ) => this.search(strategy, ...args, deps), - save: (sessionId: string, attributes: Partial) => - this.save(sessionId, attributes, deps), - get: (sessionId: string) => this.get(sessionId, deps), - find: (options: BackgroundSessionFindOptions) => this.find(options, deps), - update: (sessionId: string, attributes: Partial) => - this.update(sessionId, attributes, deps), - delete: (sessionId: string) => this.delete(sessionId, deps), + save: ( + user: AuthenticatedUser | null, + sessionId: string, + attributes: Partial + ) => this.save(user, sessionId, attributes, deps), + get: (user: AuthenticatedUser | null, sessionId: string) => this.get(user, sessionId, deps), + find: (user: AuthenticatedUser | null, options: BackgroundSessionFindOptions) => + this.find(user, options, deps), + update: ( + user: AuthenticatedUser | null, + sessionId: string, + attributes: Partial + ) => this.update(user, sessionId, attributes, deps), + delete: (user: AuthenticatedUser | null, sessionId: string) => + this.delete(user, sessionId, deps), }; }; }; + + private throwOnUserConflict = ( + user: AuthenticatedUser | null, + session: SavedObject + ) => { + if (!this.doesUserConflict(user, session)) return; + throw Boom.notFound(); + }; + + private doesUserConflict = ( + user: AuthenticatedUser | null, + session: SavedObject + ) => { + return ( + user !== null && + (user.authentication_realm.type !== session.attributes.realmType || + user.authentication_realm.name !== session.attributes.realmName || + user.username !== session.attributes.username) + ); + }; } From c0994fc06e13b81482bd3cd3d15bfc822e5e539d Mon Sep 17 00:00:00 2001 From: Lukas Olson Date: Fri, 4 Dec 2020 14:11:15 -0700 Subject: [PATCH 05/14] Update trackId & getId to accept user --- x-pack/plugins/data_enhanced/server/plugin.ts | 4 +- .../data_enhanced/server/routes/session.ts | 18 +- .../search/session/session_service.test.ts | 378 +++++++++++++----- .../server/search/session/session_service.ts | 67 ++-- 4 files changed, 322 insertions(+), 145 deletions(-) diff --git a/x-pack/plugins/data_enhanced/server/plugin.ts b/x-pack/plugins/data_enhanced/server/plugin.ts index 5aaf721465eec3..05ec9dc6bc4db0 100644 --- a/x-pack/plugins/data_enhanced/server/plugin.ts +++ b/x-pack/plugins/data_enhanced/server/plugin.ts @@ -58,12 +58,12 @@ export class EnhancedDataServerPlugin implements Plugin { const { id } = request.params; - const user = security?.authc.getCurrentUser(request); try { - const response = await context.search!.session.get(user, id); + const response = await context.search!.session.get(id); return res.ok({ body: response, @@ -110,9 +107,8 @@ export function registerSessionRoutes(router: IRouter, security?: SecurityPlugin }, async (context, request, res) => { const { page, perPage, sortField, sortOrder, filter } = request.body; - const user = security?.authc.getCurrentUser(request); try { - const response = await context.search!.session.find(user, { + const response = await context.search!.session.find({ page, perPage, sortField, @@ -148,9 +144,8 @@ export function registerSessionRoutes(router: IRouter, security?: SecurityPlugin }, async (context, request, res) => { const { id } = request.params; - const user = security?.authc.getCurrentUser(request); try { - await context.search!.session.delete(user, id); + await context.search!.session.delete(id); return res.ok(); } catch (err) { @@ -183,9 +178,8 @@ export function registerSessionRoutes(router: IRouter, security?: SecurityPlugin async (context, request, res) => { const { id } = request.params; const { name, expires } = request.body; - const user = security?.authc.getCurrentUser(request); try { - const response = await context.search!.session.update(user, id, { name, expires }); + const response = await context.search!.session.update(id, { name, expires }); return res.ok({ body: response, diff --git a/x-pack/plugins/data_enhanced/server/search/session/session_service.test.ts b/x-pack/plugins/data_enhanced/server/search/session/session_service.test.ts index 95dcb0592e23d5..4c6f69d9a3dc94 100644 --- a/x-pack/plugins/data_enhanced/server/search/session/session_service.test.ts +++ b/x-pack/plugins/data_enhanced/server/search/session/session_service.test.ts @@ -8,20 +8,37 @@ import { of } from 'rxjs'; import type { SavedObject, SavedObjectsClientContract } from 'kibana/server'; import type { SearchStrategyDependencies } from '../../../../../../src/plugins/data/server'; import { savedObjectsClientMock } from '../../../../../../src/core/server/mocks'; -import { BackgroundSessionStatus } from '../../../common'; import { BACKGROUND_SESSION_TYPE } from '../../saved_objects'; import { BackgroundSessionDependencies, BackgroundSessionService } from './session_service'; import { createRequestHash } from './utils'; +import { AuthenticatedUser } from '../../../../security/common/model'; describe('BackgroundSessionService', () => { let savedObjectsClient: jest.Mocked; let service: BackgroundSessionService; const sessionId = 'd7170a35-7e2c-48d6-8dec-9a056721b489'; - const mockSavedObject: SavedObject = { + const mockUser1 = { + username: 'foo', + authentication_realm: { + type: 'foo', + name: 'foo', + }, + } as AuthenticatedUser; + const mockUser2 = { + username: 'bar', + authentication_realm: { + type: 'bar', + name: 'bar', + }, + } as AuthenticatedUser; + const mockSavedObject: SavedObject = { id: 'd7170a35-7e2c-48d6-8dec-9a056721b489', type: BACKGROUND_SESSION_TYPE, attributes: { + realmType: mockUser1.authentication_realm.type, + realmName: mockUser1.authentication_realm.name, + username: mockUser1.username, name: 'my_name', appId: 'my_app_id', urlGeneratorId: 'my_url_generator_id', @@ -35,75 +52,227 @@ describe('BackgroundSessionService', () => { service = new BackgroundSessionService(); }); - it('search throws if `name` is not provided', () => { - expect(() => service.save(sessionId, {}, { savedObjectsClient })).rejects.toMatchInlineSnapshot( - `[Error: Name is required]` - ); - }); + describe('save', () => { + it('throws if `name` is not provided', () => { + expect(() => + service.save(mockUser1, sessionId, {}, { savedObjectsClient }) + ).rejects.toMatchInlineSnapshot(`[Error: Name is required]`); + }); + + it('calls saved objects client with the user info', async () => { + await service.save( + mockUser1, + sessionId, + { + name: 'my_name', + appId: 'my_app_id', + urlGeneratorId: 'my_url_generator_id', + }, + { savedObjectsClient } + ); + + expect(savedObjectsClient.create).toHaveBeenCalled(); + const [[, attributes]] = savedObjectsClient.create.mock.calls; + expect(attributes).toHaveProperty('realmType', mockUser1.authentication_realm.type); + expect(attributes).toHaveProperty('realmName', mockUser1.authentication_realm.name); + expect(attributes).toHaveProperty('username', mockUser1.username); + }); - it('save throws if `name` is not provided', () => { - expect(() => service.save(sessionId, {}, { savedObjectsClient })).rejects.toMatchInlineSnapshot( - `[Error: Name is required]` - ); + it('works without security', async () => { + await service.save( + null, + sessionId, + { + name: 'my_name', + appId: 'my_app_id', + urlGeneratorId: 'my_url_generator_id', + }, + { savedObjectsClient } + ); + + expect(savedObjectsClient.create).toHaveBeenCalled(); + const [[, attributes]] = savedObjectsClient.create.mock.calls; + expect(attributes).toHaveProperty('realmType', null); + expect(attributes).toHaveProperty('realmName', null); + expect(attributes).toHaveProperty('username', null); + }); }); - it('get calls saved objects client', async () => { - savedObjectsClient.get.mockResolvedValue(mockSavedObject); + describe('get', () => { + it('calls saved objects client', async () => { + savedObjectsClient.get.mockResolvedValue(mockSavedObject); - const response = await service.get(sessionId, { savedObjectsClient }); + const response = await service.get(mockUser1, sessionId, { savedObjectsClient }); + + expect(response).toBe(mockSavedObject); + expect(savedObjectsClient.get).toHaveBeenCalledWith(BACKGROUND_SESSION_TYPE, sessionId); + }); + + it('throws error if user conflicts', () => { + savedObjectsClient.get.mockResolvedValue(mockSavedObject); + + expect( + service.get(mockUser2, sessionId, { savedObjectsClient }) + ).rejects.toMatchInlineSnapshot(`[Error: Not Found]`); + }); - expect(response).toBe(mockSavedObject); - expect(savedObjectsClient.get).toHaveBeenCalledWith(BACKGROUND_SESSION_TYPE, sessionId); + it('works without security', async () => { + savedObjectsClient.get.mockResolvedValue(mockSavedObject); + + const response = await service.get(null, sessionId, { savedObjectsClient }); + + expect(response).toBe(mockSavedObject); + expect(savedObjectsClient.get).toHaveBeenCalledWith(BACKGROUND_SESSION_TYPE, sessionId); + }); }); - it('find calls saved objects client', async () => { - const mockFindSavedObject = { - ...mockSavedObject, - score: 1, - }; - const mockResponse = { - saved_objects: [mockFindSavedObject], - total: 1, - per_page: 1, - page: 0, - }; - savedObjectsClient.find.mockResolvedValue(mockResponse); - - const options = { page: 0, perPage: 5 }; - const response = await service.find(options, { savedObjectsClient }); - - expect(response).toBe(mockResponse); - expect(savedObjectsClient.find).toHaveBeenCalledWith({ - ...options, - type: BACKGROUND_SESSION_TYPE, + describe('find', () => { + it('calls saved objects client with user filter', async () => { + const mockFindSavedObject = { + ...mockSavedObject, + score: 1, + }; + const mockResponse = { + saved_objects: [mockFindSavedObject], + total: 1, + per_page: 1, + page: 0, + }; + savedObjectsClient.find.mockResolvedValue(mockResponse); + + const options = { page: 0, perPage: 5 }; + const response = await service.find(mockUser1, options, { savedObjectsClient }); + + expect(response).toBe(mockResponse); + const [[findOptions]] = savedObjectsClient.find.mock.calls; + expect(findOptions).toMatchInlineSnapshot(` + Object { + "filter": "background-session.attributes.realmType: foo and background-session.attributes.realmName: foo and background-session.attributes.username: foo", + "page": 0, + "perPage": 5, + "type": "background-session", + } + `); + }); + + it('has no filter without security', async () => { + const mockFindSavedObject = { + ...mockSavedObject, + score: 1, + }; + const mockResponse = { + saved_objects: [mockFindSavedObject], + total: 1, + per_page: 1, + page: 0, + }; + savedObjectsClient.find.mockResolvedValue(mockResponse); + + const options = { page: 0, perPage: 5 }; + const response = await service.find(null, options, { savedObjectsClient }); + + expect(response).toBe(mockResponse); + const [[findOptions]] = savedObjectsClient.find.mock.calls; + expect(findOptions).toMatchInlineSnapshot(` + Object { + "filter": "", + "page": 0, + "perPage": 5, + "type": "background-session", + } + `); }); }); - it('update calls saved objects client', async () => { - const mockUpdateSavedObject = { - ...mockSavedObject, - attributes: {}, - }; - savedObjectsClient.update.mockResolvedValue(mockUpdateSavedObject); - - const attributes = { name: 'new_name' }; - const response = await service.update(sessionId, attributes, { savedObjectsClient }); - - expect(response).toBe(mockUpdateSavedObject); - expect(savedObjectsClient.update).toHaveBeenCalledWith( - BACKGROUND_SESSION_TYPE, - sessionId, - attributes - ); + describe('update', () => { + it('update calls saved objects client', async () => { + const mockUpdateSavedObject = { + ...mockSavedObject, + attributes: {}, + }; + savedObjectsClient.get.mockResolvedValue(mockSavedObject); + savedObjectsClient.update.mockResolvedValue(mockUpdateSavedObject); + + const attributes = { name: 'new_name' }; + const response = await service.update(mockUser1, sessionId, attributes, { + savedObjectsClient, + }); + + expect(response).toBe(mockUpdateSavedObject); + expect(savedObjectsClient.update).toHaveBeenCalledWith( + BACKGROUND_SESSION_TYPE, + sessionId, + attributes + ); + }); + + it('throws if user conflicts', () => { + const mockUpdateSavedObject = { + ...mockSavedObject, + attributes: {}, + }; + savedObjectsClient.get.mockResolvedValue(mockSavedObject); + savedObjectsClient.update.mockResolvedValue(mockUpdateSavedObject); + + const attributes = { name: 'new_name' }; + expect( + service.update(mockUser2, sessionId, attributes, { + savedObjectsClient, + }) + ).rejects.toMatchInlineSnapshot(`[Error: Not Found]`); + }); + + it('works without security', async () => { + const mockUpdateSavedObject = { + ...mockSavedObject, + attributes: {}, + }; + savedObjectsClient.get.mockResolvedValue(mockSavedObject); + savedObjectsClient.update.mockResolvedValue(mockUpdateSavedObject); + + const attributes = { name: 'new_name' }; + const response = await service.update(null, sessionId, attributes, { + savedObjectsClient, + }); + + expect(response).toBe(mockUpdateSavedObject); + expect(savedObjectsClient.update).toHaveBeenCalledWith( + BACKGROUND_SESSION_TYPE, + sessionId, + attributes + ); + }); }); - it('delete calls saved objects client', async () => { - savedObjectsClient.delete.mockResolvedValue({}); + describe('delete', () => { + it('calls saved objects client', async () => { + savedObjectsClient.get.mockResolvedValue(mockSavedObject); + savedObjectsClient.delete.mockResolvedValue({}); + + const response = await service.delete(mockUser1, sessionId, { savedObjectsClient }); + + expect(response).toEqual({}); + expect(savedObjectsClient.delete).toHaveBeenCalledWith(BACKGROUND_SESSION_TYPE, sessionId); + }); - const response = await service.delete(sessionId, { savedObjectsClient }); + it('throws if user conflicts', () => { + savedObjectsClient.get.mockResolvedValue(mockSavedObject); + savedObjectsClient.delete.mockResolvedValue({}); - expect(response).toEqual({}); - expect(savedObjectsClient.delete).toHaveBeenCalledWith(BACKGROUND_SESSION_TYPE, sessionId); + expect( + service.delete(mockUser2, sessionId, { savedObjectsClient }) + ).rejects.toMatchInlineSnapshot(`[Error: Not Found]`); + }); + + it('works without security', async () => { + savedObjectsClient.get.mockResolvedValue(mockSavedObject); + savedObjectsClient.delete.mockResolvedValue({}); + + const response = await service.delete(null, sessionId, { savedObjectsClient }); + + expect(response).toEqual({}); + expect(savedObjectsClient.delete).toHaveBeenCalledWith(BACKGROUND_SESSION_TYPE, sessionId); + }); }); describe('search', () => { @@ -121,7 +290,7 @@ describe('BackgroundSessionService', () => { const options = { sessionId, isStored: false, isRestore: false }; await service - .search(mockStrategy, searchRequest, options, mockSearchDeps, mockDeps) + .search(mockUser1, mockStrategy, searchRequest, options, mockSearchDeps, mockDeps) .toPromise(); expect(mockSearch).toBeCalledWith(searchRequest, options, mockSearchDeps); @@ -133,7 +302,7 @@ describe('BackgroundSessionService', () => { const options = { sessionId, isStored: true, isRestore: true }; await service - .search(mockStrategy, searchRequest, options, mockSearchDeps, mockDeps) + .search(mockUser1, mockStrategy, searchRequest, options, mockSearchDeps, mockDeps) .toPromise(); expect(mockSearch).toBeCalledWith(searchRequest, options, mockSearchDeps); @@ -145,7 +314,7 @@ describe('BackgroundSessionService', () => { const spyGetId = jest.spyOn(service, 'getId').mockResolvedValueOnce('my_id'); await service - .search(mockStrategy, searchRequest, options, mockSearchDeps, mockDeps) + .search(mockUser1, mockStrategy, searchRequest, options, mockSearchDeps, mockDeps) .toPromise(); expect(mockSearch).toBeCalledWith({ ...searchRequest, id: 'my_id' }, options, mockSearchDeps); @@ -160,11 +329,11 @@ describe('BackgroundSessionService', () => { mockSearch.mockReturnValueOnce(of({ id: 'my_id' }, { id: 'my_id' })); await service - .search(mockStrategy, searchRequest, options, mockSearchDeps, mockDeps) + .search(mockUser1, mockStrategy, searchRequest, options, mockSearchDeps, mockDeps) .toPromise(); expect(spyTrackId).toBeCalledTimes(1); - expect(spyTrackId).toBeCalledWith(searchRequest, 'my_id', options, {}); + expect(spyTrackId).toBeCalledWith(mockUser1, searchRequest, 'my_id', options, {}); spyTrackId.mockRestore(); }); @@ -177,7 +346,7 @@ describe('BackgroundSessionService', () => { mockSearch.mockReturnValueOnce(of({ id: 'my_id' })); await service - .search(mockStrategy, searchRequest, options, mockSearchDeps, mockDeps) + .search(mockUser1, mockStrategy, searchRequest, options, mockSearchDeps, mockDeps) .toPromise(); expect(spyTrackId).not.toBeCalled(); @@ -190,16 +359,16 @@ describe('BackgroundSessionService', () => { describe('trackId', () => { it('stores hash in memory when `isStored` is `false` for when `save` is called', async () => { const searchRequest = { params: {} }; - const requestHash = createRequestHash(searchRequest.params); const searchId = 'FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0'; const isStored = false; const name = 'my saved background search session'; const appId = 'my_app_id'; const urlGeneratorId = 'my_url_generator_id'; - const created = new Date().toISOString(); - const expires = new Date().toISOString(); + const created = '2020-12-04T20:17:26.367Z'; + const expires = '2020-12-11T20:17:26.367Z'; await service.trackId( + mockUser1, searchRequest, searchId, { sessionId, isStored }, @@ -209,44 +378,64 @@ describe('BackgroundSessionService', () => { expect(savedObjectsClient.update).not.toHaveBeenCalled(); await service.save( + mockUser1, sessionId, { name, created, expires, appId, urlGeneratorId }, { savedObjectsClient } ); - expect(savedObjectsClient.create).toHaveBeenCalledWith( - BACKGROUND_SESSION_TYPE, - { - name, - created, - expires, - initialState: {}, - restoreState: {}, - status: BackgroundSessionStatus.IN_PROGRESS, - idMapping: { [requestHash]: searchId }, - appId, - urlGeneratorId, - }, - { id: sessionId } - ); + expect(savedObjectsClient.create.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "background-session", + Object { + "appId": "my_app_id", + "created": "2020-12-04T20:17:26.367Z", + "expires": "2020-12-11T20:17:26.367Z", + "idMapping": Object { + "44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a": "FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0", + }, + "initialState": Object {}, + "name": "my saved background search session", + "realmName": "foo", + "realmType": "foo", + "restoreState": Object {}, + "status": "in_progress", + "urlGeneratorId": "my_url_generator_id", + "username": "foo", + }, + Object { + "id": "d7170a35-7e2c-48d6-8dec-9a056721b489", + }, + ] + `); }); it('updates saved object when `isStored` is `true`', async () => { const searchRequest = { params: {} }; - const requestHash = createRequestHash(searchRequest.params); const searchId = 'FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0'; const isStored = true; + savedObjectsClient.get.mockResolvedValue(mockSavedObject); + await service.trackId( + mockUser1, searchRequest, searchId, { sessionId, isStored }, { savedObjectsClient } ); - expect(savedObjectsClient.update).toHaveBeenCalledWith(BACKGROUND_SESSION_TYPE, sessionId, { - idMapping: { [requestHash]: searchId }, - }); + expect(savedObjectsClient.update.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "background-session", + "d7170a35-7e2c-48d6-8dec-9a056721b489", + Object { + "idMapping": Object { + "44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a": "FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0", + }, + }, + ] + `); }); }); @@ -255,7 +444,7 @@ describe('BackgroundSessionService', () => { const searchRequest = { params: {} }; expect(() => - service.getId(searchRequest, {}, { savedObjectsClient }) + service.getId(mockUser1, searchRequest, {}, { savedObjectsClient }) ).rejects.toMatchInlineSnapshot(`[Error: Session ID is required]`); }); @@ -263,7 +452,12 @@ describe('BackgroundSessionService', () => { const searchRequest = { params: {} }; expect(() => - service.getId(searchRequest, { sessionId, isStored: false }, { savedObjectsClient }) + service.getId( + mockUser1, + searchRequest, + { sessionId, isStored: false }, + { savedObjectsClient } + ) ).rejects.toMatchInlineSnapshot( `[Error: Cannot get search ID from a session that is not stored]` ); @@ -274,6 +468,7 @@ describe('BackgroundSessionService', () => { expect(() => service.getId( + mockUser1, searchRequest, { sessionId, isStored: true, isRestore: false }, { savedObjectsClient } @@ -288,19 +483,16 @@ describe('BackgroundSessionService', () => { const requestHash = createRequestHash(searchRequest.params); const searchId = 'FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0'; const mockSession = { - id: 'd7170a35-7e2c-48d6-8dec-9a056721b489', - type: BACKGROUND_SESSION_TYPE, + ...mockSavedObject, attributes: { - name: 'my_name', - appId: 'my_app_id', - urlGeneratorId: 'my_url_generator_id', + ...mockSavedObject.attributes, idMapping: { [requestHash]: searchId }, }, - references: [], }; savedObjectsClient.get.mockResolvedValue(mockSession); const id = await service.getId( + mockUser1, searchRequest, { sessionId, isStored: true, isRestore: true }, { savedObjectsClient } diff --git a/x-pack/plugins/data_enhanced/server/search/session/session_service.ts b/x-pack/plugins/data_enhanced/server/search/session/session_service.ts index 1085f7ce147c82..d786bdc7a0dc5e 100644 --- a/x-pack/plugins/data_enhanced/server/search/session/session_service.ts +++ b/x-pack/plugins/data_enhanced/server/search/session/session_service.ts @@ -20,6 +20,7 @@ import { SearchStrategyDependencies, } from '../../../../../../src/plugins/data/server'; import { AuthenticatedUser } from '../../../../security/common/model'; +import { SecurityPluginSetup } from '../../../../security/server'; import { BackgroundSessionSavedObjectAttributes, BackgroundSessionFindOptions, @@ -41,9 +42,10 @@ export class BackgroundSessionService implements ISessionService { */ private sessionSearchMap = new Map>(); - constructor() {} + constructor(private security?: SecurityPluginSetup) {} public search( + user: AuthenticatedUser | null, strategy: ISearchStrategy, searchRequest: Request, options: ISearchOptions, @@ -56,14 +58,14 @@ export class BackgroundSessionService implements ISessionService { ? searchRequest : { ...searchRequest, - id: await this.getId(searchRequest, options, deps), + id: await this.getId(user, searchRequest, options, deps), }; return from(getSearchRequest()).pipe( switchMap((request) => strategy.search(request, options, searchDeps)), tapFirst((response) => { if (searchRequest.id || !options.sessionId || !response.id || options.isRestore) return; - this.trackId(searchRequest, response.id, options, deps); + this.trackId(user, searchRequest, response.id, options, deps); }) ); } @@ -87,9 +89,9 @@ export class BackgroundSessionService implements ISessionService { if (!appId) throw new Error('AppId is required'); if (!urlGeneratorId) throw new Error('UrlGeneratorId is required'); - const realmType = user?.authentication_realm.type; - const realmName = user?.authentication_realm.name; - const username = user?.username; + const realmType = user?.authentication_realm.type ?? null; + const realmName = user?.authentication_realm.name ?? null; + const username = user?.username ?? null; // Get the mapping of request hash/search ID for this session const searchMap = this.sessionSearchMap.get(sessionId) ?? new Map(); @@ -185,6 +187,7 @@ export class BackgroundSessionService implements ISessionService { * @internal */ public trackId = async ( + user: AuthenticatedUser | null, searchRequest: IKibanaSearchRequest, searchId: string, { sessionId, isStored }: ISearchOptions, @@ -197,7 +200,7 @@ export class BackgroundSessionService implements ISessionService { // Otherwise, just update the in-memory mapping for this session for when the session is saved. if (isStored) { const attributes = { idMapping: { [requestHash]: searchId } }; - await this.update(sessionId, attributes, deps); + await this.update(user, sessionId, attributes, deps); } else { const map = this.sessionSearchMap.get(sessionId) ?? new Map(); map.set(requestHash, searchId); @@ -211,6 +214,7 @@ export class BackgroundSessionService implements ISessionService { * @internal */ public getId = async ( + user: AuthenticatedUser | null, searchRequest: IKibanaSearchRequest, { sessionId, isStored, isRestore }: ISearchOptions, deps: BackgroundSessionDependencies @@ -223,7 +227,7 @@ export class BackgroundSessionService implements ISessionService { throw new Error('Get search ID is only supported when restoring a session'); } - const session = await this.get(sessionId, deps); + const session = await this.get(user, sessionId, deps); const requestHash = createRequestHash(searchRequest.params); if (!session.attributes.idMapping.hasOwnProperty(requestHash)) { throw new Error('No search ID in this session matching the given search request'); @@ -234,6 +238,7 @@ export class BackgroundSessionService implements ISessionService { public asScopedProvider = ({ savedObjects }: CoreStart) => { return (request: KibanaRequest) => { + const user = this.security?.authc.getCurrentUser(request) ?? null; const savedObjectsClient = savedObjects.getScopedClient(request, { includedHiddenTypes: [BACKGROUND_SESSION_TYPE], }); @@ -242,22 +247,14 @@ export class BackgroundSessionService implements ISessionService { search: ( strategy: ISearchStrategy, ...args: Parameters['search']> - ) => this.search(strategy, ...args, deps), - save: ( - user: AuthenticatedUser | null, - sessionId: string, - attributes: Partial - ) => this.save(user, sessionId, attributes, deps), - get: (user: AuthenticatedUser | null, sessionId: string) => this.get(user, sessionId, deps), - find: (user: AuthenticatedUser | null, options: BackgroundSessionFindOptions) => - this.find(user, options, deps), - update: ( - user: AuthenticatedUser | null, - sessionId: string, - attributes: Partial - ) => this.update(user, sessionId, attributes, deps), - delete: (user: AuthenticatedUser | null, sessionId: string) => - this.delete(user, sessionId, deps), + ) => this.search(user, strategy, ...args, deps), + save: (sessionId: string, attributes: Partial) => + this.save(user, sessionId, attributes, deps), + get: (sessionId: string) => this.get(user, sessionId, deps), + find: (options: BackgroundSessionFindOptions) => this.find(user, options, deps), + update: (sessionId: string, attributes: Partial) => + this.update(user, sessionId, attributes, deps), + delete: (sessionId: string) => this.delete(user, sessionId, deps), }; }; }; @@ -266,19 +263,13 @@ export class BackgroundSessionService implements ISessionService { user: AuthenticatedUser | null, session: SavedObject ) => { - if (!this.doesUserConflict(user, session)) return; - throw Boom.notFound(); - }; - - private doesUserConflict = ( - user: AuthenticatedUser | null, - session: SavedObject - ) => { - return ( - user !== null && - (user.authentication_realm.type !== session.attributes.realmType || - user.authentication_realm.name !== session.attributes.realmName || - user.username !== session.attributes.username) - ); + if (user === null) return; + if ( + user.authentication_realm.type !== session.attributes.realmType || + user.authentication_realm.name !== session.attributes.realmName || + user.username !== session.attributes.username + ) { + throw Boom.notFound(); + } }; } From cc665d939891f5db50262b634616e533dabd3fef Mon Sep 17 00:00:00 2001 From: Lukas Olson Date: Mon, 7 Dec 2020 10:28:01 -0700 Subject: [PATCH 06/14] Fix remaining merge conflicts --- .../search/session/session_service.test.ts | 188 +----------------- 1 file changed, 3 insertions(+), 185 deletions(-) diff --git a/x-pack/plugins/data_enhanced/server/search/session/session_service.test.ts b/x-pack/plugins/data_enhanced/server/search/session/session_service.test.ts index 7de26941ea8ea9..2711be7ceecc5d 100644 --- a/x-pack/plugins/data_enhanced/server/search/session/session_service.test.ts +++ b/x-pack/plugins/data_enhanced/server/search/session/session_service.test.ts @@ -8,24 +8,16 @@ import { of } from 'rxjs'; import type { SavedObject, SavedObjectsClientContract } from 'kibana/server'; import type { SearchStrategyDependencies } from '../../../../../../src/plugins/data/server'; import { savedObjectsClientMock } from '../../../../../../src/core/server/mocks'; -<<<<<<< HEAD import { BACKGROUND_SESSION_TYPE } from '../../saved_objects'; import { BackgroundSessionDependencies, BackgroundSessionService } from './session_service'; import { createRequestHash } from './utils'; import { AuthenticatedUser } from '../../../../security/common/model'; -======= -import { BackgroundSessionStatus } from '../../../common'; -import { BACKGROUND_SESSION_TYPE } from '../../saved_objects'; -import { BackgroundSessionDependencies, BackgroundSessionService } from './session_service'; -import { createRequestHash } from './utils'; ->>>>>>> master describe('BackgroundSessionService', () => { let savedObjectsClient: jest.Mocked; let service: BackgroundSessionService; const sessionId = 'd7170a35-7e2c-48d6-8dec-9a056721b489'; -<<<<<<< HEAD const mockUser1 = { username: 'foo', authentication_realm: { @@ -47,12 +39,6 @@ describe('BackgroundSessionService', () => { realmType: mockUser1.authentication_realm.type, realmName: mockUser1.authentication_realm.name, username: mockUser1.username, -======= - const mockSavedObject: SavedObject = { - id: 'd7170a35-7e2c-48d6-8dec-9a056721b489', - type: BACKGROUND_SESSION_TYPE, - attributes: { ->>>>>>> master name: 'my_name', appId: 'my_app_id', urlGeneratorId: 'my_url_generator_id', @@ -66,7 +52,6 @@ describe('BackgroundSessionService', () => { service = new BackgroundSessionService(); }); -<<<<<<< HEAD describe('save', () => { it('throws if `name` is not provided', () => { expect(() => @@ -107,9 +92,9 @@ describe('BackgroundSessionService', () => { expect(savedObjectsClient.create).toHaveBeenCalled(); const [[, attributes]] = savedObjectsClient.create.mock.calls; - expect(attributes).toHaveProperty('realmType', null); - expect(attributes).toHaveProperty('realmName', null); - expect(attributes).toHaveProperty('username', null); + expect(attributes).toHaveProperty('realmType', undefined); + expect(attributes).toHaveProperty('realmName', undefined); + expect(attributes).toHaveProperty('username', undefined); }); }); @@ -288,77 +273,6 @@ describe('BackgroundSessionService', () => { expect(response).toEqual({}); expect(savedObjectsClient.delete).toHaveBeenCalledWith(BACKGROUND_SESSION_TYPE, sessionId); }); -======= - it('search throws if `name` is not provided', () => { - expect(() => service.save(sessionId, {}, { savedObjectsClient })).rejects.toMatchInlineSnapshot( - `[Error: Name is required]` - ); - }); - - it('save throws if `name` is not provided', () => { - expect(() => service.save(sessionId, {}, { savedObjectsClient })).rejects.toMatchInlineSnapshot( - `[Error: Name is required]` - ); - }); - - it('get calls saved objects client', async () => { - savedObjectsClient.get.mockResolvedValue(mockSavedObject); - - const response = await service.get(sessionId, { savedObjectsClient }); - - expect(response).toBe(mockSavedObject); - expect(savedObjectsClient.get).toHaveBeenCalledWith(BACKGROUND_SESSION_TYPE, sessionId); - }); - - it('find calls saved objects client', async () => { - const mockFindSavedObject = { - ...mockSavedObject, - score: 1, - }; - const mockResponse = { - saved_objects: [mockFindSavedObject], - total: 1, - per_page: 1, - page: 0, - }; - savedObjectsClient.find.mockResolvedValue(mockResponse); - - const options = { page: 0, perPage: 5 }; - const response = await service.find(options, { savedObjectsClient }); - - expect(response).toBe(mockResponse); - expect(savedObjectsClient.find).toHaveBeenCalledWith({ - ...options, - type: BACKGROUND_SESSION_TYPE, - }); - }); - - it('update calls saved objects client', async () => { - const mockUpdateSavedObject = { - ...mockSavedObject, - attributes: {}, - }; - savedObjectsClient.update.mockResolvedValue(mockUpdateSavedObject); - - const attributes = { name: 'new_name' }; - const response = await service.update(sessionId, attributes, { savedObjectsClient }); - - expect(response).toBe(mockUpdateSavedObject); - expect(savedObjectsClient.update).toHaveBeenCalledWith( - BACKGROUND_SESSION_TYPE, - sessionId, - attributes - ); - }); - - it('delete calls saved objects client', async () => { - savedObjectsClient.delete.mockResolvedValue({}); - - const response = await service.delete(sessionId, { savedObjectsClient }); - - expect(response).toEqual({}); - expect(savedObjectsClient.delete).toHaveBeenCalledWith(BACKGROUND_SESSION_TYPE, sessionId); ->>>>>>> master }); describe('search', () => { @@ -376,11 +290,7 @@ describe('BackgroundSessionService', () => { const options = { sessionId, isStored: false, isRestore: false }; await service -<<<<<<< HEAD .search(mockUser1, mockStrategy, searchRequest, options, mockSearchDeps, mockDeps) -======= - .search(mockStrategy, searchRequest, options, mockSearchDeps, mockDeps) ->>>>>>> master .toPromise(); expect(mockSearch).toBeCalledWith(searchRequest, options, mockSearchDeps); @@ -392,11 +302,7 @@ describe('BackgroundSessionService', () => { const options = { sessionId, isStored: true, isRestore: true }; await service -<<<<<<< HEAD .search(mockUser1, mockStrategy, searchRequest, options, mockSearchDeps, mockDeps) -======= - .search(mockStrategy, searchRequest, options, mockSearchDeps, mockDeps) ->>>>>>> master .toPromise(); expect(mockSearch).toBeCalledWith(searchRequest, options, mockSearchDeps); @@ -408,11 +314,7 @@ describe('BackgroundSessionService', () => { const spyGetId = jest.spyOn(service, 'getId').mockResolvedValueOnce('my_id'); await service -<<<<<<< HEAD .search(mockUser1, mockStrategy, searchRequest, options, mockSearchDeps, mockDeps) -======= - .search(mockStrategy, searchRequest, options, mockSearchDeps, mockDeps) ->>>>>>> master .toPromise(); expect(mockSearch).toBeCalledWith({ ...searchRequest, id: 'my_id' }, options, mockSearchDeps); @@ -427,19 +329,11 @@ describe('BackgroundSessionService', () => { mockSearch.mockReturnValueOnce(of({ id: 'my_id' }, { id: 'my_id' })); await service -<<<<<<< HEAD .search(mockUser1, mockStrategy, searchRequest, options, mockSearchDeps, mockDeps) .toPromise(); expect(spyTrackId).toBeCalledTimes(1); expect(spyTrackId).toBeCalledWith(mockUser1, searchRequest, 'my_id', options, {}); -======= - .search(mockStrategy, searchRequest, options, mockSearchDeps, mockDeps) - .toPromise(); - - expect(spyTrackId).toBeCalledTimes(1); - expect(spyTrackId).toBeCalledWith(searchRequest, 'my_id', options, {}); ->>>>>>> master spyTrackId.mockRestore(); }); @@ -452,11 +346,7 @@ describe('BackgroundSessionService', () => { mockSearch.mockReturnValueOnce(of({ id: 'my_id' })); await service -<<<<<<< HEAD .search(mockUser1, mockStrategy, searchRequest, options, mockSearchDeps, mockDeps) -======= - .search(mockStrategy, searchRequest, options, mockSearchDeps, mockDeps) ->>>>>>> master .toPromise(); expect(spyTrackId).not.toBeCalled(); @@ -469,27 +359,16 @@ describe('BackgroundSessionService', () => { describe('trackId', () => { it('stores hash in memory when `isStored` is `false` for when `save` is called', async () => { const searchRequest = { params: {} }; -<<<<<<< HEAD -======= - const requestHash = createRequestHash(searchRequest.params); ->>>>>>> master const searchId = 'FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0'; const isStored = false; const name = 'my saved background search session'; const appId = 'my_app_id'; const urlGeneratorId = 'my_url_generator_id'; -<<<<<<< HEAD const created = '2020-12-04T20:17:26.367Z'; const expires = '2020-12-11T20:17:26.367Z'; await service.trackId( mockUser1, -======= - const created = new Date().toISOString(); - const expires = new Date().toISOString(); - - await service.trackId( ->>>>>>> master searchRequest, searchId, { sessionId, isStored }, @@ -499,16 +378,12 @@ describe('BackgroundSessionService', () => { expect(savedObjectsClient.update).not.toHaveBeenCalled(); await service.save( -<<<<<<< HEAD mockUser1, -======= ->>>>>>> master sessionId, { name, created, expires, appId, urlGeneratorId }, { savedObjectsClient } ); -<<<<<<< HEAD expect(savedObjectsClient.create.mock.calls[0]).toMatchInlineSnapshot(` Array [ "background-session", @@ -533,28 +408,10 @@ describe('BackgroundSessionService', () => { }, ] `); -======= - expect(savedObjectsClient.create).toHaveBeenCalledWith( - BACKGROUND_SESSION_TYPE, - { - name, - created, - expires, - initialState: {}, - restoreState: {}, - status: BackgroundSessionStatus.IN_PROGRESS, - idMapping: { [requestHash]: searchId }, - appId, - urlGeneratorId, - }, - { id: sessionId } - ); ->>>>>>> master }); it('updates saved object when `isStored` is `true`', async () => { const searchRequest = { params: {} }; -<<<<<<< HEAD const searchId = 'FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0'; const isStored = true; @@ -562,20 +419,12 @@ describe('BackgroundSessionService', () => { await service.trackId( mockUser1, -======= - const requestHash = createRequestHash(searchRequest.params); - const searchId = 'FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0'; - const isStored = true; - - await service.trackId( ->>>>>>> master searchRequest, searchId, { sessionId, isStored }, { savedObjectsClient } ); -<<<<<<< HEAD expect(savedObjectsClient.update.mock.calls[0]).toMatchInlineSnapshot(` Array [ "background-session", @@ -587,11 +436,6 @@ describe('BackgroundSessionService', () => { }, ] `); -======= - expect(savedObjectsClient.update).toHaveBeenCalledWith(BACKGROUND_SESSION_TYPE, sessionId, { - idMapping: { [requestHash]: searchId }, - }); ->>>>>>> master }); }); @@ -600,11 +444,7 @@ describe('BackgroundSessionService', () => { const searchRequest = { params: {} }; expect(() => -<<<<<<< HEAD service.getId(mockUser1, searchRequest, {}, { savedObjectsClient }) -======= - service.getId(searchRequest, {}, { savedObjectsClient }) ->>>>>>> master ).rejects.toMatchInlineSnapshot(`[Error: Session ID is required]`); }); @@ -612,16 +452,12 @@ describe('BackgroundSessionService', () => { const searchRequest = { params: {} }; expect(() => -<<<<<<< HEAD service.getId( mockUser1, searchRequest, { sessionId, isStored: false }, { savedObjectsClient } ) -======= - service.getId(searchRequest, { sessionId, isStored: false }, { savedObjectsClient }) ->>>>>>> master ).rejects.toMatchInlineSnapshot( `[Error: Cannot get search ID from a session that is not stored]` ); @@ -632,10 +468,7 @@ describe('BackgroundSessionService', () => { expect(() => service.getId( -<<<<<<< HEAD mockUser1, -======= ->>>>>>> master searchRequest, { sessionId, isStored: true, isRestore: false }, { savedObjectsClient } @@ -650,31 +483,16 @@ describe('BackgroundSessionService', () => { const requestHash = createRequestHash(searchRequest.params); const searchId = 'FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0'; const mockSession = { -<<<<<<< HEAD ...mockSavedObject, attributes: { ...mockSavedObject.attributes, idMapping: { [requestHash]: searchId }, }, -======= - id: 'd7170a35-7e2c-48d6-8dec-9a056721b489', - type: BACKGROUND_SESSION_TYPE, - attributes: { - name: 'my_name', - appId: 'my_app_id', - urlGeneratorId: 'my_url_generator_id', - idMapping: { [requestHash]: searchId }, - }, - references: [], ->>>>>>> master }; savedObjectsClient.get.mockResolvedValue(mockSession); const id = await service.getId( -<<<<<<< HEAD mockUser1, -======= ->>>>>>> master searchRequest, { sessionId, isStored: true, isRestore: true }, { savedObjectsClient } From c3165ffc80289f35616dce6d69a5901d6a372f31 Mon Sep 17 00:00:00 2001 From: Lukas Olson Date: Fri, 29 Jan 2021 10:29:12 -0700 Subject: [PATCH 07/14] Fix test --- .../server/search/session/session_service.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/data_enhanced/server/search/session/session_service.test.ts b/x-pack/plugins/data_enhanced/server/search/session/session_service.test.ts index d6192c33762df6..84245e3ea4340c 100644 --- a/x-pack/plugins/data_enhanced/server/search/session/session_service.test.ts +++ b/x-pack/plugins/data_enhanced/server/search/session/session_service.test.ts @@ -487,8 +487,8 @@ describe('SearchSessionService', () => { const name = 'my saved background search session'; const appId = 'my_app_id'; const urlGeneratorId = 'my_url_generator_id'; - const created = new Date().toISOString(); - const expires = new Date().toISOString(); + const created = '2021-01-28T19:02:57.466Z'; + const expires = '2021-02-28T19:02:57.466Z'; const mockIdMapping = createMockIdMapping([]); const setSpy = jest.fn(); @@ -520,7 +520,7 @@ describe('SearchSessionService', () => { Object { "appId": "my_app_id", "created": "2021-01-28T19:02:57.466Z", - "expires": "2021-01-28T19:02:57.466Z", + "expires": "2021-02-28T19:02:57.466Z", "idMapping": Object {}, "initialState": Object {}, "name": "my saved background search session", From 9fefbeaa97c6220b4fa7e3d264a51169fa1d5a94 Mon Sep 17 00:00:00 2001 From: Lukas Olson Date: Mon, 1 Feb 2021 15:10:46 -0700 Subject: [PATCH 08/14] Remove todos --- .../data_enhanced/server/search/session/session_service.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/x-pack/plugins/data_enhanced/server/search/session/session_service.ts b/x-pack/plugins/data_enhanced/server/search/session/session_service.ts index 76265a3d6168c7..cfbf4272e71df1 100644 --- a/x-pack/plugins/data_enhanced/server/search/session/session_service.ts +++ b/x-pack/plugins/data_enhanced/server/search/session/session_service.ts @@ -257,7 +257,6 @@ export class SearchSessionService implements ISessionService { ); } - // TODO: Generate the `userId` from the realm type/realm name/username public save = async ( user: AuthenticatedUser | null, sessionId: string, @@ -307,7 +306,6 @@ export class SearchSessionService implements ISessionService { return session; }; - // TODO: Throw an error if this session doesn't belong to this user public get = async ( user: AuthenticatedUser | null, sessionId: string, @@ -322,7 +320,6 @@ export class SearchSessionService implements ISessionService { return session; }; - // TODO: Throw an error if this session doesn't belong to this user public find = ( user: AuthenticatedUser | null, options: Omit, @@ -344,7 +341,6 @@ export class SearchSessionService implements ISessionService { }); }; - // TODO: Throw an error if this session doesn't belong to this user public update = async ( user: AuthenticatedUser | null, sessionId: string, @@ -361,7 +357,6 @@ export class SearchSessionService implements ISessionService { ); }; - // TODO: Throw an error if this session doesn't belong to this user public delete = async ( user: AuthenticatedUser | null, sessionId: string, From bc8202364c751ebec787169ab4c1f46d434f6d4f Mon Sep 17 00:00:00 2001 From: Lukas Olson Date: Mon, 8 Feb 2021 17:10:14 -0700 Subject: [PATCH 09/14] Fix session service to use user --- .../server/search/session/session_service.ts | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/x-pack/plugins/data_enhanced/server/search/session/session_service.ts b/x-pack/plugins/data_enhanced/server/search/session/session_service.ts index cf6de003c3302d..336a953648e81b 100644 --- a/x-pack/plugins/data_enhanced/server/search/session/session_service.ts +++ b/x-pack/plugins/data_enhanced/server/search/session/session_service.ts @@ -110,7 +110,7 @@ export class SearchSessionService if (SavedObjectsErrorHelpers.isNotFoundError(e)) { try { this.logger.debug(`Object not found | ${sessionId}`); - return await this.create(deps, sessionId, attributes); + return await this.create(deps, user, sessionId, attributes); } catch (createError) { if ( SavedObjectsErrorHelpers.isConflictError(createError) && @@ -150,10 +150,6 @@ export class SearchSessionService if (!appId) throw new Error('AppId is required'); if (!urlGeneratorId) throw new Error('UrlGeneratorId is required'); - const realmType = user?.authentication_realm.type; - const realmName = user?.authentication_realm.name; - const username = user?.username; - return this.updateOrCreate(deps, user, sessionId, { name, appId, @@ -161,18 +157,21 @@ export class SearchSessionService initialState, restoreState, persisted: true, - realmType, - realmName, - username, }); }; private create = ( { savedObjectsClient }: SearchSessionDependencies, + user: AuthenticatedUser | null, sessionId: string, attributes: Partial ) => { this.logger.debug(`create | ${sessionId}`); + + const realmType = user?.authentication_realm.type; + const realmName = user?.authentication_realm.name; + const username = user?.username; + return savedObjectsClient.create( SEARCH_SESSION_TYPE, { @@ -185,6 +184,9 @@ export class SearchSessionService touched: new Date().toISOString(), idMapping: {}, persisted: false, + realmType, + realmName, + username, ...attributes, }, { id: sessionId } From 502b8e7d8f9f47189ca2a47b68cc41a26797041c Mon Sep 17 00:00:00 2001 From: Lukas Olson Date: Tue, 9 Feb 2021 15:05:32 -0700 Subject: [PATCH 10/14] Remove user conflicts and update SO filter --- .../search/session/session_service.test.ts | 161 +++++++++++++----- .../server/search/session/session_service.ts | 52 +++--- 2 files changed, 138 insertions(+), 75 deletions(-) diff --git a/x-pack/plugins/data_enhanced/server/search/session/session_service.test.ts b/x-pack/plugins/data_enhanced/server/search/session/session_service.test.ts index 37cc7a8622a574..fe4924360f0d92 100644 --- a/x-pack/plugins/data_enhanced/server/search/session/session_service.test.ts +++ b/x-pack/plugins/data_enhanced/server/search/session/session_service.test.ts @@ -39,13 +39,6 @@ describe('SearchSessionService', () => { name: 'my_realm_name', }, } as AuthenticatedUser; - const mockUser2 = { - username: 'bar', - authentication_realm: { - type: 'bar', - name: 'bar', - }, - } as AuthenticatedUser; const mockSavedObject: SavedObject = { id: 'd7170a35-7e2c-48d6-8dec-9a056721b489', type: SEARCH_SESSION_TYPE, @@ -145,9 +138,6 @@ describe('SearchSessionService', () => { expect(callAttributes).toHaveProperty('urlGeneratorId', 'panama'); expect(callAttributes).toHaveProperty('initialState', {}); expect(callAttributes).toHaveProperty('restoreState', {}); - expect(callAttributes).toHaveProperty('realmType', mockUser1.authentication_realm.type); - expect(callAttributes).toHaveProperty('realmName', mockUser1.authentication_realm.name); - expect(callAttributes).toHaveProperty('username', mockUser1.username); }); it('saving creates a new persisted saved object, if it did not exist', async () => { @@ -183,6 +173,9 @@ describe('SearchSessionService', () => { expect(callAttributes).toHaveProperty('urlGeneratorId', 'panama'); expect(callAttributes).toHaveProperty('initialState', {}); expect(callAttributes).toHaveProperty('restoreState', {}); + expect(callAttributes).toHaveProperty('realmType', mockUser1.authentication_realm.type); + expect(callAttributes).toHaveProperty('realmName', mockUser1.authentication_realm.name); + expect(callAttributes).toHaveProperty('username', mockUser1.username); }); it('works without security', async () => { @@ -220,14 +213,6 @@ describe('SearchSessionService', () => { expect(savedObjectsClient.get).toHaveBeenCalledWith(SEARCH_SESSION_TYPE, sessionId); }); - it('throws error if user conflicts', () => { - savedObjectsClient.get.mockResolvedValue(mockSavedObject); - - expect( - service.get({ savedObjectsClient }, mockUser2, sessionId) - ).rejects.toMatchInlineSnapshot(`[Error: Not Found]`); - }); - it('works without security', async () => { savedObjectsClient.get.mockResolvedValue(mockSavedObject); @@ -259,7 +244,66 @@ describe('SearchSessionService', () => { const [[findOptions]] = savedObjectsClient.find.mock.calls; expect(findOptions).toMatchInlineSnapshot(` Object { - "filter": "search-session.attributes.realmType: my_realm_type and search-session.attributes.realmName: my_realm_name and search-session.attributes.username: my_username", + "filter": Object { + "arguments": Array [ + Object { + "arguments": Array [ + Object { + "type": "literal", + "value": "search-session.attributes.realmType", + }, + Object { + "type": "literal", + "value": "my_realm_type", + }, + Object { + "type": "literal", + "value": false, + }, + ], + "function": "is", + "type": "function", + }, + Object { + "arguments": Array [ + Object { + "type": "literal", + "value": "search-session.attributes.realmName", + }, + Object { + "type": "literal", + "value": "my_realm_name", + }, + Object { + "type": "literal", + "value": false, + }, + ], + "function": "is", + "type": "function", + }, + Object { + "arguments": Array [ + Object { + "type": "literal", + "value": "search-session.attributes.username", + }, + Object { + "type": "literal", + "value": "my_username", + }, + Object { + "type": "literal", + "value": false, + }, + ], + "function": "is", + "type": "function", + }, + ], + "function": "and", + "type": "function", + }, "page": 0, "perPage": 5, "type": "search-session", @@ -287,7 +331,7 @@ describe('SearchSessionService', () => { const [[findOptions]] = savedObjectsClient.find.mock.calls; expect(findOptions).toMatchInlineSnapshot(` Object { - "filter": "", + "filter": undefined, "page": 0, "perPage": 5, "type": "search-session", @@ -323,20 +367,6 @@ describe('SearchSessionService', () => { expect(callAttributes).toHaveProperty('touched'); }); - it('throws if user conflicts', () => { - const mockUpdateSavedObject = { - ...mockSavedObject, - attributes: {}, - }; - savedObjectsClient.get.mockResolvedValue(mockSavedObject); - savedObjectsClient.update.mockResolvedValue(mockUpdateSavedObject); - - const attributes = { name: 'new_name' }; - expect( - service.update({ savedObjectsClient }, mockUser2, sessionId, attributes) - ).rejects.toMatchInlineSnapshot(`[Error: Not Found]`); - }); - it('works without security', async () => { const mockUpdateSavedObject = { ...mockSavedObject, @@ -370,14 +400,6 @@ describe('SearchSessionService', () => { expect(callAttributes).toHaveProperty('touched'); }); - it('throws if user conflicts', () => { - savedObjectsClient.get.mockResolvedValue(mockSavedObject); - - expect( - service.cancel({ savedObjectsClient }, mockUser2, sessionId) - ).rejects.toMatchInlineSnapshot(`[Error: Not Found]`); - }); - it('works without security', async () => { savedObjectsClient.get.mockResolvedValue(mockSavedObject); @@ -515,6 +537,61 @@ describe('SearchSessionService', () => { expect(callAttributes).toHaveProperty('sessionId', sessionId); expect(callAttributes).toHaveProperty('persisted', false); }); + + it('retries updating if update returned 404 and then update returned conflict 409 (first create race condition)', async () => { + const searchRequest = { params: {} }; + const searchId = 'FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0'; + + const mockUpdateSavedObject = { + ...mockSavedObject, + attributes: {}, + }; + + let counter = 0; + + savedObjectsClient.update.mockImplementation(() => { + return new Promise((resolve, reject) => { + if (counter === 0) { + counter++; + reject(SavedObjectsErrorHelpers.createGenericNotFoundError(sessionId)); + } else { + resolve(mockUpdateSavedObject); + } + }); + }); + + savedObjectsClient.create.mockRejectedValue( + SavedObjectsErrorHelpers.createConflictError(SEARCH_SESSION_TYPE, searchId) + ); + + await service.trackId({ savedObjectsClient }, mockUser1, searchRequest, searchId, { + sessionId, + strategy: MOCK_STRATEGY, + }); + + expect(savedObjectsClient.update).toHaveBeenCalledTimes(2); + expect(savedObjectsClient.create).toHaveBeenCalledTimes(1); + }); + + it('retries everything at most MAX_RETRIES times', async () => { + const searchRequest = { params: {} }; + const searchId = 'FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0'; + + savedObjectsClient.update.mockRejectedValue( + SavedObjectsErrorHelpers.createGenericNotFoundError(sessionId) + ); + savedObjectsClient.create.mockRejectedValue( + SavedObjectsErrorHelpers.createConflictError(SEARCH_SESSION_TYPE, searchId) + ); + + await service.trackId({ savedObjectsClient }, mockUser1, searchRequest, searchId, { + sessionId, + strategy: MOCK_STRATEGY, + }); + + expect(savedObjectsClient.update).toHaveBeenCalledTimes(MAX_UPDATE_RETRIES); + expect(savedObjectsClient.create).toHaveBeenCalledTimes(MAX_UPDATE_RETRIES); + }); }); describe('getId', () => { diff --git a/x-pack/plugins/data_enhanced/server/search/session/session_service.ts b/x-pack/plugins/data_enhanced/server/search/session/session_service.ts index 336a953648e81b..48b0ac1ea092f2 100644 --- a/x-pack/plugins/data_enhanced/server/search/session/session_service.ts +++ b/x-pack/plugins/data_enhanced/server/search/session/session_service.ts @@ -5,7 +5,6 @@ * 2.0. */ -import { notFound } from '@hapi/boom'; import { CoreSetup, CoreStart, @@ -16,7 +15,11 @@ import { SavedObjectsFindOptions, SavedObjectsErrorHelpers, } from '../../../../../../src/core/server'; -import { IKibanaSearchRequest, ISearchOptions } from '../../../../../../src/plugins/data/common'; +import { + IKibanaSearchRequest, + ISearchOptions, + nodeBuilder, +} from '../../../../../../src/plugins/data/common'; import { ISearchSessionService } from '../../../../../../src/plugins/data/server'; import { AuthenticatedUser, SecurityPluginSetup } from '../../../../security/server'; import { @@ -193,18 +196,16 @@ export class SearchSessionService ); }; - public get = async ( + public get = ( { savedObjectsClient }: SearchSessionDependencies, user: AuthenticatedUser | null, sessionId: string ) => { this.logger.debug(`get | ${sessionId}`); - const session = await savedObjectsClient.get( + return savedObjectsClient.get( SEARCH_SESSION_TYPE, sessionId ); - this.throwOnUserConflict(user, session); - return session; }; public find = ( @@ -216,11 +217,17 @@ export class SearchSessionService user === null ? [] : [ - `${SEARCH_SESSION_TYPE}.attributes.realmType: ${user.authentication_realm.type}`, - `${SEARCH_SESSION_TYPE}.attributes.realmName: ${user.authentication_realm.name}`, - `${SEARCH_SESSION_TYPE}.attributes.username: ${user.username}`, + nodeBuilder.is( + `${SEARCH_SESSION_TYPE}.attributes.realmType`, + `${user.authentication_realm.type}` + ), + nodeBuilder.is( + `${SEARCH_SESSION_TYPE}.attributes.realmName`, + `${user.authentication_realm.name}` + ), + nodeBuilder.is(`${SEARCH_SESSION_TYPE}.attributes.username`, `${user.username}`), ]; - const filter = userFilters.concat(options.filter ?? []).join(' and '); + const filter = nodeBuilder.and(userFilters.concat(options.filter ?? [])); return savedObjectsClient.find({ ...options, filter, @@ -228,16 +235,13 @@ export class SearchSessionService }); }; - // TODO: Throw an error if this session doesn't belong to this user - public update = async ( + public update = ( deps: SearchSessionDependencies, user: AuthenticatedUser | null, sessionId: string, attributes: Partial ) => { this.logger.debug(`update | ${sessionId}`); - const session = await this.get(deps, user, sessionId); - this.throwOnUserConflict(user, session); return deps.savedObjectsClient.update( SEARCH_SESSION_TYPE, sessionId, @@ -259,7 +263,6 @@ export class SearchSessionService return this.update(deps, user, sessionId, { expires: expires.toISOString() }); } - // TODO: Throw an error if this session doesn't belong to this user public cancel = ( deps: SearchSessionDependencies, user: AuthenticatedUser | null, @@ -271,14 +274,11 @@ export class SearchSessionService }); }; - // TODO: Throw an error if this session doesn't belong to this user - public delete = async ( + public delete = ( deps: SearchSessionDependencies, user: AuthenticatedUser | null, sessionId: string ) => { - const session = await this.get(deps, user, sessionId); - this.throwOnUserConflict(user, session); return deps.savedObjectsClient.delete(SEARCH_SESSION_TYPE, sessionId); }; @@ -375,18 +375,4 @@ export class SearchSessionService }; }; }; - - private throwOnUserConflict = ( - user: AuthenticatedUser | null, - session?: SavedObject - ) => { - if (user === null || !session) return; - if ( - user.authentication_realm.type !== session.attributes.realmType || - user.authentication_realm.name !== session.attributes.realmName || - user.username !== session.attributes.username - ) { - throw notFound(); - } - }; } From 33d2784711bd34b8e481bce12f32961c11899967 Mon Sep 17 00:00:00 2001 From: Lukas Olson Date: Wed, 10 Feb 2021 16:41:24 -0700 Subject: [PATCH 11/14] Allow filter as string or KQL node --- .../search/session/session_service.test.ts | 192 ++++++++++++++++++ .../server/search/session/session_service.ts | 8 +- 2 files changed, 198 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/data_enhanced/server/search/session/session_service.test.ts b/x-pack/plugins/data_enhanced/server/search/session/session_service.test.ts index fe4924360f0d92..a32db02d7b2184 100644 --- a/x-pack/plugins/data_enhanced/server/search/session/session_service.test.ts +++ b/x-pack/plugins/data_enhanced/server/search/session/session_service.test.ts @@ -20,6 +20,7 @@ import { ConfigSchema } from '../../../config'; // @ts-ignore import { taskManagerMock } from '../../../../task_manager/server/mocks'; import { AuthenticatedUser } from '../../../../security/common/model'; +import { nodeBuilder } from '../../../../../../src/plugins/data/common'; const MAX_UPDATE_RETRIES = 3; @@ -311,6 +312,197 @@ describe('SearchSessionService', () => { `); }); + it('mixes in passed-in filter as string and KQL node', async () => { + const mockFindSavedObject = { + ...mockSavedObject, + score: 1, + }; + const mockResponse = { + saved_objects: [mockFindSavedObject], + total: 1, + per_page: 1, + page: 0, + }; + savedObjectsClient.find.mockResolvedValue(mockResponse); + + const options1 = { filter: 'foobar' }; + const response1 = await service.find({ savedObjectsClient }, mockUser1, options1); + + const options2 = { filter: nodeBuilder.is('foo', 'bar') }; + const response2 = await service.find({ savedObjectsClient }, mockUser1, options2); + + expect(response1).toBe(mockResponse); + expect(response2).toBe(mockResponse); + + const [[findOptions1], [findOptions2]] = savedObjectsClient.find.mock.calls; + expect(findOptions1).toMatchInlineSnapshot(` + Object { + "filter": Object { + "arguments": Array [ + Object { + "arguments": Array [ + Object { + "type": "literal", + "value": "search-session.attributes.realmType", + }, + Object { + "type": "literal", + "value": "my_realm_type", + }, + Object { + "type": "literal", + "value": false, + }, + ], + "function": "is", + "type": "function", + }, + Object { + "arguments": Array [ + Object { + "type": "literal", + "value": "search-session.attributes.realmName", + }, + Object { + "type": "literal", + "value": "my_realm_name", + }, + Object { + "type": "literal", + "value": false, + }, + ], + "function": "is", + "type": "function", + }, + Object { + "arguments": Array [ + Object { + "type": "literal", + "value": "search-session.attributes.username", + }, + Object { + "type": "literal", + "value": "my_username", + }, + Object { + "type": "literal", + "value": false, + }, + ], + "function": "is", + "type": "function", + }, + Object { + "arguments": Array [ + Object { + "type": "literal", + "value": null, + }, + Object { + "type": "literal", + "value": "foobar", + }, + Object { + "type": "literal", + "value": false, + }, + ], + "function": "is", + "type": "function", + }, + ], + "function": "and", + "type": "function", + }, + "type": "search-session", + } + `); + expect(findOptions2).toMatchInlineSnapshot(` + Object { + "filter": Object { + "arguments": Array [ + Object { + "arguments": Array [ + Object { + "type": "literal", + "value": "search-session.attributes.realmType", + }, + Object { + "type": "literal", + "value": "my_realm_type", + }, + Object { + "type": "literal", + "value": false, + }, + ], + "function": "is", + "type": "function", + }, + Object { + "arguments": Array [ + Object { + "type": "literal", + "value": "search-session.attributes.realmName", + }, + Object { + "type": "literal", + "value": "my_realm_name", + }, + Object { + "type": "literal", + "value": false, + }, + ], + "function": "is", + "type": "function", + }, + Object { + "arguments": Array [ + Object { + "type": "literal", + "value": "search-session.attributes.username", + }, + Object { + "type": "literal", + "value": "my_username", + }, + Object { + "type": "literal", + "value": false, + }, + ], + "function": "is", + "type": "function", + }, + Object { + "arguments": Array [ + Object { + "type": "literal", + "value": "foo", + }, + Object { + "type": "literal", + "value": "bar", + }, + Object { + "type": "literal", + "value": false, + }, + ], + "function": "is", + "type": "function", + }, + ], + "function": "and", + "type": "function", + }, + "type": "search-session", + } + `); + }); + it('has no filter without security', async () => { const mockFindSavedObject = { ...mockSavedObject, diff --git a/x-pack/plugins/data_enhanced/server/search/session/session_service.ts b/x-pack/plugins/data_enhanced/server/search/session/session_service.ts index 48b0ac1ea092f2..47cc2d42fc8197 100644 --- a/x-pack/plugins/data_enhanced/server/search/session/session_service.ts +++ b/x-pack/plugins/data_enhanced/server/search/session/session_service.ts @@ -20,7 +20,7 @@ import { ISearchOptions, nodeBuilder, } from '../../../../../../src/plugins/data/common'; -import { ISearchSessionService } from '../../../../../../src/plugins/data/server'; +import { esKuery, ISearchSessionService } from '../../../../../../src/plugins/data/server'; import { AuthenticatedUser, SecurityPluginSetup } from '../../../../security/server'; import { TaskManagerSetupContract, @@ -227,7 +227,11 @@ export class SearchSessionService ), nodeBuilder.is(`${SEARCH_SESSION_TYPE}.attributes.username`, `${user.username}`), ]; - const filter = nodeBuilder.and(userFilters.concat(options.filter ?? [])); + const filterKueryNode = + typeof options.filter === 'string' + ? esKuery.fromKueryExpression(options.filter) + : options.filter; + const filter = nodeBuilder.and(userFilters.concat(filterKueryNode ?? [])); return savedObjectsClient.find({ ...options, filter, From 993216a9ac9c973cec92dd903f4c7cea9cc21d12 Mon Sep 17 00:00:00 2001 From: Lukas Olson Date: Thu, 11 Feb 2021 10:00:22 -0700 Subject: [PATCH 12/14] Add back user checks --- .../search/session/session_service.test.ts | 37 ++++++++++++++++++ .../server/search/session/session_service.ts | 38 +++++++++++++++---- 2 files changed, 68 insertions(+), 7 deletions(-) diff --git a/x-pack/plugins/data_enhanced/server/search/session/session_service.test.ts b/x-pack/plugins/data_enhanced/server/search/session/session_service.test.ts index a32db02d7b2184..5a4d40c848a291 100644 --- a/x-pack/plugins/data_enhanced/server/search/session/session_service.test.ts +++ b/x-pack/plugins/data_enhanced/server/search/session/session_service.test.ts @@ -40,6 +40,13 @@ describe('SearchSessionService', () => { name: 'my_realm_name', }, } as AuthenticatedUser; + const mockUser2 = { + username: 'bar', + authentication_realm: { + type: 'bar', + name: 'bar', + }, + } as AuthenticatedUser; const mockSavedObject: SavedObject = { id: 'd7170a35-7e2c-48d6-8dec-9a056721b489', type: SEARCH_SESSION_TYPE, @@ -179,6 +186,14 @@ describe('SearchSessionService', () => { expect(callAttributes).toHaveProperty('username', mockUser1.username); }); + it('throws error if user conflicts', () => { + savedObjectsClient.get.mockResolvedValue(mockSavedObject); + + expect( + service.get({ savedObjectsClient }, mockUser2, sessionId) + ).rejects.toMatchInlineSnapshot(`[Error: Not Found]`); + }); + it('works without security', async () => { savedObjectsClient.update.mockRejectedValue( SavedObjectsErrorHelpers.createGenericNotFoundError(sessionId) @@ -559,6 +574,20 @@ describe('SearchSessionService', () => { expect(callAttributes).toHaveProperty('touched'); }); + it('throws if user conflicts', () => { + const mockUpdateSavedObject = { + ...mockSavedObject, + attributes: {}, + }; + savedObjectsClient.get.mockResolvedValue(mockSavedObject); + savedObjectsClient.update.mockResolvedValue(mockUpdateSavedObject); + + const attributes = { name: 'new_name' }; + expect( + service.update({ savedObjectsClient }, mockUser2, sessionId, attributes) + ).rejects.toMatchInlineSnapshot(`[Error: Not Found]`); + }); + it('works without security', async () => { const mockUpdateSavedObject = { ...mockSavedObject, @@ -592,6 +621,14 @@ describe('SearchSessionService', () => { expect(callAttributes).toHaveProperty('touched'); }); + it('throws if user conflicts', () => { + savedObjectsClient.get.mockResolvedValue(mockSavedObject); + + expect( + service.cancel({ savedObjectsClient }, mockUser2, sessionId) + ).rejects.toMatchInlineSnapshot(`[Error: Not Found]`); + }); + it('works without security', async () => { savedObjectsClient.get.mockResolvedValue(mockSavedObject); diff --git a/x-pack/plugins/data_enhanced/server/search/session/session_service.ts b/x-pack/plugins/data_enhanced/server/search/session/session_service.ts index 47cc2d42fc8197..cca5414c75cc03 100644 --- a/x-pack/plugins/data_enhanced/server/search/session/session_service.ts +++ b/x-pack/plugins/data_enhanced/server/search/session/session_service.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { notFound } from '@hapi/boom'; import { CoreSetup, CoreStart, @@ -196,16 +197,18 @@ export class SearchSessionService ); }; - public get = ( + public get = async ( { savedObjectsClient }: SearchSessionDependencies, user: AuthenticatedUser | null, sessionId: string ) => { this.logger.debug(`get | ${sessionId}`); - return savedObjectsClient.get( + const session = await savedObjectsClient.get( SEARCH_SESSION_TYPE, sessionId ); + this.throwOnUserConflict(user, session); + return session; }; public find = ( @@ -239,13 +242,14 @@ export class SearchSessionService }); }; - public update = ( + public update = async ( deps: SearchSessionDependencies, user: AuthenticatedUser | null, sessionId: string, attributes: Partial ) => { this.logger.debug(`update | ${sessionId}`); + await this.get(deps, user, sessionId); // Verify correct user return deps.savedObjectsClient.update( SEARCH_SESSION_TYPE, sessionId, @@ -256,33 +260,36 @@ export class SearchSessionService ); }; - public extend( + public async extend( deps: SearchSessionDependencies, user: AuthenticatedUser | null, sessionId: string, expires: Date ) { this.logger.debug(`extend | ${sessionId}`); - + await this.get(deps, user, sessionId); // Verify correct user return this.update(deps, user, sessionId, { expires: expires.toISOString() }); } - public cancel = ( + public cancel = async ( deps: SearchSessionDependencies, user: AuthenticatedUser | null, sessionId: string ) => { this.logger.debug(`delete | ${sessionId}`); + await this.get(deps, user, sessionId); // Verify correct user return this.update(deps, user, sessionId, { status: SearchSessionStatus.CANCELLED, }); }; - public delete = ( + public delete = async ( deps: SearchSessionDependencies, user: AuthenticatedUser | null, sessionId: string ) => { + this.logger.debug(`delete | ${sessionId}`); + await this.get(deps, user, sessionId); // Verify correct user return deps.savedObjectsClient.delete(SEARCH_SESSION_TYPE, sessionId); }; @@ -379,4 +386,21 @@ export class SearchSessionService }; }; }; + + private throwOnUserConflict = ( + user: AuthenticatedUser | null, + session?: SavedObject + ) => { + if (user === null || !session) return; + if ( + user.authentication_realm.type !== session.attributes.realmType || + user.authentication_realm.name !== session.attributes.realmName || + user.username !== session.attributes.username + ) { + this.logger.debug( + `User ${user.username} has no access to search session ${session.attributes.sessionId}` + ); + throw notFound(); + } + }; } From 0320c6ecd1d0f6d6ffaef1f76f42dab69c387765 Mon Sep 17 00:00:00 2001 From: Lukas Olson Date: Thu, 11 Feb 2021 13:43:00 -0700 Subject: [PATCH 13/14] Add API integration tests --- .../api_integration/apis/search/session.ts | 118 ++++++++++++++++++ 1 file changed, 118 insertions(+) diff --git a/x-pack/test/api_integration/apis/search/session.ts b/x-pack/test/api_integration/apis/search/session.ts index 984f3e3f7dd4e6..2e3de6bb7f2a68 100644 --- a/x-pack/test/api_integration/apis/search/session.ts +++ b/x-pack/test/api_integration/apis/search/session.ts @@ -11,6 +11,8 @@ import { SearchSessionStatus } from '../../../../plugins/data_enhanced/common'; export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); + const security = getService('security'); describe('search session', () => { describe('session management', () => { @@ -315,5 +317,121 @@ export default function ({ getService }: FtrProviderContext) { getSessionSecondTime.body.attributes.touched ); }); + + describe('with security', () => { + before(async () => { + await security.user.create('other_user', { + password: 'password', + roles: ['superuser'], + full_name: 'other user', + }); + }); + + after(async () => { + await security.user.delete('other_user'); + }); + + it(`should prevent users from accessing other users' sessions`, async () => { + const sessionId = `my-session-${Math.random()}`; + await supertest + .post(`/internal/session`) + .set('kbn-xsrf', 'foo') + .send({ + sessionId, + name: 'My Session', + appId: 'discover', + expires: '123', + urlGeneratorId: 'discover', + }) + .expect(200); + + await supertestWithoutAuth + .get(`/internal/session/${sessionId}`) + .set('kbn-xsrf', 'foo') + .auth('other_user', 'password') + .expect(404); + }); + + it(`should prevent users from deleting other users' sessions`, async () => { + const sessionId = `my-session-${Math.random()}`; + await supertest + .post(`/internal/session`) + .set('kbn-xsrf', 'foo') + .send({ + sessionId, + name: 'My Session', + appId: 'discover', + expires: '123', + urlGeneratorId: 'discover', + }) + .expect(200); + + await supertestWithoutAuth + .delete(`/internal/session/${sessionId}`) + .set('kbn-xsrf', 'foo') + .auth('other_user', 'password') + .expect(404); + }); + + it(`should prevent users from cancelling other users' sessions`, async () => { + const sessionId = `my-session-${Math.random()}`; + await supertest + .post(`/internal/session`) + .set('kbn-xsrf', 'foo') + .send({ + sessionId, + name: 'My Session', + appId: 'discover', + expires: '123', + urlGeneratorId: 'discover', + }) + .expect(200); + + await supertestWithoutAuth + .post(`/internal/session/${sessionId}/cancel`) + .set('kbn-xsrf', 'foo') + .auth('other_user', 'password') + .expect(404); + }); + + it(`should prevent users from extending other users' sessions`, async () => { + const sessionId = `my-session-${Math.random()}`; + await supertest + .post(`/internal/session`) + .set('kbn-xsrf', 'foo') + .send({ + sessionId, + name: 'My Session', + appId: 'discover', + expires: '123', + urlGeneratorId: 'discover', + }) + .expect(200); + + await supertestWithoutAuth + .post(`/internal/session/${sessionId}/_extend`) + .set('kbn-xsrf', 'foo') + .auth('other_user', 'password') + .send({ + expires: '2021-02-26T21:02:43.742Z', + }) + .expect(404); + }); + + it(`should prevent unauthorized users from creating sessions`, async () => { + const sessionId = `my-session-${Math.random()}`; + await supertestWithoutAuth + .post(`/internal/session`) + .set('kbn-xsrf', 'foo') + .send({ + sessionId, + name: 'My Session', + appId: 'discover', + expires: '123', + urlGeneratorId: 'discover', + }) + .expect(401); + }); + }); }); } From faaeff6f9a42529e703a2e8a346e8a532b5ea19a Mon Sep 17 00:00:00 2001 From: Lukas Olson Date: Thu, 11 Feb 2021 14:26:25 -0700 Subject: [PATCH 14/14] Remove unnecessary get calls --- .../data_enhanced/server/search/session/session_service.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/x-pack/plugins/data_enhanced/server/search/session/session_service.ts b/x-pack/plugins/data_enhanced/server/search/session/session_service.ts index ef6b6115980cc4..c95c58a8dc06ba 100644 --- a/x-pack/plugins/data_enhanced/server/search/session/session_service.ts +++ b/x-pack/plugins/data_enhanced/server/search/session/session_service.ts @@ -334,7 +334,6 @@ export class SearchSessionService expires: Date ) { this.logger.debug(`extend | ${sessionId}`); - await this.get(deps, user, sessionId); // Verify correct user return this.update(deps, user, sessionId, { expires: expires.toISOString() }); } @@ -344,7 +343,6 @@ export class SearchSessionService sessionId: string ) => { this.logger.debug(`delete | ${sessionId}`); - await this.get(deps, user, sessionId); // Verify correct user return this.update(deps, user, sessionId, { status: SearchSessionStatus.CANCELLED, });