Skip to content

Commit

Permalink
[Search Session] Control "Kibana / Search Sessions" management sectio…
Browse files Browse the repository at this point in the history
…n by privileges (#90818) (#91096)
  • Loading branch information
Dosant authored Feb 11, 2021
1 parent 80bfebf commit 06883ad
Show file tree
Hide file tree
Showing 22 changed files with 370 additions and 71 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@
| [noSearchSessionStorageCapabilityMessage](./kibana-plugin-plugins-data-public.nosearchsessionstoragecapabilitymessage.md) | Message to display in case storing session session is disabled due to turned off capability |
| [parseSearchSourceJSON](./kibana-plugin-plugins-data-public.parsesearchsourcejson.md) | |
| [QueryStringInput](./kibana-plugin-plugins-data-public.querystringinput.md) | |
| [SEARCH\_SESSIONS\_MANAGEMENT\_ID](./kibana-plugin-plugins-data-public.search_sessions_management_id.md) | |
| [search](./kibana-plugin-plugins-data-public.search.md) | |
| [SearchBar](./kibana-plugin-plugins-data-public.searchbar.md) | |
| [syncQueryStateWithUrl](./kibana-plugin-plugins-data-public.syncquerystatewithurl.md) | Helper to setup syncing of global data with the URL |
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->

[Home](./index.md) &gt; [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) &gt; [SEARCH\_SESSIONS\_MANAGEMENT\_ID](./kibana-plugin-plugins-data-public.search_sessions_management_id.md)

## SEARCH\_SESSIONS\_MANAGEMENT\_ID variable

<b>Signature:</b>

```typescript
SEARCH_SESSIONS_MANAGEMENT_ID = "search_sessions"
```
1 change: 1 addition & 0 deletions src/plugins/data/public/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,7 @@ export {
TimeoutErrorMode,
PainlessError,
noSearchSessionStorageCapabilityMessage,
SEARCH_SESSIONS_MANAGEMENT_ID,
} from './search';

export type {
Expand Down
37 changes: 21 additions & 16 deletions src/plugins/data/public/public.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -2246,6 +2246,11 @@ export const search: {
tabifyGetColumns: typeof tabifyGetColumns;
};

// Warning: (ae-missing-release-tag) "SEARCH_SESSIONS_MANAGEMENT_ID" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export const SEARCH_SESSIONS_MANAGEMENT_ID = "search_sessions";

// Warning: (ae-missing-release-tag) "SearchBar" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
Expand Down Expand Up @@ -2609,23 +2614,23 @@ export const UI_SETTINGS: {
// src/plugins/data/public/index.ts:234:27 - (ae-forgotten-export) The symbol "validateIndexPattern" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:234:27 - (ae-forgotten-export) The symbol "flattenHitWrapper" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:234:27 - (ae-forgotten-export) The symbol "formatHitProvider" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:396:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:396:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:396:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:396:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:398:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:399:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:408:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:409:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:410:1 - (ae-forgotten-export) The symbol "Ipv4Address" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:411:1 - (ae-forgotten-export) The symbol "isDateHistogramBucketAggConfig" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:415:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:416:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:419:1 - (ae-forgotten-export) The symbol "parseInterval" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:420:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:423:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:397:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:397:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:397:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:397:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:399:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:400:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:409:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:410:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:411:1 - (ae-forgotten-export) The symbol "Ipv4Address" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:412:1 - (ae-forgotten-export) The symbol "isDateHistogramBucketAggConfig" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:416:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:417:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:420:1 - (ae-forgotten-export) The symbol "parseInterval" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:421:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:424:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/query/state_sync/connect_to_query_state.ts:34:5 - (ae-forgotten-export) The symbol "FilterStateStore" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/search/session/session_service.ts:41:5 - (ae-forgotten-export) The symbol "UrlGeneratorStateMapping" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/search/session/session_service.ts:42:5 - (ae-forgotten-export) The symbol "UrlGeneratorStateMapping" needs to be exported by the entry point index.d.ts

// (No @packageDocumentation comment for this package)

Expand Down
1 change: 1 addition & 0 deletions src/plugins/data/public/search/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export {
SessionsClient,
ISessionsClient,
noSearchSessionStorageCapabilityMessage,
SEARCH_SESSIONS_MANAGEMENT_ID,
} from './session';
export { getEsPreference } from './es_search';

Expand Down
46 changes: 32 additions & 14 deletions src/plugins/data/public/search/search_interceptor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,21 +95,23 @@ describe('SearchInterceptor', () => {
});

describe('Search session', () => {
const setup = ({
isRestore = false,
isStored = false,
sessionId,
}: {
isRestore?: boolean;
isStored?: boolean;
sessionId: string;
}) => {
const setup = (
opts: {
isRestore?: boolean;
isStored?: boolean;
sessionId: string;
} | null
) => {
const sessionServiceMock = searchMock.session as jest.Mocked<ISessionService>;
sessionServiceMock.getSearchOptions.mockImplementation(() => ({
sessionId,
isRestore,
isStored,
}));
sessionServiceMock.getSearchOptions.mockImplementation(() =>
opts
? {
sessionId: opts.sessionId,
isRestore: opts.isRestore ?? false,
isStored: opts.isStored ?? false,
}
: null
);
fetchMock.mockResolvedValue({ result: 200 });
};

Expand Down Expand Up @@ -142,6 +144,22 @@ describe('SearchInterceptor', () => {
(searchMock.session as jest.Mocked<ISessionService>).getSearchOptions
).toHaveBeenCalledWith(sessionId);
});

test("doesn't forward sessionId if search options return null", async () => {
const sessionId = 'sid';
setup(null);

await searchInterceptor.search(mockRequest, { sessionId }).toPromise();
expect(fetchMock.mock.calls[0][0]).toEqual(
expect.not.objectContaining({
options: { sessionId },
})
);

expect(
(searchMock.session as jest.Mocked<ISessionService>).getSearchOptions
).toHaveBeenCalledWith(sessionId);
});
});

describe('Should throw typed errors', () => {
Expand Down
4 changes: 2 additions & 2 deletions src/plugins/data/public/search/search_interceptor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,14 +126,14 @@ export class SearchInterceptor {
request: IKibanaSearchRequest,
options?: ISearchOptions
): Promise<IKibanaSearchResponse> {
const { abortSignal, ...requestOptions } = options || {};
const { abortSignal, sessionId, ...requestOptions } = options || {};

return this.batchedFetch(
{
request,
options: {
...requestOptions,
...(options?.sessionId && this.deps.session.getSearchOptions(options.sessionId)),
...this.deps.session.getSearchOptions(sessionId),
},
},
abortSignal
Expand Down
9 changes: 9 additions & 0 deletions src/plugins/data/public/search/session/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

export const SEARCH_SESSIONS_MANAGEMENT_ID = 'search_sessions';
1 change: 1 addition & 0 deletions src/plugins/data/public/search/session/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ export { SessionService, ISessionService, SearchSessionInfoProvider } from './se
export { SearchSessionState } from './search_session_state';
export { SessionsClient, ISessionsClient } from './sessions_client';
export { noSearchSessionStorageCapabilityMessage } from './i18n';
export { SEARCH_SESSIONS_MANAGEMENT_ID } from './constants';
1 change: 1 addition & 0 deletions src/plugins/data/public/search/session/mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,5 +41,6 @@ export function getSessionServiceMock(): jest.Mocked<ISessionService> {
enableStorage: jest.fn(),
isSessionStorageReady: jest.fn(() => true),
getSearchSessionIndicatorUiConfig: jest.fn(() => ({ isDisabled: () => ({ disabled: false }) })),
hasAccess: jest.fn(() => true),
};
}
38 changes: 37 additions & 1 deletion src/plugins/data/public/search/session/session_service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,13 @@ import { BehaviorSubject } from 'rxjs';
import { SearchSessionState } from './search_session_state';
import { createNowProviderMock } from '../../now_provider/mocks';
import { NowProviderInternalContract } from '../../now_provider';
import { SEARCH_SESSIONS_MANAGEMENT_ID } from './constants';

describe('Session service', () => {
let sessionService: ISessionService;
let state$: BehaviorSubject<SearchSessionState>;
let nowProvider: jest.Mocked<NowProviderInternalContract>;
let userHasAccessToSearchSessions = true;

beforeEach(() => {
const initializerContext = coreMock.createPluginInitializerContext();
Expand All @@ -30,7 +32,18 @@ describe('Session service', () => {
startService().then(([coreStart, ...rest]) => [
{
...coreStart,
application: { ...coreStart.application, currentAppId$: new BehaviorSubject('app') },
application: {
...coreStart.application,
currentAppId$: new BehaviorSubject('app'),
capabilities: {
...coreStart.application.capabilities,
management: {
kibana: {
[SEARCH_SESSIONS_MANAGEMENT_ID]: userHasAccessToSearchSessions,
},
},
},
},
},
...rest,
]),
Expand Down Expand Up @@ -146,6 +159,8 @@ describe('Session service', () => {
isRestore: true,
sessionId,
});

expect(sessionService.getSearchOptions(undefined)).toBeNull();
});
test('isCurrentSession', () => {
expect(sessionService.isCurrentSession()).toBeFalsy();
Expand Down Expand Up @@ -214,4 +229,25 @@ describe('Session service', () => {
sessionService.start();
await expect(() => sessionService.save()).rejects.toMatchInlineSnapshot(`[Error: Haha]`);
});

describe("user doesn't have access to search session", () => {
beforeAll(() => {
userHasAccessToSearchSessions = false;
});
afterAll(() => {
userHasAccessToSearchSessions = true;
});

test("getSearchOptions doesn't return sessionId", () => {
const sessionId = sessionService.start();
expect(sessionService.getSearchOptions(sessionId)).toBeNull();
});

test('save() throws', async () => {
sessionService.start();
await expect(() => sessionService.save()).rejects.toThrowErrorMatchingInlineSnapshot(
`"No access to search sessions"`
);
});
});
});
34 changes: 32 additions & 2 deletions src/plugins/data/public/search/session/session_service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
import { ISessionsClient } from './sessions_client';
import { ISearchOptions } from '../../../common';
import { NowProviderInternalContract } from '../../now_provider';
import { SEARCH_SESSIONS_MANAGEMENT_ID } from './constants';

export type ISessionService = PublicContract<SessionService>;

Expand Down Expand Up @@ -68,6 +69,7 @@ export class SessionService {
private searchSessionIndicatorUiConfig?: Partial<SearchSessionIndicatorUiConfig>;
private subscription = new Subscription();
private curApp?: string;
private hasAccessToSearchSessions: boolean = false;

constructor(
initializerContext: PluginInitializerContext<ConfigSchema>,
Expand All @@ -94,6 +96,10 @@ export class SessionService {
);

getStartServices().then(([coreStart]) => {
// using management?.kibana? we infer if any of the apps allows current user to store sessions
this.hasAccessToSearchSessions =
coreStart.application.capabilities.management?.kibana?.[SEARCH_SESSIONS_MANAGEMENT_ID];

// Apps required to clean up their sessions before unmounting
// Make sure that apps don't leave sessions open.
this.subscription.add(
Expand All @@ -117,6 +123,15 @@ export class SessionService {
});
}

/**
* If user has access to search sessions
* This resolves to `true` in case at least one app allows user to create search session
* In this case search session management is available
*/
public hasAccess() {
return this.hasAccessToSearchSessions;
}

/**
* Used to track pending searches within current session
*
Expand Down Expand Up @@ -215,6 +230,7 @@ export class SessionService {
const sessionId = this.getSessionId();
if (!sessionId) throw new Error('No current session');
if (!this.curApp) throw new Error('No current app id');
if (!this.hasAccess()) throw new Error('No access to search sessions');
const currentSessionInfoProvider = this.searchSessionInfoProvider;
if (!currentSessionInfoProvider) throw new Error('No info provider for current session');
const [name, { initialState, restoreState, urlGeneratorId }] = await Promise.all([
Expand Down Expand Up @@ -247,11 +263,25 @@ export class SessionService {

/**
* Infers search session options for sessionId using current session state
*
* In case user doesn't has access to `search-session` SO returns null,
* meaning that sessionId and other session parameters shouldn't be used when doing searches
*
* @param sessionId
*/
public getSearchOptions(
sessionId: string
): Required<Pick<ISearchOptions, 'sessionId' | 'isRestore' | 'isStored'>> {
sessionId?: string
): Required<Pick<ISearchOptions, 'sessionId' | 'isRestore' | 'isStored'>> | null {
if (!sessionId) {
return null;
}

// in case user doesn't have permissions to search session, do not forward sessionId to the server
// because user most likely also doesn't have access to `search-session` SO
if (!this.hasAccessToSearchSessions) {
return null;
}

const isCurrentSession = this.isCurrentSession(sessionId);
return {
sessionId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ export function getTimelionRequestHandler({
});

try {
const searchSessionOptions = dataSearch.session.getSearchOptions(searchSessionId);
return await http.post('/api/timelion/run', {
body: JSON.stringify({
sheet: [expression],
Expand All @@ -108,8 +109,8 @@ export function getTimelionRequestHandler({
interval: visParams.interval,
timezone,
},
...(searchSessionId && {
searchSession: dataSearch.session.getSearchOptions(searchSessionId),
...(searchSessionOptions && {
searchSession: searchSessionOptions,
}),
}),
});
Expand Down
5 changes: 3 additions & 2 deletions src/plugins/vis_type_timeseries/public/request_handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export const metricsRequestHandler = async ({
});

try {
const searchSessionOptions = dataSearch.session.getSearchOptions(searchSessionId);
return await getCoreStart().http.post(ROUTES.VIS_DATA, {
body: JSON.stringify({
timerange: {
Expand All @@ -58,8 +59,8 @@ export const metricsRequestHandler = async ({
filters: input?.filters,
panels: [visParams],
state: uiStateObj,
...(searchSessionId && {
searchSession: dataSearch.session.getSearchOptions(searchSessionId),
...(searchSessionOptions && {
searchSession: searchSessionOptions,
}),
}),
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import type { ConfigSchema } from '../../../config';
import type { DataEnhancedStartDependencies } from '../../plugin';
import type { SearchSessionsMgmtAPI } from './lib/api';
import type { AsyncSearchIntroDocumentation } from './lib/documentation';
import { SEARCH_SESSIONS_MANAGEMENT_ID } from '../../../../../../src/plugins/data/public';

export interface IManagementSectionsPluginsSetup {
management: ManagementSetup;
Expand All @@ -38,7 +39,7 @@ export interface AppDependencies {
}

export const APP = {
id: 'search_sessions',
id: SEARCH_SESSIONS_MANAGEMENT_ID,
getI18nName: (): string =>
i18n.translate('xpack.data.mgmt.searchSessions.appTitle', {
defaultMessage: 'Search Sessions',
Expand Down
Loading

0 comments on commit 06883ad

Please sign in to comment.