Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Search Session] Control "Kibana / Search Sessions" management section by privileges #90818

Merged
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -2238,6 +2238,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 @@ -2601,23 +2606,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
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),
};
}
36 changes: 35 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 @@ -214,4 +227,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"`
);
});
});
});
28 changes: 27 additions & 1 deletion 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,21 @@ 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'>> {
): Required<Pick<ISearchOptions, 'sessionId' | 'isRestore' | 'isStored'>> | 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;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Noticed that after this pr #89570 we track all searches in es and trackId would through in case a user doesn't have access to search-session SO.
To avoid this ensure we don't send sessionId to the server in case a user doesn't have access to work with sessions

}

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

try {
const searchSessionOptions =
searchSessionId && dataSearch.session.getSearchOptions(searchSessionId);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getSearchOptions can return null now. timelion route validation fails in case we send
searchSession: null, so this change just makes sure searchSession key is not send in case getSearchOptions returns null

return await http.post('/api/timelion/run', {
body: JSON.stringify({
sheet: [expression],
Expand All @@ -108,8 +110,8 @@ export function getTimelionRequestHandler({
interval: visParams.interval,
timezone,
},
...(searchSessionId && {
searchSession: dataSearch.session.getSearchOptions(searchSessionId),
...(searchSessionOptions && {
searchSession: searchSessionOptions,
}),
}),
});
Expand Down
6 changes: 4 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,8 @@ export const metricsRequestHandler = async ({
});

try {
const searchSessionOptions =
searchSessionId && dataSearch.session.getSearchOptions(searchSessionId);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getSearchOptions can return null now. TSVB route validation fails in case we send
searchSession: null, so this change just makes sure searchSession key is not send in case getSearchOptions returns null

return await getCoreStart().http.post(ROUTES.VIS_DATA, {
body: JSON.stringify({
timerange: {
Expand All @@ -58,8 +60,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