mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[data.search] Add user information to background session service (#84975)
* [data.search] Move search method inside session service and add tests * Move background session service to data_enhanced plugin * Fix types * [data.search] Add user information to background session service * Update trackId & getId to accept user * Fix remaining merge conflicts * Fix test * Remove todos * Fix session service to use user * Remove user conflicts and update SO filter * Allow filter as string or KQL node * Add back user checks * Add API integration tests * Remove unnecessary get calls
This commit is contained in:
parent
874fadf388
commit
104eacb59a
8 changed files with 839 additions and 197 deletions
|
@ -57,6 +57,12 @@ export interface SearchSessionSavedObjectAttributes {
|
|||
* This value is true if the session was actively stored by the user. If it is false, the session may be purged by the system.
|
||||
*/
|
||||
persisted: boolean;
|
||||
/**
|
||||
* The realm type/name & username uniquely identifies the user who created this search session
|
||||
*/
|
||||
realmType?: string;
|
||||
realmName?: string;
|
||||
username?: string;
|
||||
}
|
||||
|
||||
export interface SearchSessionRequestInfo {
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
"kibanaVersion": "kibana",
|
||||
"configPath": ["xpack", "data_enhanced"],
|
||||
"requiredPlugins": ["bfetch", "data", "features", "management", "share", "taskManager"],
|
||||
"optionalPlugins": ["kibanaUtils", "usageCollection"],
|
||||
"optionalPlugins": ["kibanaUtils", "usageCollection", "security"],
|
||||
"server": true,
|
||||
"ui": true,
|
||||
"requiredBundles": ["kibanaUtils", "kibanaReact"]
|
||||
|
|
|
@ -24,12 +24,15 @@ import {
|
|||
import { getUiSettings } from './ui_settings';
|
||||
import type { DataEnhancedRequestHandlerContext } from './type';
|
||||
import { ConfigSchema } from '../config';
|
||||
import { SecurityPluginSetup } from '../../security/server';
|
||||
|
||||
interface SetupDependencies {
|
||||
data: DataPluginSetup;
|
||||
usageCollection?: UsageCollectionSetup;
|
||||
taskManager: TaskManagerSetupContract;
|
||||
security?: SecurityPluginSetup;
|
||||
}
|
||||
|
||||
export interface StartDependencies {
|
||||
data: DataPluginStart;
|
||||
taskManager: TaskManagerStartContract;
|
||||
|
@ -67,7 +70,7 @@ export class EnhancedDataServerPlugin
|
|||
eqlSearchStrategyProvider(this.logger)
|
||||
);
|
||||
|
||||
this.sessionService = new SearchSessionService(this.logger, this.config);
|
||||
this.sessionService = new SearchSessionService(this.logger, this.config, deps.security);
|
||||
|
||||
deps.data.__enhance({
|
||||
search: {
|
||||
|
|
|
@ -53,6 +53,15 @@ export const searchSessionMapping: SavedObjectsType = {
|
|||
type: 'object',
|
||||
enabled: false,
|
||||
},
|
||||
realmType: {
|
||||
type: 'keyword',
|
||||
},
|
||||
realmName: {
|
||||
type: 'keyword',
|
||||
},
|
||||
username: {
|
||||
type: 'keyword',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -19,6 +19,8 @@ import { coreMock } from 'src/core/server/mocks';
|
|||
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;
|
||||
|
||||
|
@ -31,7 +33,21 @@ describe('SearchSessionService', () => {
|
|||
const MOCK_STRATEGY = 'ese';
|
||||
|
||||
const sessionId = 'd7170a35-7e2c-48d6-8dec-9a056721b489';
|
||||
const mockSavedObject: SavedObject = {
|
||||
const mockUser1 = {
|
||||
username: 'my_username',
|
||||
authentication_realm: {
|
||||
type: 'my_realm_type',
|
||||
name: 'my_realm_name',
|
||||
},
|
||||
} as AuthenticatedUser;
|
||||
const mockUser2 = {
|
||||
username: 'bar',
|
||||
authentication_realm: {
|
||||
type: 'bar',
|
||||
name: 'bar',
|
||||
},
|
||||
} as AuthenticatedUser;
|
||||
const mockSavedObject: SavedObject<any> = {
|
||||
id: 'd7170a35-7e2c-48d6-8dec-9a056721b489',
|
||||
type: SEARCH_SESSION_TYPE,
|
||||
attributes: {
|
||||
|
@ -39,6 +55,9 @@ describe('SearchSessionService', () => {
|
|||
appId: 'my_app_id',
|
||||
urlGeneratorId: 'my_url_generator_id',
|
||||
idMapping: {},
|
||||
realmType: mockUser1.authentication_realm.type,
|
||||
realmName: mockUser1.authentication_realm.name,
|
||||
username: mockUser1.username,
|
||||
},
|
||||
references: [],
|
||||
};
|
||||
|
@ -77,66 +96,551 @@ describe('SearchSessionService', () => {
|
|||
service.stop();
|
||||
});
|
||||
|
||||
it('get calls saved objects client', async () => {
|
||||
savedObjectsClient.get.mockResolvedValue(mockSavedObject);
|
||||
describe('save', () => {
|
||||
it('throws if `name` is not provided', () => {
|
||||
expect(() =>
|
||||
service.save({ savedObjectsClient }, mockUser1, sessionId, {})
|
||||
).rejects.toMatchInlineSnapshot(`[Error: Name is required]`);
|
||||
});
|
||||
|
||||
const response = await service.get({ savedObjectsClient }, sessionId);
|
||||
it('throws if `appId` is not provided', () => {
|
||||
expect(
|
||||
service.save({ savedObjectsClient }, mockUser1, sessionId, { name: 'banana' })
|
||||
).rejects.toMatchInlineSnapshot(`[Error: AppId is required]`);
|
||||
});
|
||||
|
||||
expect(response).toBe(mockSavedObject);
|
||||
expect(savedObjectsClient.get).toHaveBeenCalledWith(SEARCH_SESSION_TYPE, sessionId);
|
||||
});
|
||||
it('throws if `generator id` is not provided', () => {
|
||||
expect(
|
||||
service.save({ savedObjectsClient }, mockUser1, sessionId, {
|
||||
name: 'banana',
|
||||
appId: 'nanana',
|
||||
})
|
||||
).rejects.toMatchInlineSnapshot(`[Error: UrlGeneratorId is required]`);
|
||||
});
|
||||
|
||||
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);
|
||||
it('saving updates an existing saved object and persists it', async () => {
|
||||
const mockUpdateSavedObject = {
|
||||
...mockSavedObject,
|
||||
attributes: {},
|
||||
};
|
||||
savedObjectsClient.get.mockResolvedValue(mockSavedObject);
|
||||
savedObjectsClient.update.mockResolvedValue(mockUpdateSavedObject);
|
||||
|
||||
const options = { page: 0, perPage: 5 };
|
||||
const response = await service.find({ savedObjectsClient }, options);
|
||||
await service.save({ savedObjectsClient }, mockUser1, sessionId, {
|
||||
name: 'banana',
|
||||
appId: 'nanana',
|
||||
urlGeneratorId: 'panama',
|
||||
});
|
||||
|
||||
expect(response).toBe(mockResponse);
|
||||
expect(savedObjectsClient.find).toHaveBeenCalledWith({
|
||||
...options,
|
||||
type: SEARCH_SESSION_TYPE,
|
||||
expect(savedObjectsClient.update).toHaveBeenCalled();
|
||||
expect(savedObjectsClient.create).not.toHaveBeenCalled();
|
||||
|
||||
const [type, id, callAttributes] = savedObjectsClient.update.mock.calls[0];
|
||||
expect(type).toBe(SEARCH_SESSION_TYPE);
|
||||
expect(id).toBe(sessionId);
|
||||
expect(callAttributes).not.toHaveProperty('idMapping');
|
||||
expect(callAttributes).toHaveProperty('touched');
|
||||
expect(callAttributes).toHaveProperty('persisted', true);
|
||||
expect(callAttributes).toHaveProperty('name', 'banana');
|
||||
expect(callAttributes).toHaveProperty('appId', 'nanana');
|
||||
expect(callAttributes).toHaveProperty('urlGeneratorId', 'panama');
|
||||
expect(callAttributes).toHaveProperty('initialState', {});
|
||||
expect(callAttributes).toHaveProperty('restoreState', {});
|
||||
});
|
||||
|
||||
it('saving creates a new persisted saved object, if it did not exist', async () => {
|
||||
const mockCreatedSavedObject = {
|
||||
...mockSavedObject,
|
||||
attributes: {},
|
||||
};
|
||||
|
||||
savedObjectsClient.update.mockRejectedValue(
|
||||
SavedObjectsErrorHelpers.createGenericNotFoundError(sessionId)
|
||||
);
|
||||
savedObjectsClient.create.mockResolvedValue(mockCreatedSavedObject);
|
||||
|
||||
await service.save({ savedObjectsClient }, mockUser1, sessionId, {
|
||||
name: 'banana',
|
||||
appId: 'nanana',
|
||||
urlGeneratorId: 'panama',
|
||||
});
|
||||
|
||||
expect(savedObjectsClient.update).toHaveBeenCalledTimes(1);
|
||||
expect(savedObjectsClient.create).toHaveBeenCalledTimes(1);
|
||||
|
||||
const [type, callAttributes, options] = savedObjectsClient.create.mock.calls[0];
|
||||
expect(type).toBe(SEARCH_SESSION_TYPE);
|
||||
expect(options?.id).toBe(sessionId);
|
||||
expect(callAttributes).toHaveProperty('idMapping', {});
|
||||
expect(callAttributes).toHaveProperty('touched');
|
||||
expect(callAttributes).toHaveProperty('expires');
|
||||
expect(callAttributes).toHaveProperty('created');
|
||||
expect(callAttributes).toHaveProperty('persisted', true);
|
||||
expect(callAttributes).toHaveProperty('name', 'banana');
|
||||
expect(callAttributes).toHaveProperty('appId', 'nanana');
|
||||
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('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)
|
||||
);
|
||||
|
||||
await service.save(
|
||||
{ savedObjectsClient },
|
||||
|
||||
null,
|
||||
sessionId,
|
||||
{
|
||||
name: 'my_name',
|
||||
appId: 'my_app_id',
|
||||
urlGeneratorId: 'my_url_generator_id',
|
||||
}
|
||||
);
|
||||
|
||||
expect(savedObjectsClient.create).toHaveBeenCalled();
|
||||
const [[, attributes]] = savedObjectsClient.create.mock.calls;
|
||||
expect(attributes).toHaveProperty('realmType', undefined);
|
||||
expect(attributes).toHaveProperty('realmName', undefined);
|
||||
expect(attributes).toHaveProperty('username', undefined);
|
||||
});
|
||||
});
|
||||
|
||||
it('update calls saved objects client with added touch time', async () => {
|
||||
const mockUpdateSavedObject = {
|
||||
...mockSavedObject,
|
||||
attributes: {},
|
||||
};
|
||||
savedObjectsClient.update.mockResolvedValue(mockUpdateSavedObject);
|
||||
describe('get', () => {
|
||||
it('calls saved objects client', async () => {
|
||||
savedObjectsClient.get.mockResolvedValue(mockSavedObject);
|
||||
|
||||
const attributes = { name: 'new_name' };
|
||||
const response = await service.update({ savedObjectsClient }, sessionId, attributes);
|
||||
const response = await service.get({ savedObjectsClient }, mockUser1, sessionId);
|
||||
|
||||
expect(response).toBe(mockUpdateSavedObject);
|
||||
expect(response).toBe(mockSavedObject);
|
||||
expect(savedObjectsClient.get).toHaveBeenCalledWith(SEARCH_SESSION_TYPE, sessionId);
|
||||
});
|
||||
|
||||
const [type, id, callAttributes] = savedObjectsClient.update.mock.calls[0];
|
||||
it('works without security', async () => {
|
||||
savedObjectsClient.get.mockResolvedValue(mockSavedObject);
|
||||
|
||||
expect(type).toBe(SEARCH_SESSION_TYPE);
|
||||
expect(id).toBe(sessionId);
|
||||
expect(callAttributes).toHaveProperty('name', attributes.name);
|
||||
expect(callAttributes).toHaveProperty('touched');
|
||||
const response = await service.get({ savedObjectsClient }, null, sessionId);
|
||||
|
||||
expect(response).toBe(mockSavedObject);
|
||||
expect(savedObjectsClient.get).toHaveBeenCalledWith(SEARCH_SESSION_TYPE, sessionId);
|
||||
});
|
||||
});
|
||||
|
||||
it('cancel updates object status', async () => {
|
||||
await service.cancel({ savedObjectsClient }, sessionId);
|
||||
const [type, id, callAttributes] = savedObjectsClient.update.mock.calls[0];
|
||||
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);
|
||||
|
||||
expect(type).toBe(SEARCH_SESSION_TYPE);
|
||||
expect(id).toBe(sessionId);
|
||||
expect(callAttributes).toHaveProperty('status', SearchSessionStatus.CANCELLED);
|
||||
expect(callAttributes).toHaveProperty('touched');
|
||||
const options = { page: 0, perPage: 5 };
|
||||
const response = await service.find({ savedObjectsClient }, mockUser1, options);
|
||||
|
||||
expect(response).toBe(mockResponse);
|
||||
const [[findOptions]] = savedObjectsClient.find.mock.calls;
|
||||
expect(findOptions).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",
|
||||
},
|
||||
],
|
||||
"function": "and",
|
||||
"type": "function",
|
||||
},
|
||||
"page": 0,
|
||||
"perPage": 5,
|
||||
"type": "search-session",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
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,
|
||||
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({ savedObjectsClient }, null, options);
|
||||
|
||||
expect(response).toBe(mockResponse);
|
||||
const [[findOptions]] = savedObjectsClient.find.mock.calls;
|
||||
expect(findOptions).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"filter": undefined,
|
||||
"page": 0,
|
||||
"perPage": 5,
|
||||
"type": "search-session",
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('update', () => {
|
||||
it('update calls saved objects client with added touch time', async () => {
|
||||
const mockUpdateSavedObject = {
|
||||
...mockSavedObject,
|
||||
attributes: {},
|
||||
};
|
||||
savedObjectsClient.get.mockResolvedValue(mockSavedObject);
|
||||
savedObjectsClient.update.mockResolvedValue(mockUpdateSavedObject);
|
||||
|
||||
const attributes = { name: 'new_name' };
|
||||
const response = await service.update(
|
||||
{ savedObjectsClient },
|
||||
mockUser1,
|
||||
sessionId,
|
||||
attributes
|
||||
);
|
||||
|
||||
expect(response).toBe(mockUpdateSavedObject);
|
||||
|
||||
const [type, id, callAttributes] = savedObjectsClient.update.mock.calls[0];
|
||||
|
||||
expect(type).toBe(SEARCH_SESSION_TYPE);
|
||||
expect(id).toBe(sessionId);
|
||||
expect(callAttributes).toHaveProperty('name', attributes.name);
|
||||
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,
|
||||
attributes: {},
|
||||
};
|
||||
savedObjectsClient.get.mockResolvedValue(mockSavedObject);
|
||||
savedObjectsClient.update.mockResolvedValue(mockUpdateSavedObject);
|
||||
|
||||
const attributes = { name: 'new_name' };
|
||||
const response = await service.update({ savedObjectsClient }, null, sessionId, attributes);
|
||||
const [type, id, callAttributes] = savedObjectsClient.update.mock.calls[0];
|
||||
|
||||
expect(response).toBe(mockUpdateSavedObject);
|
||||
expect(type).toBe(SEARCH_SESSION_TYPE);
|
||||
expect(id).toBe(sessionId);
|
||||
expect(callAttributes).toHaveProperty('name', 'new_name');
|
||||
expect(callAttributes).toHaveProperty('touched');
|
||||
});
|
||||
});
|
||||
|
||||
describe('cancel', () => {
|
||||
it('updates object status', async () => {
|
||||
savedObjectsClient.get.mockResolvedValue(mockSavedObject);
|
||||
|
||||
await service.cancel({ savedObjectsClient }, mockUser1, sessionId);
|
||||
const [type, id, callAttributes] = savedObjectsClient.update.mock.calls[0];
|
||||
|
||||
expect(type).toBe(SEARCH_SESSION_TYPE);
|
||||
expect(id).toBe(sessionId);
|
||||
expect(callAttributes).toHaveProperty('status', SearchSessionStatus.CANCELLED);
|
||||
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);
|
||||
|
||||
await service.cancel({ savedObjectsClient }, null, sessionId);
|
||||
|
||||
const [type, id, callAttributes] = savedObjectsClient.update.mock.calls[0];
|
||||
|
||||
expect(type).toBe(SEARCH_SESSION_TYPE);
|
||||
expect(id).toBe(sessionId);
|
||||
expect(callAttributes).toHaveProperty('status', SearchSessionStatus.CANCELLED);
|
||||
expect(callAttributes).toHaveProperty('touched');
|
||||
});
|
||||
});
|
||||
|
||||
describe('trackId', () => {
|
||||
|
@ -151,7 +655,7 @@ describe('SearchSessionService', () => {
|
|||
};
|
||||
savedObjectsClient.update.mockResolvedValue(mockUpdateSavedObject);
|
||||
|
||||
await service.trackId({ savedObjectsClient }, searchRequest, searchId, {
|
||||
await service.trackId({ savedObjectsClient }, mockUser1, searchRequest, searchId, {
|
||||
sessionId,
|
||||
strategy: MOCK_STRATEGY,
|
||||
});
|
||||
|
@ -194,7 +698,7 @@ describe('SearchSessionService', () => {
|
|||
});
|
||||
});
|
||||
|
||||
await service.trackId({ savedObjectsClient }, searchRequest, searchId, {
|
||||
await service.trackId({ savedObjectsClient }, mockUser1, searchRequest, searchId, {
|
||||
sessionId,
|
||||
strategy: MOCK_STRATEGY,
|
||||
});
|
||||
|
@ -213,7 +717,7 @@ describe('SearchSessionService', () => {
|
|||
});
|
||||
});
|
||||
|
||||
await service.trackId({ savedObjectsClient }, searchRequest, searchId, {
|
||||
await service.trackId({ savedObjectsClient }, mockUser1, searchRequest, searchId, {
|
||||
sessionId,
|
||||
strategy: MOCK_STRATEGY,
|
||||
});
|
||||
|
@ -238,7 +742,7 @@ describe('SearchSessionService', () => {
|
|||
);
|
||||
savedObjectsClient.create.mockResolvedValue(mockCreatedSavedObject);
|
||||
|
||||
await service.trackId({ savedObjectsClient }, searchRequest, searchId, {
|
||||
await service.trackId({ savedObjectsClient }, mockUser1, searchRequest, searchId, {
|
||||
sessionId,
|
||||
strategy: MOCK_STRATEGY,
|
||||
});
|
||||
|
@ -289,7 +793,7 @@ describe('SearchSessionService', () => {
|
|||
SavedObjectsErrorHelpers.createConflictError(SEARCH_SESSION_TYPE, searchId)
|
||||
);
|
||||
|
||||
await service.trackId({ savedObjectsClient }, searchRequest, searchId, {
|
||||
await service.trackId({ savedObjectsClient }, mockUser1, searchRequest, searchId, {
|
||||
sessionId,
|
||||
strategy: MOCK_STRATEGY,
|
||||
});
|
||||
|
@ -309,7 +813,7 @@ describe('SearchSessionService', () => {
|
|||
SavedObjectsErrorHelpers.createConflictError(SEARCH_SESSION_TYPE, searchId)
|
||||
);
|
||||
|
||||
await service.trackId({ savedObjectsClient }, searchRequest, searchId, {
|
||||
await service.trackId({ savedObjectsClient }, mockUser1, searchRequest, searchId, {
|
||||
sessionId,
|
||||
strategy: MOCK_STRATEGY,
|
||||
});
|
||||
|
@ -341,15 +845,15 @@ describe('SearchSessionService', () => {
|
|||
savedObjectsClient.update.mockResolvedValue(mockUpdateSavedObject);
|
||||
|
||||
await Promise.all([
|
||||
service.trackId({ savedObjectsClient }, searchRequest1, searchId1, {
|
||||
service.trackId({ savedObjectsClient }, mockUser1, searchRequest1, searchId1, {
|
||||
sessionId: sessionId1,
|
||||
strategy: MOCK_STRATEGY,
|
||||
}),
|
||||
service.trackId({ savedObjectsClient }, searchRequest2, searchId2, {
|
||||
service.trackId({ savedObjectsClient }, mockUser1, searchRequest2, searchId2, {
|
||||
sessionId: sessionId1,
|
||||
strategy: MOCK_STRATEGY,
|
||||
}),
|
||||
service.trackId({ savedObjectsClient }, searchRequest3, searchId3, {
|
||||
service.trackId({ savedObjectsClient }, mockUser1, searchRequest3, searchId3, {
|
||||
sessionId: sessionId2,
|
||||
strategy: MOCK_STRATEGY,
|
||||
}),
|
||||
|
@ -394,7 +898,7 @@ describe('SearchSessionService', () => {
|
|||
const searchRequest = { params: {} };
|
||||
|
||||
expect(() =>
|
||||
service.getId({ savedObjectsClient }, searchRequest, {})
|
||||
service.getId({ savedObjectsClient }, mockUser1, searchRequest, {})
|
||||
).rejects.toMatchInlineSnapshot(`[Error: Session ID is required]`);
|
||||
});
|
||||
|
||||
|
@ -402,7 +906,10 @@ describe('SearchSessionService', () => {
|
|||
const searchRequest = { params: {} };
|
||||
|
||||
expect(() =>
|
||||
service.getId({ savedObjectsClient }, searchRequest, { sessionId, isStored: false })
|
||||
service.getId({ savedObjectsClient }, mockUser1, searchRequest, {
|
||||
sessionId,
|
||||
isStored: false,
|
||||
})
|
||||
).rejects.toMatchInlineSnapshot(
|
||||
`[Error: Cannot get search ID from a session that is not stored]`
|
||||
);
|
||||
|
@ -412,7 +919,7 @@ describe('SearchSessionService', () => {
|
|||
const searchRequest = { params: {} };
|
||||
|
||||
expect(() =>
|
||||
service.getId({ savedObjectsClient }, searchRequest, {
|
||||
service.getId({ savedObjectsClient }, mockUser1, searchRequest, {
|
||||
sessionId,
|
||||
isStored: true,
|
||||
isRestore: false,
|
||||
|
@ -427,24 +934,19 @@ describe('SearchSessionService', () => {
|
|||
const requestHash = createRequestHash(searchRequest.params);
|
||||
const searchId = 'FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0';
|
||||
const mockSession = {
|
||||
id: 'd7170a35-7e2c-48d6-8dec-9a056721b489',
|
||||
type: SEARCH_SESSION_TYPE,
|
||||
...mockSavedObject,
|
||||
attributes: {
|
||||
name: 'my_name',
|
||||
appId: 'my_app_id',
|
||||
urlGeneratorId: 'my_url_generator_id',
|
||||
...mockSavedObject.attributes,
|
||||
idMapping: {
|
||||
[requestHash]: {
|
||||
id: searchId,
|
||||
strategy: MOCK_STRATEGY,
|
||||
},
|
||||
},
|
||||
},
|
||||
references: [],
|
||||
};
|
||||
savedObjectsClient.get.mockResolvedValue(mockSession);
|
||||
|
||||
const id = await service.getId({ savedObjectsClient }, searchRequest, {
|
||||
const id = await service.getId({ savedObjectsClient }, mockUser1, searchRequest, {
|
||||
sessionId,
|
||||
isStored: true,
|
||||
isRestore: true,
|
||||
|
@ -457,12 +959,9 @@ describe('SearchSessionService', () => {
|
|||
describe('getSearchIdMapping', () => {
|
||||
it('retrieves the search IDs and strategies from the saved object', async () => {
|
||||
const mockSession = {
|
||||
id: 'd7170a35-7e2c-48d6-8dec-9a056721b489',
|
||||
type: SEARCH_SESSION_TYPE,
|
||||
...mockSavedObject,
|
||||
attributes: {
|
||||
name: 'my_name',
|
||||
appId: 'my_app_id',
|
||||
urlGeneratorId: 'my_url_generator_id',
|
||||
...mockSavedObject.attributes,
|
||||
idMapping: {
|
||||
foo: {
|
||||
id: 'FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0',
|
||||
|
@ -470,11 +969,11 @@ describe('SearchSessionService', () => {
|
|||
},
|
||||
},
|
||||
},
|
||||
references: [],
|
||||
};
|
||||
savedObjectsClient.get.mockResolvedValue(mockSession);
|
||||
const searchIdMapping = await service.getSearchIdMapping(
|
||||
{ savedObjectsClient },
|
||||
mockUser1,
|
||||
mockSession.id
|
||||
);
|
||||
expect(searchIdMapping).toMatchInlineSnapshot(`
|
||||
|
@ -484,88 +983,4 @@ describe('SearchSessionService', () => {
|
|||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('save', () => {
|
||||
it('save throws if `name` is not provided', () => {
|
||||
expect(service.save({ savedObjectsClient }, sessionId, {})).rejects.toMatchInlineSnapshot(
|
||||
`[Error: Name is required]`
|
||||
);
|
||||
});
|
||||
|
||||
it('save throws if `appId` is not provided', () => {
|
||||
expect(
|
||||
service.save({ savedObjectsClient }, sessionId, { name: 'banana' })
|
||||
).rejects.toMatchInlineSnapshot(`[Error: AppId is required]`);
|
||||
});
|
||||
|
||||
it('save throws if `generator id` is not provided', () => {
|
||||
expect(
|
||||
service.save({ savedObjectsClient }, sessionId, { name: 'banana', appId: 'nanana' })
|
||||
).rejects.toMatchInlineSnapshot(`[Error: UrlGeneratorId is required]`);
|
||||
});
|
||||
|
||||
it('saving updates an existing saved object and persists it', async () => {
|
||||
const mockUpdateSavedObject = {
|
||||
...mockSavedObject,
|
||||
attributes: {},
|
||||
};
|
||||
savedObjectsClient.update.mockResolvedValue(mockUpdateSavedObject);
|
||||
|
||||
await service.save({ savedObjectsClient }, sessionId, {
|
||||
name: 'banana',
|
||||
appId: 'nanana',
|
||||
urlGeneratorId: 'panama',
|
||||
});
|
||||
|
||||
expect(savedObjectsClient.update).toHaveBeenCalled();
|
||||
expect(savedObjectsClient.create).not.toHaveBeenCalled();
|
||||
|
||||
const [type, id, callAttributes] = savedObjectsClient.update.mock.calls[0];
|
||||
expect(type).toBe(SEARCH_SESSION_TYPE);
|
||||
expect(id).toBe(sessionId);
|
||||
expect(callAttributes).not.toHaveProperty('idMapping');
|
||||
expect(callAttributes).toHaveProperty('touched');
|
||||
expect(callAttributes).toHaveProperty('persisted', true);
|
||||
expect(callAttributes).toHaveProperty('name', 'banana');
|
||||
expect(callAttributes).toHaveProperty('appId', 'nanana');
|
||||
expect(callAttributes).toHaveProperty('urlGeneratorId', 'panama');
|
||||
expect(callAttributes).toHaveProperty('initialState', {});
|
||||
expect(callAttributes).toHaveProperty('restoreState', {});
|
||||
});
|
||||
|
||||
it('saving creates a new persisted saved object, if it did not exist', async () => {
|
||||
const mockCreatedSavedObject = {
|
||||
...mockSavedObject,
|
||||
attributes: {},
|
||||
};
|
||||
|
||||
savedObjectsClient.update.mockRejectedValue(
|
||||
SavedObjectsErrorHelpers.createGenericNotFoundError(sessionId)
|
||||
);
|
||||
savedObjectsClient.create.mockResolvedValue(mockCreatedSavedObject);
|
||||
|
||||
await service.save({ savedObjectsClient }, sessionId, {
|
||||
name: 'banana',
|
||||
appId: 'nanana',
|
||||
urlGeneratorId: 'panama',
|
||||
});
|
||||
|
||||
expect(savedObjectsClient.update).toHaveBeenCalledTimes(1);
|
||||
expect(savedObjectsClient.create).toHaveBeenCalledTimes(1);
|
||||
|
||||
const [type, callAttributes, options] = savedObjectsClient.create.mock.calls[0];
|
||||
expect(type).toBe(SEARCH_SESSION_TYPE);
|
||||
expect(options?.id).toBe(sessionId);
|
||||
expect(callAttributes).toHaveProperty('idMapping', {});
|
||||
expect(callAttributes).toHaveProperty('touched');
|
||||
expect(callAttributes).toHaveProperty('expires');
|
||||
expect(callAttributes).toHaveProperty('created');
|
||||
expect(callAttributes).toHaveProperty('persisted', true);
|
||||
expect(callAttributes).toHaveProperty('name', 'banana');
|
||||
expect(callAttributes).toHaveProperty('appId', 'nanana');
|
||||
expect(callAttributes).toHaveProperty('urlGeneratorId', 'panama');
|
||||
expect(callAttributes).toHaveProperty('initialState', {});
|
||||
expect(callAttributes).toHaveProperty('restoreState', {});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { notFound } from '@hapi/boom';
|
||||
import { debounce } from 'lodash';
|
||||
import {
|
||||
CoreSetup,
|
||||
|
@ -16,8 +17,13 @@ import {
|
|||
SavedObjectsFindOptions,
|
||||
SavedObjectsErrorHelpers,
|
||||
} from '../../../../../../src/core/server';
|
||||
import { IKibanaSearchRequest, ISearchOptions } from '../../../../../../src/plugins/data/common';
|
||||
import { ISearchSessionService } from '../../../../../../src/plugins/data/server';
|
||||
import {
|
||||
IKibanaSearchRequest,
|
||||
ISearchOptions,
|
||||
nodeBuilder,
|
||||
} from '../../../../../../src/plugins/data/common';
|
||||
import { esKuery, ISearchSessionService } from '../../../../../../src/plugins/data/server';
|
||||
import { AuthenticatedUser, SecurityPluginSetup } from '../../../../security/server';
|
||||
import {
|
||||
TaskManagerSetupContract,
|
||||
TaskManagerStartContract,
|
||||
|
@ -49,6 +55,7 @@ const DEBOUNCE_UPDATE_OR_CREATE_MAX_WAIT = 5000;
|
|||
|
||||
interface UpdateOrCreateQueueEntry {
|
||||
deps: SearchSessionDependencies;
|
||||
user: AuthenticatedUser | null;
|
||||
sessionId: string;
|
||||
attributes: Partial<SearchSessionSavedObjectAttributes>;
|
||||
resolve: () => void;
|
||||
|
@ -63,7 +70,11 @@ export class SearchSessionService
|
|||
private sessionConfig: SearchSessionsConfig;
|
||||
private readonly updateOrCreateBatchQueue: UpdateOrCreateQueueEntry[] = [];
|
||||
|
||||
constructor(private readonly logger: Logger, private readonly config: ConfigSchema) {
|
||||
constructor(
|
||||
private readonly logger: Logger,
|
||||
private readonly config: ConfigSchema,
|
||||
private readonly security?: SecurityPluginSetup
|
||||
) {
|
||||
this.sessionConfig = this.config.search.sessions;
|
||||
}
|
||||
|
||||
|
@ -114,7 +125,12 @@ export class SearchSessionService
|
|||
|
||||
Object.keys(batchedSessionAttributes).forEach((sessionId) => {
|
||||
const thisSession = queue.filter((s) => s.sessionId === sessionId);
|
||||
this.updateOrCreate(thisSession[0].deps, sessionId, batchedSessionAttributes[sessionId])
|
||||
this.updateOrCreate(
|
||||
thisSession[0].deps,
|
||||
thisSession[0].user,
|
||||
sessionId,
|
||||
batchedSessionAttributes[sessionId]
|
||||
)
|
||||
.then(() => {
|
||||
thisSession.forEach((s) => s.resolve());
|
||||
})
|
||||
|
@ -128,11 +144,12 @@ export class SearchSessionService
|
|||
);
|
||||
private scheduleUpdateOrCreate = (
|
||||
deps: SearchSessionDependencies,
|
||||
user: AuthenticatedUser | null,
|
||||
sessionId: string,
|
||||
attributes: Partial<SearchSessionSavedObjectAttributes>
|
||||
): Promise<void> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.updateOrCreateBatchQueue.push({ deps, sessionId, attributes, resolve, reject });
|
||||
this.updateOrCreateBatchQueue.push({ deps, user, sessionId, attributes, resolve, reject });
|
||||
// TODO: this would be better if we'd debounce per sessionId
|
||||
this.processUpdateOrCreateBatchQueue();
|
||||
});
|
||||
|
@ -140,6 +157,7 @@ export class SearchSessionService
|
|||
|
||||
private updateOrCreate = async (
|
||||
deps: SearchSessionDependencies,
|
||||
user: AuthenticatedUser | null,
|
||||
sessionId: string,
|
||||
attributes: Partial<SearchSessionSavedObjectAttributes>,
|
||||
retry: number = 1
|
||||
|
@ -148,13 +166,14 @@ export class SearchSessionService
|
|||
this.logger.debug(`Conflict error | ${sessionId}`);
|
||||
// Randomize sleep to spread updates out in case of conflicts
|
||||
await sleep(100 + Math.random() * 50);
|
||||
return await this.updateOrCreate(deps, sessionId, attributes, retry + 1);
|
||||
return await this.updateOrCreate(deps, user, sessionId, attributes, retry + 1);
|
||||
};
|
||||
|
||||
this.logger.debug(`updateOrCreate | ${sessionId} | ${retry}`);
|
||||
try {
|
||||
return (await this.update(
|
||||
deps,
|
||||
user,
|
||||
sessionId,
|
||||
attributes
|
||||
)) as SavedObject<SearchSessionSavedObjectAttributes>;
|
||||
|
@ -162,7 +181,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) &&
|
||||
|
@ -188,6 +207,7 @@ export class SearchSessionService
|
|||
|
||||
public save = async (
|
||||
deps: SearchSessionDependencies,
|
||||
user: AuthenticatedUser | null,
|
||||
sessionId: string,
|
||||
{
|
||||
name,
|
||||
|
@ -201,7 +221,7 @@ export class SearchSessionService
|
|||
if (!appId) throw new Error('AppId is required');
|
||||
if (!urlGeneratorId) throw new Error('UrlGeneratorId is required');
|
||||
|
||||
return this.updateOrCreate(deps, sessionId, {
|
||||
return this.updateOrCreate(deps, user, sessionId, {
|
||||
name,
|
||||
appId,
|
||||
urlGeneratorId,
|
||||
|
@ -213,10 +233,16 @@ export class SearchSessionService
|
|||
|
||||
private create = (
|
||||
{ savedObjectsClient }: SearchSessionDependencies,
|
||||
user: AuthenticatedUser | null,
|
||||
sessionId: string,
|
||||
attributes: Partial<SearchSessionSavedObjectAttributes>
|
||||
) => {
|
||||
this.logger.debug(`create | ${sessionId}`);
|
||||
|
||||
const realmType = user?.authentication_realm.type;
|
||||
const realmName = user?.authentication_realm.name;
|
||||
const username = user?.username;
|
||||
|
||||
return savedObjectsClient.create<SearchSessionSavedObjectAttributes>(
|
||||
SEARCH_SESSION_TYPE,
|
||||
{
|
||||
|
@ -229,40 +255,69 @@ export class SearchSessionService
|
|||
touched: new Date().toISOString(),
|
||||
idMapping: {},
|
||||
persisted: false,
|
||||
realmType,
|
||||
realmName,
|
||||
username,
|
||||
...attributes,
|
||||
},
|
||||
{ id: sessionId }
|
||||
);
|
||||
};
|
||||
|
||||
// TODO: Throw an error if this session doesn't belong to this user
|
||||
public get = ({ savedObjectsClient }: SearchSessionDependencies, sessionId: string) => {
|
||||
public get = async (
|
||||
{ savedObjectsClient }: SearchSessionDependencies,
|
||||
user: AuthenticatedUser | null,
|
||||
sessionId: string
|
||||
) => {
|
||||
this.logger.debug(`get | ${sessionId}`);
|
||||
return savedObjectsClient.get<SearchSessionSavedObjectAttributes>(
|
||||
const session = await savedObjectsClient.get<SearchSessionSavedObjectAttributes>(
|
||||
SEARCH_SESSION_TYPE,
|
||||
sessionId
|
||||
);
|
||||
this.throwOnUserConflict(user, session);
|
||||
return session;
|
||||
};
|
||||
|
||||
// TODO: Throw an error if this session doesn't belong to this user
|
||||
public find = (
|
||||
{ savedObjectsClient }: SearchSessionDependencies,
|
||||
user: AuthenticatedUser | null,
|
||||
options: Omit<SavedObjectsFindOptions, 'type'>
|
||||
) => {
|
||||
const userFilters =
|
||||
user === null
|
||||
? []
|
||||
: [
|
||||
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 filterKueryNode =
|
||||
typeof options.filter === 'string'
|
||||
? esKuery.fromKueryExpression(options.filter)
|
||||
: options.filter;
|
||||
const filter = nodeBuilder.and(userFilters.concat(filterKueryNode ?? []));
|
||||
return savedObjectsClient.find<SearchSessionSavedObjectAttributes>({
|
||||
...options,
|
||||
filter,
|
||||
type: SEARCH_SESSION_TYPE,
|
||||
});
|
||||
};
|
||||
|
||||
// TODO: Throw an error if this session doesn't belong to this user
|
||||
public update = (
|
||||
{ savedObjectsClient }: SearchSessionDependencies,
|
||||
public update = async (
|
||||
deps: SearchSessionDependencies,
|
||||
user: AuthenticatedUser | null,
|
||||
sessionId: string,
|
||||
attributes: Partial<SearchSessionSavedObjectAttributes>
|
||||
) => {
|
||||
this.logger.debug(`update | ${sessionId}`);
|
||||
return savedObjectsClient.update<SearchSessionSavedObjectAttributes>(
|
||||
await this.get(deps, user, sessionId); // Verify correct user
|
||||
return deps.savedObjectsClient.update<SearchSessionSavedObjectAttributes>(
|
||||
SEARCH_SESSION_TYPE,
|
||||
sessionId,
|
||||
{
|
||||
|
@ -272,22 +327,35 @@ export class SearchSessionService
|
|||
);
|
||||
};
|
||||
|
||||
public extend(deps: SearchSessionDependencies, sessionId: string, expires: Date) {
|
||||
public async extend(
|
||||
deps: SearchSessionDependencies,
|
||||
user: AuthenticatedUser | null,
|
||||
sessionId: string,
|
||||
expires: Date
|
||||
) {
|
||||
this.logger.debug(`extend | ${sessionId}`);
|
||||
|
||||
return this.update(deps, sessionId, { expires: expires.toISOString() });
|
||||
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, sessionId: string) => {
|
||||
return this.update(deps, sessionId, {
|
||||
public cancel = async (
|
||||
deps: SearchSessionDependencies,
|
||||
user: AuthenticatedUser | null,
|
||||
sessionId: string
|
||||
) => {
|
||||
this.logger.debug(`delete | ${sessionId}`);
|
||||
return this.update(deps, user, sessionId, {
|
||||
status: SearchSessionStatus.CANCELLED,
|
||||
});
|
||||
};
|
||||
|
||||
// TODO: Throw an error if this session doesn't belong to this user
|
||||
public delete = ({ savedObjectsClient }: SearchSessionDependencies, sessionId: string) => {
|
||||
return savedObjectsClient.delete(SEARCH_SESSION_TYPE, sessionId);
|
||||
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);
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -296,6 +364,7 @@ export class SearchSessionService
|
|||
*/
|
||||
public trackId = async (
|
||||
deps: SearchSessionDependencies,
|
||||
user: AuthenticatedUser | null,
|
||||
searchRequest: IKibanaSearchRequest,
|
||||
searchId: string,
|
||||
{ sessionId, strategy }: ISearchOptions
|
||||
|
@ -315,11 +384,15 @@ export class SearchSessionService
|
|||
idMapping = { [requestHash]: searchInfo };
|
||||
}
|
||||
|
||||
await this.scheduleUpdateOrCreate(deps, sessionId, { idMapping });
|
||||
await this.scheduleUpdateOrCreate(deps, user, sessionId, { idMapping });
|
||||
};
|
||||
|
||||
public async getSearchIdMapping(deps: SearchSessionDependencies, sessionId: string) {
|
||||
const searchSession = await this.get(deps, sessionId);
|
||||
public async getSearchIdMapping(
|
||||
deps: SearchSessionDependencies,
|
||||
user: AuthenticatedUser | null,
|
||||
sessionId: string
|
||||
) {
|
||||
const searchSession = await this.get(deps, user, sessionId);
|
||||
const searchIdMapping = new Map<string, string>();
|
||||
Object.values(searchSession.attributes.idMapping).forEach((requestInfo) => {
|
||||
searchIdMapping.set(requestInfo.id, requestInfo.strategy);
|
||||
|
@ -334,6 +407,7 @@ export class SearchSessionService
|
|||
*/
|
||||
public getId = async (
|
||||
deps: SearchSessionDependencies,
|
||||
user: AuthenticatedUser | null,
|
||||
searchRequest: IKibanaSearchRequest,
|
||||
{ sessionId, isStored, isRestore }: ISearchOptions
|
||||
) => {
|
||||
|
@ -345,7 +419,7 @@ export class SearchSessionService
|
|||
throw new Error('Get search ID is only supported when restoring a session');
|
||||
}
|
||||
|
||||
const session = await this.get(deps, sessionId);
|
||||
const session = await this.get(deps, user, sessionId);
|
||||
const requestHash = createRequestHash(searchRequest.params);
|
||||
if (!session.attributes.idMapping.hasOwnProperty(requestHash)) {
|
||||
this.logger.error(`getId | ${sessionId} | ${requestHash} not found`);
|
||||
|
@ -358,22 +432,40 @@ export class SearchSessionService
|
|||
|
||||
public asScopedProvider = ({ savedObjects }: CoreStart) => {
|
||||
return (request: KibanaRequest) => {
|
||||
const user = this.security?.authc.getCurrentUser(request) ?? null;
|
||||
const savedObjectsClient = savedObjects.getScopedClient(request, {
|
||||
includedHiddenTypes: [SEARCH_SESSION_TYPE],
|
||||
});
|
||||
const deps = { savedObjectsClient };
|
||||
return {
|
||||
getId: this.getId.bind(this, deps),
|
||||
trackId: this.trackId.bind(this, deps),
|
||||
getSearchIdMapping: this.getSearchIdMapping.bind(this, deps),
|
||||
save: this.save.bind(this, deps),
|
||||
get: this.get.bind(this, deps),
|
||||
find: this.find.bind(this, deps),
|
||||
update: this.update.bind(this, deps),
|
||||
extend: this.extend.bind(this, deps),
|
||||
cancel: this.cancel.bind(this, deps),
|
||||
delete: this.delete.bind(this, deps),
|
||||
getId: this.getId.bind(this, deps, user),
|
||||
trackId: this.trackId.bind(this, deps, user),
|
||||
getSearchIdMapping: this.getSearchIdMapping.bind(this, deps, user),
|
||||
save: this.save.bind(this, deps, user),
|
||||
get: this.get.bind(this, deps, user),
|
||||
find: this.find.bind(this, deps, user),
|
||||
update: this.update.bind(this, deps, user),
|
||||
extend: this.extend.bind(this, deps, user),
|
||||
cancel: this.cancel.bind(this, deps, user),
|
||||
delete: this.delete.bind(this, deps, user),
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
private throwOnUserConflict = (
|
||||
user: AuthenticatedUser | null,
|
||||
session?: SavedObject<SearchSessionSavedObjectAttributes>
|
||||
) => {
|
||||
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();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -25,6 +25,7 @@
|
|||
{ "path": "../../../src/plugins/kibana_utils/tsconfig.json" },
|
||||
{ "path": "../../../src/plugins/usage_collection/tsconfig.json" },
|
||||
{ "path": "../../../src/plugins/management/tsconfig.json" },
|
||||
{ "path": "../security/tsconfig.json" },
|
||||
{ "path": "../task_manager/tsconfig.json" },
|
||||
|
||||
{ "path": "../features/tsconfig.json" },
|
||||
|
|
|
@ -328,6 +328,122 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
);
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
describe('search session permissions', () => {
|
||||
before(async () => {
|
||||
await security.role.create('data_analyst', {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue