From 61b3972e5dea889b7cfb4622c89db13c0e3923d5 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Tue, 26 Nov 2019 08:01:20 -0700 Subject: [PATCH 01/12] [Maps] refactor static can skip logic from layer class (#51627) --- .../plugins/maps/public/layers/layer.js | 41 --- .../plugins/maps/public/layers/layer.test.js | 113 ------- .../maps/public/layers/util/can_skip_fetch.js | 130 ++++++++ .../public/layers/util/can_skip_fetch.test.js | 287 ++++++++++++++++++ .../maps/public/layers/vector_layer.js | 111 ++----- .../maps/public/layers/vector_layer.test.js | 185 ----------- 6 files changed, 436 insertions(+), 431 deletions(-) delete mode 100644 x-pack/legacy/plugins/maps/public/layers/layer.test.js create mode 100644 x-pack/legacy/plugins/maps/public/layers/util/can_skip_fetch.js create mode 100644 x-pack/legacy/plugins/maps/public/layers/util/can_skip_fetch.test.js delete mode 100644 x-pack/legacy/plugins/maps/public/layers/vector_layer.test.js diff --git a/x-pack/legacy/plugins/maps/public/layers/layer.js b/x-pack/legacy/plugins/maps/public/layers/layer.js index 72a89046ed2f5a..1c2f33df66bf89 100644 --- a/x-pack/legacy/plugins/maps/public/layers/layer.js +++ b/x-pack/legacy/plugins/maps/public/layers/layer.js @@ -6,8 +6,6 @@ import _ from 'lodash'; import React from 'react'; import { EuiIcon, EuiLoadingSpinner } from '@elastic/eui'; -import turf from 'turf'; -import turfBooleanContains from '@turf/boolean-contains'; import { DataRequest } from './util/data_request'; import { MAX_ZOOM, @@ -19,9 +17,6 @@ import uuid from 'uuid/v4'; import { copyPersistentState } from '../reducers/util'; import { i18n } from '@kbn/i18n'; -const SOURCE_UPDATE_REQUIRED = true; -const NO_SOURCE_UPDATE_REQUIRED = false; - export class AbstractLayer { constructor({ layerDescriptor, source }) { @@ -316,42 +311,7 @@ export class AbstractLayer { throw new Error('Should implement AbstractLayer#syncLayerWithMB'); } - updateDueToExtent(source, prevMeta = {}, nextMeta = {}) { - const extentAware = source.isFilterByMapBounds(); - if (!extentAware) { - return NO_SOURCE_UPDATE_REQUIRED; - } - const { buffer: previousBuffer } = prevMeta; - const { buffer: newBuffer } = nextMeta; - - if (!previousBuffer) { - return SOURCE_UPDATE_REQUIRED; - } - - if (_.isEqual(previousBuffer, newBuffer)) { - return NO_SOURCE_UPDATE_REQUIRED; - } - - const previousBufferGeometry = turf.bboxPolygon([ - previousBuffer.minLon, - previousBuffer.minLat, - previousBuffer.maxLon, - previousBuffer.maxLat - ]); - const newBufferGeometry = turf.bboxPolygon([ - newBuffer.minLon, - newBuffer.minLat, - newBuffer.maxLon, - newBuffer.maxLat - ]); - const doesPreviousBufferContainNewBuffer = turfBooleanContains(previousBufferGeometry, newBufferGeometry); - - const isTrimmed = _.get(prevMeta, 'areResultsTrimmed', false); - return doesPreviousBufferContainNewBuffer && !isTrimmed - ? NO_SOURCE_UPDATE_REQUIRED - : SOURCE_UPDATE_REQUIRED; - } getLayerTypeIconName() { throw new Error('should implement Layer#getLayerTypeIconName'); @@ -407,4 +367,3 @@ export class AbstractLayer { } } - diff --git a/x-pack/legacy/plugins/maps/public/layers/layer.test.js b/x-pack/legacy/plugins/maps/public/layers/layer.test.js deleted file mode 100644 index 98be0855cd4b7b..00000000000000 --- a/x-pack/legacy/plugins/maps/public/layers/layer.test.js +++ /dev/null @@ -1,113 +0,0 @@ -/* - * 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 { AbstractLayer } from './layer'; - -describe('layer', () => { - const layer = new AbstractLayer({ layerDescriptor: {} }); - - describe('updateDueToExtent', () => { - - it('should be false when the source is not extent aware', async () => { - const sourceMock = { - isFilterByMapBounds: () => { return false; } - }; - const updateDueToExtent = layer.updateDueToExtent(sourceMock); - expect(updateDueToExtent).toBe(false); - }); - - it('should be false when buffers are the same', async () => { - const sourceMock = { - isFilterByMapBounds: () => { return true; } - }; - const oldBuffer = { - maxLat: 12.5, - maxLon: 102.5, - minLat: 2.5, - minLon: 92.5, - }; - const newBuffer = { - maxLat: 12.5, - maxLon: 102.5, - minLat: 2.5, - minLon: 92.5, - }; - const updateDueToExtent = layer.updateDueToExtent(sourceMock, { buffer: oldBuffer }, { buffer: newBuffer }); - expect(updateDueToExtent).toBe(false); - }); - - it('should be false when the new buffer is contained in the old buffer', async () => { - const sourceMock = { - isFilterByMapBounds: () => { return true; } - }; - const oldBuffer = { - maxLat: 12.5, - maxLon: 102.5, - minLat: 2.5, - minLon: 92.5, - }; - const newBuffer = { - maxLat: 10, - maxLon: 100, - minLat: 5, - minLon: 95, - }; - const updateDueToExtent = layer.updateDueToExtent(sourceMock, { buffer: oldBuffer }, { buffer: newBuffer }); - expect(updateDueToExtent).toBe(false); - }); - - it('should be true when the new buffer is contained in the old buffer and the past results were truncated', async () => { - const sourceMock = { - isFilterByMapBounds: () => { return true; } - }; - const oldBuffer = { - maxLat: 12.5, - maxLon: 102.5, - minLat: 2.5, - minLon: 92.5, - }; - const newBuffer = { - maxLat: 10, - maxLon: 100, - minLat: 5, - minLon: 95, - }; - const updateDueToExtent = layer.updateDueToExtent( - sourceMock, - { buffer: oldBuffer, areResultsTrimmed: true }, - { buffer: newBuffer }); - expect(updateDueToExtent).toBe(true); - }); - - it('should be true when meta has no old buffer', async () => { - const sourceMock = { - isFilterByMapBounds: () => { return true; } - }; - const updateDueToExtent = layer.updateDueToExtent(sourceMock); - expect(updateDueToExtent).toBe(true); - }); - - it('should be true when the new buffer is not contained in the old buffer', async () => { - const sourceMock = { - isFilterByMapBounds: () => { return true; } - }; - const oldBuffer = { - maxLat: 12.5, - maxLon: 102.5, - minLat: 2.5, - minLon: 92.5, - }; - const newBuffer = { - maxLat: 7.5, - maxLon: 92.5, - minLat: -2.5, - minLon: 82.5, - }; - const updateDueToExtent = layer.updateDueToExtent(sourceMock, { buffer: oldBuffer }, { buffer: newBuffer }); - expect(updateDueToExtent).toBe(true); - }); - }); -}); diff --git a/x-pack/legacy/plugins/maps/public/layers/util/can_skip_fetch.js b/x-pack/legacy/plugins/maps/public/layers/util/can_skip_fetch.js new file mode 100644 index 00000000000000..610c704b34ec67 --- /dev/null +++ b/x-pack/legacy/plugins/maps/public/layers/util/can_skip_fetch.js @@ -0,0 +1,130 @@ +/* + * 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 _ from 'lodash'; +import turf from 'turf'; +import turfBooleanContains from '@turf/boolean-contains'; +import { isRefreshOnlyQuery } from './is_refresh_only_query'; + +const SOURCE_UPDATE_REQUIRED = true; +const NO_SOURCE_UPDATE_REQUIRED = false; + +export function updateDueToExtent(source, prevMeta = {}, nextMeta = {}) { + const extentAware = source.isFilterByMapBounds(); + if (!extentAware) { + return NO_SOURCE_UPDATE_REQUIRED; + } + + const { buffer: previousBuffer } = prevMeta; + const { buffer: newBuffer } = nextMeta; + + if (!previousBuffer) { + return SOURCE_UPDATE_REQUIRED; + } + + if (_.isEqual(previousBuffer, newBuffer)) { + return NO_SOURCE_UPDATE_REQUIRED; + } + + const previousBufferGeometry = turf.bboxPolygon([ + previousBuffer.minLon, + previousBuffer.minLat, + previousBuffer.maxLon, + previousBuffer.maxLat + ]); + const newBufferGeometry = turf.bboxPolygon([ + newBuffer.minLon, + newBuffer.minLat, + newBuffer.maxLon, + newBuffer.maxLat + ]); + const doesPreviousBufferContainNewBuffer = turfBooleanContains(previousBufferGeometry, newBufferGeometry); + + const isTrimmed = _.get(prevMeta, 'areResultsTrimmed', false); + return doesPreviousBufferContainNewBuffer && !isTrimmed + ? NO_SOURCE_UPDATE_REQUIRED + : SOURCE_UPDATE_REQUIRED; +} + +export async function canSkipSourceUpdate({ source, prevDataRequest, nextMeta }) { + + const timeAware = await source.isTimeAware(); + const refreshTimerAware = await source.isRefreshTimerAware(); + const extentAware = source.isFilterByMapBounds(); + const isFieldAware = source.isFieldAware(); + const isQueryAware = source.isQueryAware(); + const isGeoGridPrecisionAware = source.isGeoGridPrecisionAware(); + + if ( + !timeAware && + !refreshTimerAware && + !extentAware && + !isFieldAware && + !isQueryAware && + !isGeoGridPrecisionAware + ) { + return (prevDataRequest && prevDataRequest.hasDataOrRequestInProgress()); + } + + if (!prevDataRequest) { + return false; + } + const prevMeta = prevDataRequest.getMeta(); + if (!prevMeta) { + return false; + } + + let updateDueToTime = false; + if (timeAware) { + updateDueToTime = !_.isEqual(prevMeta.timeFilters, nextMeta.timeFilters); + } + + let updateDueToRefreshTimer = false; + if (refreshTimerAware && nextMeta.refreshTimerLastTriggeredAt) { + updateDueToRefreshTimer = !_.isEqual(prevMeta.refreshTimerLastTriggeredAt, nextMeta.refreshTimerLastTriggeredAt); + } + + let updateDueToFields = false; + if (isFieldAware) { + updateDueToFields = !_.isEqual(prevMeta.fieldNames, nextMeta.fieldNames); + } + + let updateDueToQuery = false; + let updateDueToFilters = false; + let updateDueToSourceQuery = false; + let updateDueToApplyGlobalQuery = false; + if (isQueryAware) { + updateDueToApplyGlobalQuery = prevMeta.applyGlobalQuery !== nextMeta.applyGlobalQuery; + updateDueToSourceQuery = !_.isEqual(prevMeta.sourceQuery, nextMeta.sourceQuery); + if (nextMeta.applyGlobalQuery) { + updateDueToQuery = !_.isEqual(prevMeta.query, nextMeta.query); + updateDueToFilters = !_.isEqual(prevMeta.filters, nextMeta.filters); + } else { + // Global filters and query are not applied to layer search request so no re-fetch required. + // Exception is "Refresh" query. + updateDueToQuery = isRefreshOnlyQuery(prevMeta.query, nextMeta.query); + } + } + + let updateDueToPrecisionChange = false; + if (isGeoGridPrecisionAware) { + updateDueToPrecisionChange = !_.isEqual(prevMeta.geogridPrecision, nextMeta.geogridPrecision); + } + + const updateDueToExtentChange = updateDueToExtent(source, prevMeta, nextMeta); + + const updateDueToSourceMetaChange = !_.isEqual(prevMeta.sourceMeta, nextMeta.sourceMeta); + + return !updateDueToTime + && !updateDueToRefreshTimer + && !updateDueToExtentChange + && !updateDueToFields + && !updateDueToQuery + && !updateDueToFilters + && !updateDueToSourceQuery + && !updateDueToApplyGlobalQuery + && !updateDueToPrecisionChange + && !updateDueToSourceMetaChange; +} diff --git a/x-pack/legacy/plugins/maps/public/layers/util/can_skip_fetch.test.js b/x-pack/legacy/plugins/maps/public/layers/util/can_skip_fetch.test.js new file mode 100644 index 00000000000000..77359a6def48f5 --- /dev/null +++ b/x-pack/legacy/plugins/maps/public/layers/util/can_skip_fetch.test.js @@ -0,0 +1,287 @@ +/* + * 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 { canSkipSourceUpdate, updateDueToExtent } from './can_skip_fetch'; +import { DataRequest } from './data_request'; + +describe('updateDueToExtent', () => { + + it('should be false when the source is not extent aware', async () => { + const sourceMock = { + isFilterByMapBounds: () => { return false; } + }; + expect(updateDueToExtent(sourceMock)).toBe(false); + }); + + describe('source is extent aware', () => { + const sourceMock = { + isFilterByMapBounds: () => { return true; } + }; + + it('should be false when buffers are the same', async () => { + const oldBuffer = { + maxLat: 12.5, + maxLon: 102.5, + minLat: 2.5, + minLon: 92.5, + }; + const newBuffer = { + maxLat: 12.5, + maxLon: 102.5, + minLat: 2.5, + minLon: 92.5, + }; + expect(updateDueToExtent(sourceMock, { buffer: oldBuffer }, { buffer: newBuffer })) + .toBe(false); + }); + + it('should be false when the new buffer is contained in the old buffer', async () => { + const oldBuffer = { + maxLat: 12.5, + maxLon: 102.5, + minLat: 2.5, + minLon: 92.5, + }; + const newBuffer = { + maxLat: 10, + maxLon: 100, + minLat: 5, + minLon: 95, + }; + expect(updateDueToExtent(sourceMock, { buffer: oldBuffer }, { buffer: newBuffer })).toBe(false); + }); + + it('should be true when the new buffer is contained in the old buffer and the past results were truncated', async () => { + const oldBuffer = { + maxLat: 12.5, + maxLon: 102.5, + minLat: 2.5, + minLon: 92.5, + }; + const newBuffer = { + maxLat: 10, + maxLon: 100, + minLat: 5, + minLon: 95, + }; + expect(updateDueToExtent( + sourceMock, + { buffer: oldBuffer, areResultsTrimmed: true }, + { buffer: newBuffer } + )).toBe(true); + }); + + it('should be true when meta has no old buffer', async () => { + expect(updateDueToExtent(sourceMock)).toBe(true); + }); + + it('should be true when the new buffer is not contained in the old buffer', async () => { + const oldBuffer = { + maxLat: 12.5, + maxLon: 102.5, + minLat: 2.5, + minLon: 92.5, + }; + const newBuffer = { + maxLat: 7.5, + maxLon: 92.5, + minLat: -2.5, + minLon: 82.5, + }; + expect(updateDueToExtent(sourceMock, { buffer: oldBuffer }, { buffer: newBuffer })).toBe(true); + }); + }); +}); + +describe('canSkipSourceUpdate', () => { + const SOURCE_DATA_REQUEST_ID = 'foo'; + + describe('isQueryAware', () => { + + const queryAwareSourceMock = { + isTimeAware: () => { return false; }, + isRefreshTimerAware: () => { return false; }, + isFilterByMapBounds: () => { return false; }, + isFieldAware: () => { return false; }, + isQueryAware: () => { return true; }, + isGeoGridPrecisionAware: () => { return false; }, + }; + const prevFilters = []; + const prevQuery = { + language: 'kuery', + query: 'machine.os.keyword : "win 7"', + queryLastTriggeredAt: '2019-04-25T20:53:22.331Z' + }; + + describe('applyGlobalQuery is false', () => { + + const prevApplyGlobalQuery = false; + + const prevDataRequest = new DataRequest({ + dataId: SOURCE_DATA_REQUEST_ID, + dataMeta: { + applyGlobalQuery: prevApplyGlobalQuery, + filters: prevFilters, + query: prevQuery, + } + }); + + it('can skip update when filter changes', async () => { + const nextMeta = { + applyGlobalQuery: prevApplyGlobalQuery, + filters: [prevQuery], + query: prevQuery, + }; + + const canSkipUpdate = await canSkipSourceUpdate({ + source: queryAwareSourceMock, + prevDataRequest, + nextMeta, + }); + + expect(canSkipUpdate).toBe(true); + }); + + it('can skip update when query changes', async () => { + const nextMeta = { + applyGlobalQuery: prevApplyGlobalQuery, + filters: prevFilters, + query: { + ...prevQuery, + query: 'a new query string', + } + }; + + const canSkipUpdate = await canSkipSourceUpdate({ + source: queryAwareSourceMock, + prevDataRequest, + nextMeta + }); + + expect(canSkipUpdate).toBe(true); + }); + + it('can not skip update when query is refreshed', async () => { + const nextMeta = { + applyGlobalQuery: prevApplyGlobalQuery, + filters: prevFilters, + query: { + ...prevQuery, + queryLastTriggeredAt: 'sometime layer when Refresh button is clicked' + } + }; + + const canSkipUpdate = await canSkipSourceUpdate({ + source: queryAwareSourceMock, + prevDataRequest, + nextMeta + }); + + expect(canSkipUpdate).toBe(false); + }); + + it('can not skip update when applyGlobalQuery changes', async () => { + const nextMeta = { + applyGlobalQuery: !prevApplyGlobalQuery, + filters: prevFilters, + query: prevQuery + }; + + const canSkipUpdate = await canSkipSourceUpdate({ + source: queryAwareSourceMock, + prevDataRequest, + nextMeta + }); + + expect(canSkipUpdate).toBe(false); + }); + }); + + describe('applyGlobalQuery is true', () => { + + const prevApplyGlobalQuery = true; + + const prevDataRequest = new DataRequest({ + dataId: SOURCE_DATA_REQUEST_ID, + dataMeta: { + applyGlobalQuery: prevApplyGlobalQuery, + filters: prevFilters, + query: prevQuery, + } + }); + + it('can not skip update when filter changes', async () => { + const nextMeta = { + applyGlobalQuery: prevApplyGlobalQuery, + filters: [prevQuery], + query: prevQuery, + }; + + const canSkipUpdate = await canSkipSourceUpdate({ + source: queryAwareSourceMock, + prevDataRequest, + nextMeta + }); + + expect(canSkipUpdate).toBe(false); + }); + + it('can not skip update when query changes', async () => { + const nextMeta = { + applyGlobalQuery: prevApplyGlobalQuery, + filters: prevFilters, + query: { + ...prevQuery, + query: 'a new query string', + } + }; + + const canSkipUpdate = await canSkipSourceUpdate({ + source: queryAwareSourceMock, + prevDataRequest, + nextMeta + }); + + expect(canSkipUpdate).toBe(false); + }); + + it('can not skip update when query is refreshed', async () => { + const nextMeta = { + applyGlobalQuery: prevApplyGlobalQuery, + filters: prevFilters, + query: { + ...prevQuery, + queryLastTriggeredAt: 'sometime layer when Refresh button is clicked' + } + }; + + const canSkipUpdate = await canSkipSourceUpdate({ + source: queryAwareSourceMock, + prevDataRequest, + nextMeta + }); + + expect(canSkipUpdate).toBe(false); + }); + + it('can not skip update when applyGlobalQuery changes', async () => { + const nextMeta = { + applyGlobalQuery: !prevApplyGlobalQuery, + filters: prevFilters, + query: prevQuery + }; + + const canSkipUpdate = await canSkipSourceUpdate({ + source: queryAwareSourceMock, + prevDataRequest, + nextMeta + }); + + expect(canSkipUpdate).toBe(false); + }); + }); + }); +}); diff --git a/x-pack/legacy/plugins/maps/public/layers/vector_layer.js b/x-pack/legacy/plugins/maps/public/layers/vector_layer.js index 9b553803606ed4..57126bb7681b85 100644 --- a/x-pack/legacy/plugins/maps/public/layers/vector_layer.js +++ b/x-pack/legacy/plugins/maps/public/layers/vector_layer.js @@ -18,10 +18,10 @@ import { } from '../../common/constants'; import _ from 'lodash'; import { JoinTooltipProperty } from './tooltips/join_tooltip_property'; -import { isRefreshOnlyQuery } from './util/is_refresh_only_query'; import { EuiIcon } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { DataRequestAbortError } from './util/data_request'; +import { canSkipSourceUpdate } from './util/can_skip_fetch'; import { assignFeatureIds } from './util/assign_feature_ids'; import { getFillFilterExpression, @@ -229,109 +229,31 @@ export class VectorLayer extends AbstractLayer { return this._dataRequests.find(dataRequest => dataRequest.getDataId() === sourceDataId); } - async _canSkipSourceUpdate(source, sourceDataId, nextMeta) { - const timeAware = await source.isTimeAware(); - const refreshTimerAware = await source.isRefreshTimerAware(); - const extentAware = source.isFilterByMapBounds(); - const isFieldAware = source.isFieldAware(); - const isQueryAware = source.isQueryAware(); - const isGeoGridPrecisionAware = source.isGeoGridPrecisionAware(); - - if ( - !timeAware && - !refreshTimerAware && - !extentAware && - !isFieldAware && - !isQueryAware && - !isGeoGridPrecisionAware - ) { - const sourceDataRequest = this._findDataRequestForSource(sourceDataId); - return (sourceDataRequest && sourceDataRequest.hasDataOrRequestInProgress()); - } - - const sourceDataRequest = this._findDataRequestForSource(sourceDataId); - if (!sourceDataRequest) { - return false; - } - const prevMeta = sourceDataRequest.getMeta(); - if (!prevMeta) { - return false; - } - - let updateDueToTime = false; - if (timeAware) { - updateDueToTime = !_.isEqual(prevMeta.timeFilters, nextMeta.timeFilters); - } - - let updateDueToRefreshTimer = false; - if (refreshTimerAware && nextMeta.refreshTimerLastTriggeredAt) { - updateDueToRefreshTimer = !_.isEqual(prevMeta.refreshTimerLastTriggeredAt, nextMeta.refreshTimerLastTriggeredAt); - } - - let updateDueToFields = false; - if (isFieldAware) { - updateDueToFields = !_.isEqual(prevMeta.fieldNames, nextMeta.fieldNames); - } - - let updateDueToQuery = false; - let updateDueToFilters = false; - let updateDueToSourceQuery = false; - let updateDueToApplyGlobalQuery = false; - if (isQueryAware) { - updateDueToApplyGlobalQuery = prevMeta.applyGlobalQuery !== nextMeta.applyGlobalQuery; - updateDueToSourceQuery = !_.isEqual(prevMeta.sourceQuery, nextMeta.sourceQuery); - if (nextMeta.applyGlobalQuery) { - updateDueToQuery = !_.isEqual(prevMeta.query, nextMeta.query); - updateDueToFilters = !_.isEqual(prevMeta.filters, nextMeta.filters); - } else { - // Global filters and query are not applied to layer search request so no re-fetch required. - // Exception is "Refresh" query. - updateDueToQuery = isRefreshOnlyQuery(prevMeta.query, nextMeta.query); - } - } - - let updateDueToPrecisionChange = false; - if (isGeoGridPrecisionAware) { - updateDueToPrecisionChange = !_.isEqual(prevMeta.geogridPrecision, nextMeta.geogridPrecision); - } - - const updateDueToExtentChange = this.updateDueToExtent(source, prevMeta, nextMeta); - - const updateDueToSourceMetaChange = !_.isEqual(prevMeta.sourceMeta, nextMeta.sourceMeta); - - return !updateDueToTime - && !updateDueToRefreshTimer - && !updateDueToExtentChange - && !updateDueToFields - && !updateDueToQuery - && !updateDueToFilters - && !updateDueToSourceQuery - && !updateDueToApplyGlobalQuery - && !updateDueToPrecisionChange - && !updateDueToSourceMetaChange; - } async _syncJoin({ join, startLoading, stopLoading, onLoadError, registerCancelCallback, dataFilters }) { const joinSource = join.getRightJoinSource(); const sourceDataId = join.getSourceId(); const requestToken = Symbol(`layer-join-refresh:${this.getId()} - ${sourceDataId}`); - const searchFilters = { ...dataFilters, fieldNames: joinSource.getFieldNames(), sourceQuery: joinSource.getWhereQuery(), applyGlobalQuery: joinSource.getApplyGlobalQuery(), }; - const canSkip = await this._canSkipSourceUpdate(joinSource, sourceDataId, searchFilters); - if (canSkip) { - const sourceDataRequest = this._findDataRequestForSource(sourceDataId); - const propertiesMap = sourceDataRequest ? sourceDataRequest.getData() : null; + const prevDataRequest = this._findDataRequestForSource(sourceDataId); + + const canSkipFetch = await canSkipSourceUpdate({ + source: joinSource, + prevDataRequest, + nextMeta: searchFilters, + }); + if (canSkipFetch) { return { dataHasChanged: false, join: join, - propertiesMap: propertiesMap + propertiesMap: prevDataRequest.getData() }; } @@ -430,12 +352,17 @@ export class VectorLayer extends AbstractLayer { const requestToken = Symbol(`layer-source-refresh:${this.getId()} - source`); const searchFilters = this._getSearchFilters(dataFilters); - const canSkip = await this._canSkipSourceUpdate(this._source, SOURCE_DATA_ID_ORIGIN, searchFilters); - if (canSkip) { - const sourceDataRequest = this.getSourceDataRequest(); + const prevDataRequest = this.getSourceDataRequest(); + + const canSkipFetch = await canSkipSourceUpdate({ + source: this._source, + prevDataRequest, + nextMeta: searchFilters, + }); + if (canSkipFetch) { return { refreshed: false, - featureCollection: sourceDataRequest.getData() + featureCollection: prevDataRequest.getData() }; } diff --git a/x-pack/legacy/plugins/maps/public/layers/vector_layer.test.js b/x-pack/legacy/plugins/maps/public/layers/vector_layer.test.js deleted file mode 100644 index 0a07582c57856d..00000000000000 --- a/x-pack/legacy/plugins/maps/public/layers/vector_layer.test.js +++ /dev/null @@ -1,185 +0,0 @@ -/* - * 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. - */ - -jest.mock('./joins/inner_join', () => ({ - InnerJoin: Object -})); - -jest.mock('./tooltips/join_tooltip_property', () => ({ - JoinTooltipProperty: Object -})); - -import { VectorLayer } from './vector_layer'; - -describe('_canSkipSourceUpdate', () => { - const SOURCE_DATA_REQUEST_ID = 'foo'; - - describe('isQueryAware', () => { - - const queryAwareSourceMock = { - isTimeAware: () => { return false; }, - isRefreshTimerAware: () => { return false; }, - isFilterByMapBounds: () => { return false; }, - isFieldAware: () => { return false; }, - isQueryAware: () => { return true; }, - isGeoGridPrecisionAware: () => { return false; }, - }; - const prevFilters = []; - const prevQuery = { - language: 'kuery', - query: 'machine.os.keyword : "win 7"', - queryLastTriggeredAt: '2019-04-25T20:53:22.331Z' - }; - - describe('applyGlobalQuery is false', () => { - - const prevApplyGlobalQuery = false; - - const vectorLayer = new VectorLayer({ - layerDescriptor: { - __dataRequests: [ - { - dataId: SOURCE_DATA_REQUEST_ID, - dataMeta: { - applyGlobalQuery: prevApplyGlobalQuery, - filters: prevFilters, - query: prevQuery, - } - } - ] - } - }); - - it('can skip update when filter changes', async () => { - const searchFilters = { - applyGlobalQuery: prevApplyGlobalQuery, - filters: [prevQuery], - query: prevQuery, - }; - - const canSkipUpdate = await vectorLayer._canSkipSourceUpdate(queryAwareSourceMock, SOURCE_DATA_REQUEST_ID, searchFilters); - - expect(canSkipUpdate).toBe(true); - }); - - it('can skip update when query changes', async () => { - const searchFilters = { - applyGlobalQuery: prevApplyGlobalQuery, - filters: prevFilters, - query: { - ...prevQuery, - query: 'a new query string', - } - }; - - const canSkipUpdate = await vectorLayer._canSkipSourceUpdate(queryAwareSourceMock, SOURCE_DATA_REQUEST_ID, searchFilters); - - expect(canSkipUpdate).toBe(true); - }); - - it('can not skip update when query is refreshed', async () => { - const searchFilters = { - applyGlobalQuery: prevApplyGlobalQuery, - filters: prevFilters, - query: { - ...prevQuery, - queryLastTriggeredAt: 'sometime layer when Refresh button is clicked' - } - }; - - const canSkipUpdate = await vectorLayer._canSkipSourceUpdate(queryAwareSourceMock, SOURCE_DATA_REQUEST_ID, searchFilters); - - expect(canSkipUpdate).toBe(false); - }); - - it('can not skip update when applyGlobalQuery changes', async () => { - const searchFilters = { - applyGlobalQuery: !prevApplyGlobalQuery, - filters: prevFilters, - query: prevQuery - }; - - const canSkipUpdate = await vectorLayer._canSkipSourceUpdate(queryAwareSourceMock, SOURCE_DATA_REQUEST_ID, searchFilters); - - expect(canSkipUpdate).toBe(false); - }); - }); - - describe('applyGlobalQuery is true', () => { - - const prevApplyGlobalQuery = true; - - const vectorLayer = new VectorLayer({ - layerDescriptor: { - __dataRequests: [ - { - dataId: SOURCE_DATA_REQUEST_ID, - dataMeta: { - applyGlobalQuery: prevApplyGlobalQuery, - filters: prevFilters, - query: prevQuery, - } - } - ] - } - }); - - it('can not skip update when filter changes', async () => { - const searchFilters = { - applyGlobalQuery: prevApplyGlobalQuery, - filters: [prevQuery], - query: prevQuery, - }; - - const canSkipUpdate = await vectorLayer._canSkipSourceUpdate(queryAwareSourceMock, SOURCE_DATA_REQUEST_ID, searchFilters); - - expect(canSkipUpdate).toBe(false); - }); - - it('can not skip update when query changes', async () => { - const searchFilters = { - applyGlobalQuery: prevApplyGlobalQuery, - filters: prevFilters, - query: { - ...prevQuery, - query: 'a new query string', - } - }; - - const canSkipUpdate = await vectorLayer._canSkipSourceUpdate(queryAwareSourceMock, SOURCE_DATA_REQUEST_ID, searchFilters); - - expect(canSkipUpdate).toBe(false); - }); - - it('can not skip update when query is refreshed', async () => { - const searchFilters = { - applyGlobalQuery: prevApplyGlobalQuery, - filters: prevFilters, - query: { - ...prevQuery, - queryLastTriggeredAt: 'sometime layer when Refresh button is clicked' - } - }; - - const canSkipUpdate = await vectorLayer._canSkipSourceUpdate(queryAwareSourceMock, SOURCE_DATA_REQUEST_ID, searchFilters); - - expect(canSkipUpdate).toBe(false); - }); - - it('can not skip update when applyGlobalQuery changes', async () => { - const searchFilters = { - applyGlobalQuery: !prevApplyGlobalQuery, - filters: prevFilters, - query: prevQuery - }; - - const canSkipUpdate = await vectorLayer._canSkipSourceUpdate(queryAwareSourceMock, SOURCE_DATA_REQUEST_ID, searchFilters); - - expect(canSkipUpdate).toBe(false); - }); - }); - }); -}); From f7d9e7bbf6ddd494ce94575c10238fea12ea0436 Mon Sep 17 00:00:00 2001 From: Frank Hassanabad Date: Tue, 26 Nov 2019 08:10:25 -0700 Subject: [PATCH 02/12] [SIEM][Detection Engine] Disambiguates signals, rules, alerts, and detection engine by renaming them (#51684) ## Summary * Renames `signals -> rules` when it is specific about rules * Renames `signals -> detection engine` when is generically talking about both rules and signals * Renames `signals -> alerts` in a few spots when it is talking specifically about alerting plugins * Keeps the name of signal when it involves the signals output index or a source input index for potential signals to be generated from * Did a `git mv ` for everything * Updated local variables as well per rules above. ### Checklist Use ~~strikethroughs~~ to remove checklist items you don't feel are applicable to this PR. ~~- [ ] This was checked for cross-browser compatibility, [including a check against IE11](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#cross-browser-compatibility)~~ ~~- [ ] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/master/packages/kbn-i18n/README.md)~~ ~~- [ ] [Documentation](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#writing-documentation) was added for features that require explanation or tutorials~~ - [ ] [Unit or functional tests](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#cross-browser-compatibility) were updated or added to match the most common scenarios ~~- [ ] This was checked for [keyboard-only and screenreader accessibility](https://developer.mozilla.org/en-US/docs/Learn/Tools_and_testing/Cross_browser_testing/Accessibility#Accessibility_testing_checklist)~~ ### For maintainers ~~- [ ] This was checked for breaking API changes and was [labeled appropriately](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#release-notes-process)~~ - [ ] This includes a feature addition or change that requires a release note and was [labeled appropriately](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#release-notes-process) --- ...ls.js => convert_saved_search_to_rules.js} | 26 +- .../plugins/siem/server/kibana.index.ts | 26 +- .../server/lib/detection_engine/README.md | 18 +- .../alerts/__mocks__/es_results.ts | 8 +- .../{create_signals.ts => create_rules.ts} | 6 +- .../{delete_signals.ts => delete_rules.ts} | 18 +- ...ind_signals.test.ts => find_rules.test.ts} | 4 +- .../alerts/{find_signals.ts => find_rules.ts} | 6 +- .../lib/detection_engine/alerts/get_filter.ts | 6 +- ...ead_signals.test.ts => read_rules.test.ts} | 70 ++-- .../alerts/{read_signals.ts => read_rules.ts} | 50 +-- ...nals_alert_type.ts => rules_alert_type.ts} | 8 +- .../lib/detection_engine/alerts/types.ts | 74 ++-- ...e_signals.test.ts => update_rules.test.ts} | 10 +- .../{update_signals.ts => update_rules.ts} | 40 +- .../lib/detection_engine/alerts/utils.test.ts | 94 ++--- .../lib/detection_engine/alerts/utils.ts | 93 +++-- .../routes/__mocks__/request_responses.ts | 12 +- ...ute.test.ts => create_rules_route.test.ts} | 14 +- ...signals_route.ts => create_rules_route.ts} | 28 +- ...ute.test.ts => delete_rules_route.test.ts} | 18 +- ...signals_route.ts => delete_rules_route.ts} | 18 +- ...route.test.ts => find_rules_route.test.ts} | 14 +- ...d_signals_route.ts => find_rules_route.ts} | 20 +- ...route.test.ts => read_rules_route.test.ts} | 12 +- ...d_signals_route.ts => read_rules_route.ts} | 18 +- .../detection_engine/routes/schemas.test.ts | 343 ++++++++---------- .../lib/detection_engine/routes/schemas.ts | 8 +- ...ute.test.ts => update_rules_route.test.ts} | 16 +- ...signals_route.ts => update_rules_route.ts} | 22 +- .../lib/detection_engine/routes/utils.test.ts | 54 +-- .../lib/detection_engine/routes/utils.ts | 66 ++-- .../lib/detection_engine/scripts/README.md | 7 +- ...ls.sh => convert_saved_search_to_rules.sh} | 2 +- ...e_signal_by_id.sh => delete_rule_by_id.sh} | 2 +- ...y_rule_id.sh => delete_rule_by_rule_id.sh} | 2 +- ...al_by_filter.sh => find_rule_by_filter.sh} | 4 +- .../{find_signals.sh => find_rules.sh} | 2 +- ...ind_signals_sort.sh => find_rules_sort.sh} | 2 +- ...{get_signal_by_id.sh => get_rule_by_id.sh} | 2 +- ...l_by_rule_id.sh => get_rule_by_rule_id.sh} | 2 +- .../scripts/{post_signal.sh => post_rule.sh} | 14 +- .../{post_x_signals.sh => post_x_rules.sh} | 4 +- .../filter_with_empty_query.json | 0 .../filter_without_query.json | 0 .../{signals => rules}/root_or_admin_1.json | 0 .../{signals => rules}/root_or_admin_10.json | 0 .../{signals => rules}/root_or_admin_2.json | 0 .../{signals => rules}/root_or_admin_3.json | 0 .../{signals => rules}/root_or_admin_4.json | 0 .../{signals => rules}/root_or_admin_5.json | 0 .../{signals => rules}/root_or_admin_6.json | 0 .../{signals => rules}/root_or_admin_7.json | 0 .../{signals => rules}/root_or_admin_8.json | 0 .../{signals => rules}/root_or_admin_9.json | 0 .../root_or_admin_filter_9998.json | 0 .../root_or_admin_filter_9999.json | 0 .../root_or_admin_meta.json | 0 .../root_or_admin_saved_query_1.json | 0 .../root_or_admin_saved_query_2.json | 0 .../root_or_admin_saved_query_3.json | 0 .../root_or_admin_update_1.json | 0 .../root_or_admin_update_2.json | 0 .../{signals => rules}/watch_longmont.json | 0 .../{update_signal.sh => update_rule.sh} | 14 +- .../legacy/plugins/siem/server/lib/types.ts | 4 +- 66 files changed, 630 insertions(+), 651 deletions(-) rename x-pack/legacy/plugins/siem/scripts/{convert_saved_search_to_signals.js => convert_saved_search_to_rules.js} (84%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/{create_signals.ts => create_rules.ts} (92%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/{delete_signals.ts => delete_rules.ts} (65%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/{find_signals.test.ts => find_rules.test.ts} (90%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/{find_signals.ts => find_rules.ts} (88%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/{read_signals.test.ts => read_rules.test.ts} (82%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/{read_signals.ts => read_rules.ts} (50%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/{signals_alert_type.ts => rules_alert_type.ts} (97%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/{update_signals.test.ts => update_rules.test.ts} (80%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/{update_signals.ts => update_rules.ts} (66%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/{create_signals_route.test.ts => create_rules_route.test.ts} (92%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/{create_signals_route.ts => create_rules_route.ts} (72%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/{delete_signals_route.test.ts => delete_rules_route.test.ts} (83%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/{delete_signals_route.ts => delete_rules_route.ts} (75%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/{find_signals_route.test.ts => find_rules_route.test.ts} (89%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/{find_signals_route.ts => find_rules_route.ts} (70%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/{read_signals_route.test.ts => read_rules_route.test.ts} (88%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/{read_signals_route.ts => read_rules_route.ts} (75%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/{update_signals_route.test.ts => update_rules_route.test.ts} (92%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/{update_signals_route.ts => update_rules_route.ts} (78%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/{convert_saved_search_to_signals.sh => convert_saved_search_to_rules.sh} (80%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/{delete_signal_by_id.sh => delete_rule_by_id.sh} (91%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/{delete_signal_by_rule_id.sh => delete_rule_by_rule_id.sh} (89%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/{find_signal_by_filter.sh => find_rule_by_filter.sh} (81%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/{find_signals.sh => find_rules.sh} (93%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/{find_signals_sort.sh => find_rules_sort.sh} (91%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/{get_signal_by_id.sh => get_rule_by_id.sh} (90%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/{get_signal_by_rule_id.sh => get_rule_by_rule_id.sh} (90%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/{post_signal.sh => post_rule.sh} (68%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/{post_x_signals.sh => post_x_rules.sh} (94%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/{signals => rules}/filter_with_empty_query.json (100%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/{signals => rules}/filter_without_query.json (100%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/{signals => rules}/root_or_admin_1.json (100%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/{signals => rules}/root_or_admin_10.json (100%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/{signals => rules}/root_or_admin_2.json (100%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/{signals => rules}/root_or_admin_3.json (100%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/{signals => rules}/root_or_admin_4.json (100%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/{signals => rules}/root_or_admin_5.json (100%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/{signals => rules}/root_or_admin_6.json (100%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/{signals => rules}/root_or_admin_7.json (100%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/{signals => rules}/root_or_admin_8.json (100%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/{signals => rules}/root_or_admin_9.json (100%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/{signals => rules}/root_or_admin_filter_9998.json (100%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/{signals => rules}/root_or_admin_filter_9999.json (100%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/{signals => rules}/root_or_admin_meta.json (100%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/{signals => rules}/root_or_admin_saved_query_1.json (100%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/{signals => rules}/root_or_admin_saved_query_2.json (100%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/{signals => rules}/root_or_admin_saved_query_3.json (100%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/{signals => rules}/root_or_admin_update_1.json (100%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/{signals => rules}/root_or_admin_update_2.json (100%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/{signals => rules}/watch_longmont.json (100%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/{update_signal.sh => update_rule.sh} (67%) diff --git a/x-pack/legacy/plugins/siem/scripts/convert_saved_search_to_signals.js b/x-pack/legacy/plugins/siem/scripts/convert_saved_search_to_rules.js similarity index 84% rename from x-pack/legacy/plugins/siem/scripts/convert_saved_search_to_signals.js rename to x-pack/legacy/plugins/siem/scripts/convert_saved_search_to_rules.js index 263a2a59de31f5..3e1c5f51ebb5c8 100644 --- a/x-pack/legacy/plugins/siem/scripts/convert_saved_search_to_signals.js +++ b/x-pack/legacy/plugins/siem/scripts/convert_saved_search_to_rules.js @@ -11,22 +11,22 @@ const path = require('path'); /* * This script is used to parse a set of saved searches on a file system - * and output signal data compatible json files. + * and output rule data compatible json files. * Example: - * node saved_query_to_signals.js ${HOME}/saved_searches ${HOME}/saved_signals + * node saved_query_to_rules.js ${HOME}/saved_searches ${HOME}/saved_rules * - * After editing any changes in the files of ${HOME}/saved_signals/*.json - * you can then post the signals with a CURL post script such as: + * After editing any changes in the files of ${HOME}/saved_rules/*.json + * you can then post the rules with a CURL post script such as: * - * ./post_signal.sh ${HOME}/saved_signals/*.json + * ./post_rule.sh ${HOME}/saved_rules/*.json * * Note: This script is recursive and but does not preserve folder structure - * when it outputs the saved signals. + * when it outputs the saved rules. */ -// Defaults of the outputted signals since the saved KQL searches do not have +// Defaults of the outputted rules since the saved KQL searches do not have // this type of information. You usually will want to make any hand edits after -// doing a search to KQL conversion before posting it as a signal or checking it +// doing a search to KQL conversion before posting it as a rule or checking it // into another repository. const INTERVAL = '5m'; const SEVERITY = 'low'; @@ -36,8 +36,8 @@ const TO = 'now'; const IMMUTABLE = true; const RISK_SCORE = 50; const ENABLED = false; -let allSignals = ''; -const allSignalsNdJson = 'all_rules.ndjson'; +let allRules = ''; +const allRulesNdJson = 'all_rules.ndjson'; // For converting, if you want to use these instead of rely on the defaults then // comment these in and use them for the script. Otherwise this is commented out @@ -74,7 +74,7 @@ const cleanupFileName = file => { async function main() { if (process.argv.length !== 4) { throw new Error( - 'usage: saved_query_to_signals [input directory with saved searches] [output directory]' + 'usage: saved_query_to_rules [input directory with saved searches] [output directory]' ); } @@ -152,11 +152,11 @@ async function main() { `${outputDir}/${fileToWrite}.json`, JSON.stringify(outputMessage, null, 2) ); - allSignals += `${JSON.stringify(outputMessage)}\n`; + allRules += `${JSON.stringify(outputMessage)}\n`; } } ); - fs.writeFileSync(`${outputDir}/${allSignalsNdJson}`, allSignals); + fs.writeFileSync(`${outputDir}/${allRulesNdJson}`, allRules); } if (require.main === module) { diff --git a/x-pack/legacy/plugins/siem/server/kibana.index.ts b/x-pack/legacy/plugins/siem/server/kibana.index.ts index a92bca064dab9c..2f1530a777042a 100644 --- a/x-pack/legacy/plugins/siem/server/kibana.index.ts +++ b/x-pack/legacy/plugins/siem/server/kibana.index.ts @@ -15,13 +15,13 @@ import { timelineSavedObjectType, } from './saved_objects'; -import { signalsAlertType } from './lib/detection_engine/alerts/signals_alert_type'; +import { rulesAlertType } from './lib/detection_engine/alerts/rules_alert_type'; import { isAlertExecutor } from './lib/detection_engine/alerts/types'; -import { createSignalsRoute } from './lib/detection_engine/routes/create_signals_route'; -import { readSignalsRoute } from './lib/detection_engine/routes/read_signals_route'; -import { findSignalsRoute } from './lib/detection_engine/routes/find_signals_route'; -import { deleteSignalsRoute } from './lib/detection_engine/routes/delete_signals_route'; -import { updateSignalsRoute } from './lib/detection_engine/routes/update_signals_route'; +import { createRulesRoute } from './lib/detection_engine/routes/create_rules_route'; +import { readRulesRoute } from './lib/detection_engine/routes/read_rules_route'; +import { findRulesRoute } from './lib/detection_engine/routes/find_rules_route'; +import { deleteRulesRoute } from './lib/detection_engine/routes/delete_rules_route'; +import { updateRulesRoute } from './lib/detection_engine/routes/update_rules_route'; import { ServerFacade } from './types'; const APP_ID = 'siem'; @@ -33,7 +33,7 @@ export const initServerWithKibana = ( ) => { if (kbnServer.plugins.alerting != null) { const version = kbnServer.config().get('pkg.version'); - const type = signalsAlertType({ logger, version }); + const type = rulesAlertType({ logger, version }); if (isAlertExecutor(type)) { kbnServer.plugins.alerting.setup.registerType(type); } @@ -49,13 +49,13 @@ export const initServerWithKibana = ( kbnServer.config().has('xpack.alerting.enabled') === true ) { logger.info( - 'Detected feature flags for actions and alerting and enabling signals API endpoints' + 'Detected feature flags for actions and alerting and enabling detection engine API endpoints' ); - createSignalsRoute(kbnServer); - readSignalsRoute(kbnServer); - updateSignalsRoute(kbnServer); - deleteSignalsRoute(kbnServer); - findSignalsRoute(kbnServer); + createRulesRoute(kbnServer); + readRulesRoute(kbnServer); + updateRulesRoute(kbnServer); + deleteRulesRoute(kbnServer); + findRulesRoute(kbnServer); } const xpackMainPlugin = kbnServer.plugins.xpack_main; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/README.md b/x-pack/legacy/plugins/siem/server/lib/detection_engine/README.md index 5d9d87a1cbc2fd..4b1dbf62d0dd4c 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/README.md +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/README.md @@ -24,10 +24,10 @@ xpack.alerting.enabled: true xpack.actions.enabled: true ``` -Start Kibana and you will see these messages indicating signals is activated like so: +Start Kibana and you will see these messages indicating detection engine is activated like so: ```sh -server log [11:39:05.561] [info][siem] Detected feature flags for actions and alerting and enabling signals API endpoints +server log [11:39:05.561] [info][siem] Detected feature flags for actions and alerting and enabling detection engine API endpoints ``` If you see crashes like this: @@ -98,10 +98,10 @@ server log [22:05:22.277] [info][status][plugin:alerting@8.0.0] Status chan server log [22:05:22.270] [info][status][plugin:actions@8.0.0] Status changed from uninitialized to green - Ready ``` -You should also see the SIEM detect the feature flags and start the API endpoints for signals +You should also see the SIEM detect the feature flags and start the API endpoints for detection engine ```sh -server log [11:39:05.561] [info][siem] Detected feature flags for actions and alerting and enabling signals API endpoints +server log [11:39:05.561] [info][siem] Detected feature flags for actions and alerting and enabling detection engine API endpoints ``` Go into your SIEM Advanced settings and underneath the setting of `siem:defaultSignalsIndex`, set that to the same @@ -125,16 +125,16 @@ which will: - Delete any existing alert tasks you have - Delete any existing signal mapping you might have had. - Add the latest signal index and its mappings using your settings from `SIGNALS_INDEX` environment variable. -- Posts the sample signal from `signals/root_or_admin_1.json` by replacing its `output_index` with your `SIGNALS_INDEX` environment variable -- The sample signal checks for root or admin every 5 minutes and reports that as a signal if it is a positive hit +- Posts the sample rule from `rules/root_or_admin_1.json` by replacing its `output_index` with your `SIGNALS_INDEX` environment variable +- The sample rule checks for root or admin every 5 minutes and reports that as a signal if it is a positive hit Now you can run ```sh -./find_signals.sh +./find_rules.sh ``` -You should see the new signals created like so: +You should see the new rules created like so: ```sh { @@ -184,7 +184,7 @@ Every 5 minutes if you get positive hits you will see messages on info like so: server log [09:54:59.013] [info][plugins][siem] Total signals found from signal rule "id: a556065c-0656-4ba1-ad64-a77ca9d2013b", "ruleId: rule-1": 10000 ``` -Signals are space aware and default to the "default" space for these scripts if you do not export +Rules are space aware and default to the "default" space for these scripts if you do not export the variable of SPACE_URL. For example, if you want to post rules to the space `test-space` you would set your SPACE_URL to be: diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/__mocks__/es_results.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/__mocks__/es_results.ts index 7d3b51c071c091..079d3658461fa5 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/__mocks__/es_results.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/__mocks__/es_results.ts @@ -4,12 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SignalSourceHit, SignalSearchResponse, AlertTypeParams } from '../types'; +import { SignalSourceHit, SignalSearchResponse, RuleTypeParams } from '../types'; -export const sampleSignalAlertParams = ( +export const sampleRuleAlertParams = ( maxSignals: number | undefined, riskScore?: number | undefined -): AlertTypeParams => ({ +): RuleTypeParams => ({ ruleId: 'rule-1', description: 'Detecting root and admin users', falsePositives: [], @@ -242,4 +242,4 @@ export const sampleDocSearchResultsWithSortId = (someUuid: string): SignalSearch }, }); -export const sampleSignalId = '04128c15-0d1b-4716-a4c5-46997ac7f3bd'; +export const sampleRuleGuid = '04128c15-0d1b-4716-a4c5-46997ac7f3bd'; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/create_signals.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/create_rules.ts similarity index 92% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/create_signals.ts rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/create_rules.ts index 8770282356cf56..7c66714484383f 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/create_signals.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/create_rules.ts @@ -5,9 +5,9 @@ */ import { SIGNALS_ID } from '../../../../common/constants'; -import { SignalParams } from './types'; +import { RuleParams } from './types'; -export const createSignals = async ({ +export const createRules = async ({ alertsClient, actionsClient, // TODO: Use this actionsClient once we have actions such as email, etc... description, @@ -33,7 +33,7 @@ export const createSignals = async ({ to, type, references, -}: SignalParams) => { +}: RuleParams) => { return alertsClient.create({ data: { name, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/delete_signals.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/delete_rules.ts similarity index 65% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/delete_signals.ts rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/delete_rules.ts index d89895772f1efc..c3ca1d79424cf2 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/delete_signals.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/delete_rules.ts @@ -4,27 +4,27 @@ * you may not use this file except in compliance with the Elastic License. */ -import { readSignals } from './read_signals'; -import { DeleteSignalParams } from './types'; +import { readRules } from './read_rules'; +import { DeleteRuleParams } from './types'; -export const deleteSignals = async ({ +export const deleteRules = async ({ alertsClient, actionsClient, // TODO: Use this when we have actions such as email, etc... id, ruleId, -}: DeleteSignalParams) => { - const signal = await readSignals({ alertsClient, id, ruleId }); - if (signal == null) { +}: DeleteRuleParams) => { + const rule = await readRules({ alertsClient, id, ruleId }); + if (rule == null) { return null; } if (ruleId != null) { - await alertsClient.delete({ id: signal.id }); - return signal; + await alertsClient.delete({ id: rule.id }); + return rule; } else if (id != null) { try { await alertsClient.delete({ id }); - return signal; + return rule; } catch (err) { if (err.output.statusCode === 404) { return null; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/find_signals.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/find_rules.test.ts similarity index 90% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/find_signals.test.ts rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/find_rules.test.ts index 7873781fb05c47..23f031b22a9dd8 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/find_signals.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/find_rules.test.ts @@ -4,10 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getFilter } from './find_signals'; +import { getFilter } from './find_rules'; import { SIGNALS_ID } from '../../../../common/constants'; -describe('find_signals', () => { +describe('find_rules', () => { test('it returns a full filter with an AND if sent down', () => { expect(getFilter('alert.attributes.enabled: true')).toEqual( `alert.attributes.alertTypeId: ${SIGNALS_ID} AND alert.attributes.enabled: true` diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/find_signals.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/find_rules.ts similarity index 88% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/find_signals.ts rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/find_rules.ts index 63e6a069c0cfe1..c1058bd353e8ce 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/find_signals.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/find_rules.ts @@ -5,7 +5,7 @@ */ import { SIGNALS_ID } from '../../../../common/constants'; -import { FindSignalParams } from './types'; +import { FindRuleParams } from './types'; export const getFilter = (filter: string | null | undefined) => { if (filter == null) { @@ -15,7 +15,7 @@ export const getFilter = (filter: string | null | undefined) => { } }; -export const findSignals = async ({ +export const findRules = async ({ alertsClient, perPage, page, @@ -23,7 +23,7 @@ export const findSignals = async ({ filter, sortField, sortOrder, -}: FindSignalParams) => { +}: FindRuleParams) => { return alertsClient.find({ options: { fields, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/get_filter.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/get_filter.ts index 1aa22ea024cc85..5d3b47ecebfd51 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/get_filter.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/get_filter.ts @@ -5,7 +5,7 @@ */ import { AlertServices } from '../../../../../alerting/server/types'; -import { SignalAlertParams, PartialFilter } from './types'; +import { RuleAlertParams, PartialFilter } from './types'; import { assertUnreachable } from '../../../utils/build_query'; import { Query, @@ -41,7 +41,7 @@ export const getQueryFilter = ( }; interface GetFilterArgs { - type: SignalAlertParams['type']; + type: RuleAlertParams['type']; filter: Record | undefined | null; filters: PartialFilter[] | undefined | null; language: string | undefined | null; @@ -86,7 +86,7 @@ export const getFilter = async ({ if (query != null && language != null && index != null) { return getQueryFilter(query, language, filters || [], index); } else { - // user did not give any additional fall back mechanism for generating a signal + // user did not give any additional fall back mechanism for generating a rule // rethrow error for activity monitoring throw err; } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/read_signals.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/read_rules.test.ts similarity index 82% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/read_signals.test.ts rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/read_rules.test.ts index 39d1fac8f7a09f..b3d7ab13227750 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/read_signals.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/read_rules.test.ts @@ -5,7 +5,7 @@ */ import { alertsClientMock } from '../../../../../alerting/server/alerts_client.mock'; -import { readSignals, readSignalByRuleId, findSignalInArrayByRuleId } from './read_signals'; +import { readRules, readRuleByRuleId, findRuleInArrayByRuleId } from './read_rules'; import { AlertsClient } from '../../../../../alerting'; import { getResult, @@ -14,19 +14,19 @@ import { } from '../routes/__mocks__/request_responses'; import { SIGNALS_ID } from '../../../../common/constants'; -describe('read_signals', () => { - describe('readSignals', () => { +describe('read_rules', () => { + describe('readRules', () => { test('should return the output from alertsClient if id is set but ruleId is undefined', async () => { const alertsClient = alertsClientMock.create(); alertsClient.get.mockResolvedValue(getResult()); const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const signal = await readSignals({ + const rule = await readRules({ alertsClient: unsafeCast, id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', ruleId: undefined, }); - expect(signal).toEqual(getResult()); + expect(rule).toEqual(getResult()); }); test('should return the output from alertsClient if id is set but ruleId is null', async () => { @@ -34,12 +34,12 @@ describe('read_signals', () => { alertsClient.get.mockResolvedValue(getResult()); const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const signal = await readSignals({ + const rule = await readRules({ alertsClient: unsafeCast, id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', ruleId: null, }); - expect(signal).toEqual(getResult()); + expect(rule).toEqual(getResult()); }); test('should return the output from alertsClient if id is undefined but ruleId is set', async () => { @@ -48,12 +48,12 @@ describe('read_signals', () => { alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const signal = await readSignals({ + const rule = await readRules({ alertsClient: unsafeCast, id: undefined, ruleId: 'rule-1', }); - expect(signal).toEqual(getResult()); + expect(rule).toEqual(getResult()); }); test('should return the output from alertsClient if id is null but ruleId is set', async () => { @@ -62,12 +62,12 @@ describe('read_signals', () => { alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const signal = await readSignals({ + const rule = await readRules({ alertsClient: unsafeCast, id: null, ruleId: 'rule-1', }); - expect(signal).toEqual(getResult()); + expect(rule).toEqual(getResult()); }); test('should return null if id and ruleId are null', async () => { @@ -76,12 +76,12 @@ describe('read_signals', () => { alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const signal = await readSignals({ + const rule = await readRules({ alertsClient: unsafeCast, id: null, ruleId: null, }); - expect(signal).toEqual(null); + expect(rule).toEqual(null); }); test('should return null if id and ruleId are undefined', async () => { @@ -90,27 +90,27 @@ describe('read_signals', () => { alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const signal = await readSignals({ + const rule = await readRules({ alertsClient: unsafeCast, id: undefined, ruleId: undefined, }); - expect(signal).toEqual(null); + expect(rule).toEqual(null); }); }); - describe('readSignalByRuleId', () => { + describe('readRuleByRuleId', () => { test('should return a single value if the rule id matches', async () => { const alertsClient = alertsClientMock.create(); alertsClient.get.mockResolvedValue(getResult()); alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const signal = await readSignalByRuleId({ + const rule = await readRuleByRuleId({ alertsClient: unsafeCast, ruleId: 'rule-1', }); - expect(signal).toEqual(getResult()); + expect(rule).toEqual(getResult()); }); test('should not return a single value if the rule id does not match', async () => { @@ -119,11 +119,11 @@ describe('read_signals', () => { alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const signal = await readSignalByRuleId({ + const rule = await readRuleByRuleId({ alertsClient: unsafeCast, ruleId: 'rule-that-should-not-match-anything', }); - expect(signal).toEqual(null); + expect(rule).toEqual(null); }); test('should return a single value of rule-1 with multiple values', async () => { @@ -140,11 +140,11 @@ describe('read_signals', () => { alertsClient.find.mockResolvedValue(getFindResultWithMultiHits([result1, result2])); const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const signal = await readSignalByRuleId({ + const rule = await readRuleByRuleId({ alertsClient: unsafeCast, ruleId: 'rule-1', }); - expect(signal).toEqual(result1); + expect(rule).toEqual(result1); }); test('should return a single value of rule-2 with multiple values', async () => { @@ -161,11 +161,11 @@ describe('read_signals', () => { alertsClient.find.mockResolvedValue(getFindResultWithMultiHits([result1, result2])); const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const signal = await readSignalByRuleId({ + const rule = await readRuleByRuleId({ alertsClient: unsafeCast, ruleId: 'rule-2', }); - expect(signal).toEqual(result2); + expect(rule).toEqual(result2); }); test('should return null for a made up value with multiple values', async () => { @@ -182,57 +182,57 @@ describe('read_signals', () => { alertsClient.find.mockResolvedValue(getFindResultWithMultiHits([result1, result2])); const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const signal = await readSignalByRuleId({ + const rule = await readRuleByRuleId({ alertsClient: unsafeCast, ruleId: 'rule-that-should-not-match-anything', }); - expect(signal).toEqual(null); + expect(rule).toEqual(null); }); }); - describe('findSignalInArrayByRuleId', () => { + describe('findRuleInArrayByRuleId', () => { test('returns null if the objects are not of a signal rule type', () => { - const signal = findSignalInArrayByRuleId( + const rule = findRuleInArrayByRuleId( [ { alertTypeId: 'made up 1', params: { ruleId: '123' } }, { alertTypeId: 'made up 2', params: { ruleId: '456' } }, ], '123' ); - expect(signal).toEqual(null); + expect(rule).toEqual(null); }); test('returns correct type if the objects are of a signal rule type', () => { - const signal = findSignalInArrayByRuleId( + const rule = findRuleInArrayByRuleId( [ { alertTypeId: SIGNALS_ID, params: { ruleId: '123' } }, { alertTypeId: 'made up 2', params: { ruleId: '456' } }, ], '123' ); - expect(signal).toEqual({ alertTypeId: 'siem.signals', params: { ruleId: '123' } }); + expect(rule).toEqual({ alertTypeId: 'siem.signals', params: { ruleId: '123' } }); }); test('returns second correct type if the objects are of a signal rule type', () => { - const signal = findSignalInArrayByRuleId( + const rule = findRuleInArrayByRuleId( [ { alertTypeId: SIGNALS_ID, params: { ruleId: '123' } }, { alertTypeId: SIGNALS_ID, params: { ruleId: '456' } }, ], '456' ); - expect(signal).toEqual({ alertTypeId: 'siem.signals', params: { ruleId: '456' } }); + expect(rule).toEqual({ alertTypeId: 'siem.signals', params: { ruleId: '456' } }); }); test('returns null with correct types but data does not exist', () => { - const signal = findSignalInArrayByRuleId( + const rule = findRuleInArrayByRuleId( [ { alertTypeId: SIGNALS_ID, params: { ruleId: '123' } }, { alertTypeId: SIGNALS_ID, params: { ruleId: '456' } }, ], '892' ); - expect(signal).toEqual(null); + expect(rule).toEqual(null); }); }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/read_signals.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/read_rules.ts similarity index 50% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/read_signals.ts rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/read_rules.ts index 3c49112aaf50b5..5c335263290163 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/read_signals.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/read_rules.ts @@ -4,20 +4,20 @@ * you may not use this file except in compliance with the Elastic License. */ -import { findSignals } from './find_signals'; -import { SignalAlertType, isAlertTypeArray, ReadSignalParams, ReadSignalByRuleId } from './types'; +import { findRules } from './find_rules'; +import { RuleAlertType, isAlertTypeArray, ReadRuleParams, ReadRuleByRuleId } from './types'; -export const findSignalInArrayByRuleId = ( +export const findRuleInArrayByRuleId = ( objects: object[], ruleId: string -): SignalAlertType | null => { +): RuleAlertType | null => { if (isAlertTypeArray(objects)) { - const signals: SignalAlertType[] = objects; - const signal: SignalAlertType[] = signals.filter(datum => { + const rules: RuleAlertType[] = objects; + const rule: RuleAlertType[] = rules.filter(datum => { return datum.params.ruleId === ruleId; }); - if (signal.length !== 0) { - return signal[0]; + if (rule.length !== 0) { + return rule[0]; } else { return null; } @@ -26,32 +26,32 @@ export const findSignalInArrayByRuleId = ( } }; -// This an extremely slow and inefficient way of getting a signal by its id. -// I have to manually query every single record since the Signal Params are +// This an extremely slow and inefficient way of getting a rule by its id. +// I have to manually query every single record since the rule Params are // not indexed and I cannot push in my own _id when I create an alert at the moment. // TODO: Once we can directly push in the _id, then we should no longer need this way. // TODO: This is meant to be _very_ temporary. -export const readSignalByRuleId = async ({ +export const readRuleByRuleId = async ({ alertsClient, ruleId, -}: ReadSignalByRuleId): Promise => { - const firstSignals = await findSignals({ alertsClient, page: 1 }); - const firstSignal = findSignalInArrayByRuleId(firstSignals.data, ruleId); - if (firstSignal != null) { - return firstSignal; +}: ReadRuleByRuleId): Promise => { + const firstRules = await findRules({ alertsClient, page: 1 }); + const firstRule = findRuleInArrayByRuleId(firstRules.data, ruleId); + if (firstRule != null) { + return firstRule; } else { - const totalPages = Math.ceil(firstSignals.total / firstSignals.perPage); + const totalPages = Math.ceil(firstRules.total / firstRules.perPage); return Array(totalPages) .fill({}) .map((_, page) => { // page index never starts at zero. It always has to be 1 or greater - return findSignals({ alertsClient, page: page + 1 }); + return findRules({ alertsClient, page: page + 1 }); }) - .reduce>(async (accum, findSignal) => { - const signals = await findSignal; - const signal = findSignalInArrayByRuleId(signals.data, ruleId); - if (signal != null) { - return signal; + .reduce>(async (accum, findRule) => { + const rules = await findRule; + const rule = findRuleInArrayByRuleId(rules.data, ruleId); + if (rule != null) { + return rule; } else { return accum; } @@ -59,7 +59,7 @@ export const readSignalByRuleId = async ({ } }; -export const readSignals = async ({ alertsClient, id, ruleId }: ReadSignalParams) => { +export const readRules = async ({ alertsClient, id, ruleId }: ReadRuleParams) => { if (id != null) { try { const output = await alertsClient.get({ id }); @@ -73,7 +73,7 @@ export const readSignals = async ({ alertsClient, id, ruleId }: ReadSignalParams } } } else if (ruleId != null) { - return readSignalByRuleId({ alertsClient, ruleId }); + return readRuleByRuleId({ alertsClient, ruleId }); } else { // should never get here, and yet here we are. return null; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/signals_alert_type.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/rules_alert_type.ts similarity index 97% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/signals_alert_type.ts rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/rules_alert_type.ts index 69eb3eb6650608..91d7d18a4945cd 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/signals_alert_type.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/rules_alert_type.ts @@ -14,17 +14,17 @@ import { import { buildEventsSearchQuery } from './build_events_query'; import { searchAfterAndBulkCreate } from './utils'; -import { SignalAlertTypeDefinition } from './types'; +import { RuleAlertTypeDefinition } from './types'; import { getFilter } from './get_filter'; import { getInputOutputIndex } from './get_input_output_index'; -export const signalsAlertType = ({ +export const rulesAlertType = ({ logger, version, }: { logger: Logger; version: string; -}): SignalAlertTypeDefinition => { +}): RuleAlertTypeDefinition => { return { id: SIGNALS_ID, name: 'SIEM Signals', @@ -127,7 +127,7 @@ export const signalsAlertType = ({ const bulkIndexResult = await searchAfterAndBulkCreate({ someResult: noReIndexResult, - signalParams: params, + ruleParams: params, services, logger, id: alertId, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/types.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/types.ts index 29eb7872f163dc..28431b81652660 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/types.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/types.ts @@ -21,7 +21,7 @@ import { esFilters } from '../../../../../../../../src/plugins/data/server'; export type PartialFilter = Partial; -export interface SignalAlertParams { +export interface RuleAlertParams { description: string; enabled: boolean; falsePositives: string[]; @@ -47,31 +47,31 @@ export interface SignalAlertParams { type: 'filter' | 'query' | 'saved_query'; } -export type SignalAlertParamsRest = Omit< - SignalAlertParams, +export type RuleAlertParamsRest = Omit< + RuleAlertParams, 'ruleId' | 'falsePositives' | 'maxSignals' | 'savedId' | 'riskScore' | 'outputIndex' > & { - rule_id: SignalAlertParams['ruleId']; - false_positives: SignalAlertParams['falsePositives']; - saved_id: SignalAlertParams['savedId']; - max_signals: SignalAlertParams['maxSignals']; - risk_score: SignalAlertParams['riskScore']; - output_index: SignalAlertParams['outputIndex']; + rule_id: RuleAlertParams['ruleId']; + false_positives: RuleAlertParams['falsePositives']; + saved_id: RuleAlertParams['savedId']; + max_signals: RuleAlertParams['maxSignals']; + risk_score: RuleAlertParams['riskScore']; + output_index: RuleAlertParams['outputIndex']; }; -export type OutputSignalAlertRest = SignalAlertParamsRest & { +export type OutputRuleAlertRest = RuleAlertParamsRest & { id: string; created_by: string | undefined | null; updated_by: string | undefined | null; }; -export type OutputSignalES = OutputSignalAlertRest & { +export type OutputRuleES = OutputRuleAlertRest & { status: 'open' | 'closed'; }; -export type UpdateSignalAlertParamsRest = Partial & { +export type UpdateRuleAlertParamsRest = Partial & { id: string | undefined; - rule_id: SignalAlertParams['ruleId'] | undefined; + rule_id: RuleAlertParams['ruleId'] | undefined; }; export interface FindParamsRest { @@ -88,18 +88,18 @@ export interface Clients { actionsClient: ActionsClient; } -export type SignalParams = SignalAlertParams & Clients; +export type RuleParams = RuleAlertParams & Clients; -export type UpdateSignalParams = Partial & { +export type UpdateRuleParams = Partial & { id: string | undefined | null; } & Clients; -export type DeleteSignalParams = Clients & { +export type DeleteRuleParams = Clients & { id: string | undefined; ruleId: string | undefined | null; }; -export interface FindSignalsRequest extends Omit { +export interface FindRulesRequest extends Omit { query: { per_page: number; page: number; @@ -111,7 +111,7 @@ export interface FindSignalsRequest extends Omit { }; } -export interface FindSignalParams { +export interface FindRuleParams { alertsClient: AlertsClient; perPage?: number; page?: number; @@ -121,34 +121,34 @@ export interface FindSignalParams { sortOrder?: 'asc' | 'desc'; } -export interface ReadSignalParams { +export interface ReadRuleParams { alertsClient: AlertsClient; id?: string | undefined | null; ruleId?: string | undefined | null; } -export interface ReadSignalByRuleId { +export interface ReadRuleByRuleId { alertsClient: AlertsClient; ruleId: string; } -export type AlertTypeParams = Omit; +export type RuleTypeParams = Omit; -export type SignalAlertType = Alert & { +export type RuleAlertType = Alert & { id: string; - params: AlertTypeParams; + params: RuleTypeParams; }; -export interface SignalsRequest extends RequestFacade { - payload: SignalAlertParamsRest; +export interface RulesRequest extends RequestFacade { + payload: RuleAlertParamsRest; } -export interface UpdateSignalsRequest extends RequestFacade { - payload: UpdateSignalAlertParamsRest; +export interface UpdateRulesRequest extends RequestFacade { + payload: UpdateRuleAlertParamsRest; } -export type SignalExecutorOptions = Omit & { - params: SignalAlertParams & { +export type RuleExecutorOptions = Omit & { + params: RuleAlertParams & { scrollSize: number; scrollLock: string; }; @@ -221,24 +221,24 @@ export type QueryRequest = Omit & { query: { id: string | undefined; rule_id: string | undefined }; }; -// This returns true because by default a SignalAlertTypeDefinition is an AlertType +// This returns true because by default a RuleAlertTypeDefinition is an AlertType // since we are only increasing the strictness of params. -export const isAlertExecutor = (obj: SignalAlertTypeDefinition): obj is AlertType => { +export const isAlertExecutor = (obj: RuleAlertTypeDefinition): obj is AlertType => { return true; }; -export type SignalAlertTypeDefinition = Omit & { - executor: ({ services, params, state }: SignalExecutorOptions) => Promise; +export type RuleAlertTypeDefinition = Omit & { + executor: ({ services, params, state }: RuleExecutorOptions) => Promise; }; -export const isAlertTypes = (obj: unknown[]): obj is SignalAlertType[] => { - return obj.every(signal => isAlertType(signal)); +export const isAlertTypes = (obj: unknown[]): obj is RuleAlertType[] => { + return obj.every(rule => isAlertType(rule)); }; -export const isAlertType = (obj: unknown): obj is SignalAlertType => { +export const isAlertType = (obj: unknown): obj is RuleAlertType => { return get('alertTypeId', obj) === SIGNALS_ID; }; -export const isAlertTypeArray = (objArray: unknown[]): objArray is SignalAlertType[] => { +export const isAlertTypeArray = (objArray: unknown[]): objArray is RuleAlertType[] => { return objArray.length === 0 || isAlertType(objArray[0]); }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/update_signals.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/update_rules.test.ts similarity index 80% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/update_signals.test.ts rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/update_rules.test.ts index 39f7951a8eab98..1022fea93200fa 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/update_signals.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/update_rules.test.ts @@ -4,21 +4,21 @@ * you may not use this file except in compliance with the Elastic License. */ -import { calculateInterval, calculateName } from './update_signals'; +import { calculateInterval, calculateName } from './update_rules'; -describe('update_signals', () => { +describe('update_rules', () => { describe('#calculateInterval', () => { - test('given a undefined interval, it returns the signalInterval ', () => { + test('given a undefined interval, it returns the ruleInterval ', () => { const interval = calculateInterval(undefined, '10m'); expect(interval).toEqual('10m'); }); - test('given a undefined signalInterval, it returns a undefined interval ', () => { + test('given a undefined ruleInterval, it returns a undefined interval ', () => { const interval = calculateInterval('10m', undefined); expect(interval).toEqual('10m'); }); - test('given both an undefined signalInterval and a undefined interval, it returns 5m', () => { + test('given both an undefined ruleInterval and a undefined interval, it returns 5m', () => { const interval = calculateInterval(undefined, undefined); expect(interval).toEqual('5m'); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/update_signals.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/update_rules.ts similarity index 66% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/update_signals.ts rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/update_rules.ts index a38fd7756afa14..81360d78242302 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/update_signals.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/update_rules.ts @@ -6,17 +6,17 @@ import { defaults } from 'lodash/fp'; import { AlertAction } from '../../../../../alerting/server/types'; -import { readSignals } from './read_signals'; -import { UpdateSignalParams } from './types'; +import { readRules } from './read_rules'; +import { UpdateRuleParams } from './types'; export const calculateInterval = ( interval: string | undefined, - signalInterval: string | undefined + ruleInterval: string | undefined ): string => { if (interval != null) { return interval; - } else if (signalInterval != null) { - return signalInterval; + } else if (ruleInterval != null) { + return ruleInterval; } else { return '5m'; } @@ -35,13 +35,13 @@ export const calculateName = ({ return originalName; } else { // You really should never get to this point. This is a fail safe way to send back - // the name of "untitled" just in case a signal rule name became null or undefined at + // the name of "untitled" just in case a rule name became null or undefined at // some point since TypeScript allows it. return 'untitled'; } }; -export const updateSignal = async ({ +export const updateRules = async ({ alertsClient, actionsClient, // TODO: Use this whenever we add feature support for different action types description, @@ -68,17 +68,17 @@ export const updateSignal = async ({ to, type, references, -}: UpdateSignalParams) => { - const signal = await readSignals({ alertsClient, ruleId, id }); - if (signal == null) { +}: UpdateRuleParams) => { + const rule = await readRules({ alertsClient, ruleId, id }); + if (rule == null) { return null; } - // TODO: Remove this as cast as soon as signal.actions TypeScript bug is fixed + // TODO: Remove this as cast as soon as rule.actions TypeScript bug is fixed // where it is trying to return AlertAction[] or RawAlertAction[] - const actions = (signal.actions as AlertAction[] | undefined) || []; + const actions = (rule.actions as AlertAction[] | undefined) || []; - const params = signal.params || {}; + const params = rule.params || {}; const nextParams = defaults( { @@ -107,18 +107,18 @@ export const updateSignal = async ({ } ); - if (signal.enabled && !enabled) { - await alertsClient.disable({ id: signal.id }); - } else if (!signal.enabled && enabled) { - await alertsClient.enable({ id: signal.id }); + if (rule.enabled && !enabled) { + await alertsClient.disable({ id: rule.id }); + } else if (!rule.enabled && enabled) { + await alertsClient.enable({ id: rule.id }); } return alertsClient.update({ - id: signal.id, + id: rule.id, data: { tags: [], - name: calculateName({ updatedName: name, originalName: signal.name }), - interval: calculateInterval(interval, signal.interval), + name: calculateName({ updatedName: name, originalName: rule.name }), + interval: calculateInterval(interval, rule.interval), actions, params: nextParams, }, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/utils.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/utils.test.ts index 4aac425c7f80f8..19c8d5ccc87ca2 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/utils.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/utils.test.ts @@ -17,7 +17,7 @@ import { } from './utils'; import { sampleDocNoSortId, - sampleSignalAlertParams, + sampleRuleAlertParams, sampleDocSearchResultsNoSortId, sampleDocSearchResultsNoSortIdNoHits, sampleDocSearchResultsNoSortIdNoVersion, @@ -25,7 +25,7 @@ import { sampleEmptyDocSearchResults, repeatedSearchResultsWithSortId, sampleBulkCreateDuplicateResult, - sampleSignalId, + sampleRuleGuid, } from './__mocks__/es_results'; import { DEFAULT_SIGNALS_INDEX } from '../../../../common/constants'; @@ -52,11 +52,11 @@ describe('utils', () => { describe('buildBulkBody', () => { test('if bulk body builds well-defined body', () => { const fakeUuid = uuid.v4(); - const sampleParams = sampleSignalAlertParams(undefined); + const sampleParams = sampleRuleAlertParams(undefined); const fakeSignalSourceHit = buildBulkBody({ doc: sampleDocNoSortId(fakeUuid), - signalParams: sampleParams, - id: sampleSignalId, + ruleParams: sampleParams, + id: sampleRuleGuid, name: 'rule-name', createdBy: 'elastic', updatedBy: 'elastic', @@ -214,7 +214,7 @@ describe('utils', () => { }); test('create successful bulk create', async () => { const fakeUuid = uuid.v4(); - const sampleParams = sampleSignalAlertParams(undefined); + const sampleParams = sampleRuleAlertParams(undefined); const sampleSearchResult = sampleDocSearchResultsNoSortId; mockService.callCluster.mockReturnValueOnce({ took: 100, @@ -227,10 +227,10 @@ describe('utils', () => { }); const successfulsingleBulkCreate = await singleBulkCreate({ someResult: sampleSearchResult(fakeUuid), - signalParams: sampleParams, + ruleParams: sampleParams, services: mockService, logger: mockLogger, - id: sampleSignalId, + id: sampleRuleGuid, signalsIndex: DEFAULT_SIGNALS_INDEX, name: 'rule-name', createdBy: 'elastic', @@ -242,7 +242,7 @@ describe('utils', () => { }); test('create successful bulk create with docs with no versioning', async () => { const fakeUuid = uuid.v4(); - const sampleParams = sampleSignalAlertParams(undefined); + const sampleParams = sampleRuleAlertParams(undefined); const sampleSearchResult = sampleDocSearchResultsNoSortIdNoVersion; mockService.callCluster.mockReturnValueOnce({ took: 100, @@ -255,10 +255,10 @@ describe('utils', () => { }); const successfulsingleBulkCreate = await singleBulkCreate({ someResult: sampleSearchResult(fakeUuid), - signalParams: sampleParams, + ruleParams: sampleParams, services: mockService, logger: mockLogger, - id: sampleSignalId, + id: sampleRuleGuid, signalsIndex: DEFAULT_SIGNALS_INDEX, name: 'rule-name', createdBy: 'elastic', @@ -269,15 +269,15 @@ describe('utils', () => { expect(successfulsingleBulkCreate).toEqual(true); }); test('create unsuccessful bulk create due to empty search results', async () => { - const sampleParams = sampleSignalAlertParams(undefined); + const sampleParams = sampleRuleAlertParams(undefined); const sampleSearchResult = sampleEmptyDocSearchResults; mockService.callCluster.mockReturnValue(false); const successfulsingleBulkCreate = await singleBulkCreate({ someResult: sampleSearchResult, - signalParams: sampleParams, + ruleParams: sampleParams, services: mockService, logger: mockLogger, - id: sampleSignalId, + id: sampleRuleGuid, signalsIndex: DEFAULT_SIGNALS_INDEX, name: 'rule-name', createdBy: 'elastic', @@ -289,15 +289,15 @@ describe('utils', () => { }); test('create successful bulk create when bulk create has errors', async () => { const fakeUuid = uuid.v4(); - const sampleParams = sampleSignalAlertParams(undefined); + const sampleParams = sampleRuleAlertParams(undefined); const sampleSearchResult = sampleDocSearchResultsNoSortId; mockService.callCluster.mockReturnValue(sampleBulkCreateDuplicateResult); const successfulsingleBulkCreate = await singleBulkCreate({ someResult: sampleSearchResult(fakeUuid), - signalParams: sampleParams, + ruleParams: sampleParams, services: mockService, logger: mockLogger, - id: sampleSignalId, + id: sampleRuleGuid, signalsIndex: DEFAULT_SIGNALS_INDEX, name: 'rule-name', createdBy: 'elastic', @@ -312,12 +312,12 @@ describe('utils', () => { describe('singleSearchAfter', () => { test('if singleSearchAfter works without a given sort id', async () => { let searchAfterSortId; - const sampleParams = sampleSignalAlertParams(undefined); + const sampleParams = sampleRuleAlertParams(undefined); mockService.callCluster.mockReturnValue(sampleDocSearchResultsNoSortId); await expect( singleSearchAfter({ searchAfterSortId, - signalParams: sampleParams, + ruleParams: sampleParams, services: mockService, logger: mockLogger, pageSize: 1, @@ -326,11 +326,11 @@ describe('utils', () => { }); test('if singleSearchAfter works with a given sort id', async () => { const searchAfterSortId = '1234567891111'; - const sampleParams = sampleSignalAlertParams(undefined); + const sampleParams = sampleRuleAlertParams(undefined); mockService.callCluster.mockReturnValue(sampleDocSearchResultsWithSortId); const searchAfterResult = await singleSearchAfter({ searchAfterSortId, - signalParams: sampleParams, + ruleParams: sampleParams, services: mockService, logger: mockLogger, pageSize: 1, @@ -339,14 +339,14 @@ describe('utils', () => { }); test('if singleSearchAfter throws error', async () => { const searchAfterSortId = '1234567891111'; - const sampleParams = sampleSignalAlertParams(undefined); + const sampleParams = sampleRuleAlertParams(undefined); mockService.callCluster.mockImplementation(async () => { throw Error('Fake Error'); }); await expect( singleSearchAfter({ searchAfterSortId, - signalParams: sampleParams, + ruleParams: sampleParams, services: mockService, logger: mockLogger, pageSize: 1, @@ -356,13 +356,13 @@ describe('utils', () => { }); describe('searchAfterAndBulkCreate', () => { test('if successful with empty search results', async () => { - const sampleParams = sampleSignalAlertParams(undefined); + const sampleParams = sampleRuleAlertParams(undefined); const result = await searchAfterAndBulkCreate({ someResult: sampleEmptyDocSearchResults, - signalParams: sampleParams, + ruleParams: sampleParams, services: mockService, logger: mockLogger, - id: sampleSignalId, + id: sampleRuleGuid, signalsIndex: DEFAULT_SIGNALS_INDEX, name: 'rule-name', createdBy: 'elastic', @@ -375,7 +375,7 @@ describe('utils', () => { expect(result).toEqual(true); }); test('if successful iteration of while loop with maxDocs', async () => { - const sampleParams = sampleSignalAlertParams(30); + const sampleParams = sampleRuleAlertParams(30); const someGuids = Array.from({ length: 13 }).map(x => uuid.v4()); mockService.callCluster .mockReturnValueOnce({ @@ -409,10 +409,10 @@ describe('utils', () => { }); const result = await searchAfterAndBulkCreate({ someResult: repeatedSearchResultsWithSortId(3, 1, someGuids.slice(6, 9)), - signalParams: sampleParams, + ruleParams: sampleParams, services: mockService, logger: mockLogger, - id: sampleSignalId, + id: sampleRuleGuid, signalsIndex: DEFAULT_SIGNALS_INDEX, name: 'rule-name', createdBy: 'elastic', @@ -426,14 +426,14 @@ describe('utils', () => { }); test('if unsuccessful first bulk create', async () => { const someGuids = Array.from({ length: 4 }).map(x => uuid.v4()); - const sampleParams = sampleSignalAlertParams(10); + const sampleParams = sampleRuleAlertParams(10); mockService.callCluster.mockReturnValue(sampleBulkCreateDuplicateResult); const result = await searchAfterAndBulkCreate({ someResult: repeatedSearchResultsWithSortId(4, 1, someGuids), - signalParams: sampleParams, + ruleParams: sampleParams, services: mockService, logger: mockLogger, - id: sampleSignalId, + id: sampleRuleGuid, signalsIndex: DEFAULT_SIGNALS_INDEX, name: 'rule-name', createdBy: 'elastic', @@ -446,7 +446,7 @@ describe('utils', () => { expect(result).toEqual(false); }); test('if unsuccessful iteration of searchAfterAndBulkCreate due to empty sort ids', async () => { - const sampleParams = sampleSignalAlertParams(undefined); + const sampleParams = sampleRuleAlertParams(undefined); const someUuid = uuid.v4(); mockService.callCluster.mockReturnValueOnce({ took: 100, @@ -459,10 +459,10 @@ describe('utils', () => { }); const result = await searchAfterAndBulkCreate({ someResult: sampleDocSearchResultsNoSortId(someUuid), - signalParams: sampleParams, + ruleParams: sampleParams, services: mockService, logger: mockLogger, - id: sampleSignalId, + id: sampleRuleGuid, signalsIndex: DEFAULT_SIGNALS_INDEX, name: 'rule-name', createdBy: 'elastic', @@ -475,7 +475,7 @@ describe('utils', () => { expect(result).toEqual(false); }); test('if unsuccessful iteration of searchAfterAndBulkCreate due to empty sort ids and 0 total hits', async () => { - const sampleParams = sampleSignalAlertParams(undefined); + const sampleParams = sampleRuleAlertParams(undefined); const someUuid = uuid.v4(); mockService.callCluster.mockReturnValueOnce({ took: 100, @@ -488,10 +488,10 @@ describe('utils', () => { }); const result = await searchAfterAndBulkCreate({ someResult: sampleDocSearchResultsNoSortIdNoHits(someUuid), - signalParams: sampleParams, + ruleParams: sampleParams, services: mockService, logger: mockLogger, - id: sampleSignalId, + id: sampleRuleGuid, signalsIndex: DEFAULT_SIGNALS_INDEX, name: 'rule-name', createdBy: 'elastic', @@ -503,7 +503,7 @@ describe('utils', () => { expect(result).toEqual(true); }); test('if successful iteration of while loop with maxDocs and search after returns results with no sort ids', async () => { - const sampleParams = sampleSignalAlertParams(10); + const sampleParams = sampleRuleAlertParams(10); const oneGuid = uuid.v4(); const someGuids = Array.from({ length: 4 }).map(x => uuid.v4()); mockService.callCluster @@ -519,10 +519,10 @@ describe('utils', () => { .mockReturnValueOnce(sampleDocSearchResultsNoSortId(oneGuid)); const result = await searchAfterAndBulkCreate({ someResult: repeatedSearchResultsWithSortId(4, 1, someGuids), - signalParams: sampleParams, + ruleParams: sampleParams, services: mockService, logger: mockLogger, - id: sampleSignalId, + id: sampleRuleGuid, signalsIndex: DEFAULT_SIGNALS_INDEX, name: 'rule-name', createdBy: 'elastic', @@ -534,7 +534,7 @@ describe('utils', () => { expect(result).toEqual(true); }); test('if successful iteration of while loop with maxDocs and search after returns empty results with no sort ids', async () => { - const sampleParams = sampleSignalAlertParams(10); + const sampleParams = sampleRuleAlertParams(10); const someGuids = Array.from({ length: 4 }).map(x => uuid.v4()); mockService.callCluster .mockReturnValueOnce({ @@ -549,10 +549,10 @@ describe('utils', () => { .mockReturnValueOnce(sampleEmptyDocSearchResults); const result = await searchAfterAndBulkCreate({ someResult: repeatedSearchResultsWithSortId(4, 1, someGuids), - signalParams: sampleParams, + ruleParams: sampleParams, services: mockService, logger: mockLogger, - id: sampleSignalId, + id: sampleRuleGuid, signalsIndex: DEFAULT_SIGNALS_INDEX, name: 'rule-name', createdBy: 'elastic', @@ -564,7 +564,7 @@ describe('utils', () => { expect(result).toEqual(true); }); test('if returns false when singleSearchAfter throws an exception', async () => { - const sampleParams = sampleSignalAlertParams(10); + const sampleParams = sampleRuleAlertParams(10); const someGuids = Array.from({ length: 4 }).map(x => uuid.v4()); mockService.callCluster .mockReturnValueOnce({ @@ -581,10 +581,10 @@ describe('utils', () => { }); const result = await searchAfterAndBulkCreate({ someResult: repeatedSearchResultsWithSortId(4, 1, someGuids), - signalParams: sampleParams, + ruleParams: sampleParams, services: mockService, logger: mockLogger, - id: sampleSignalId, + id: sampleRuleGuid, signalsIndex: DEFAULT_SIGNALS_INDEX, name: 'rule-name', createdBy: 'elastic', diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/utils.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/utils.ts index f2a34246559457..ba3f310c886ceb 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/utils.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/utils.ts @@ -13,13 +13,13 @@ import { SignalSourceHit, SignalSearchResponse, BulkResponse, - AlertTypeParams, - OutputSignalES, + RuleTypeParams, + OutputRuleES, } from './types'; import { buildEventsSearchQuery } from './build_events_query'; interface BuildRuleParams { - signalParams: AlertTypeParams; + ruleParams: RuleTypeParams; name: string; id: string; enabled: boolean; @@ -29,46 +29,46 @@ interface BuildRuleParams { } export const buildRule = ({ - signalParams, + ruleParams, name, id, enabled, createdBy, updatedBy, interval, -}: BuildRuleParams): Partial => { - return pickBy((value: unknown) => value != null, { +}: BuildRuleParams): Partial => { + return pickBy((value: unknown) => value != null, { id, status: 'open', - rule_id: signalParams.ruleId, - false_positives: signalParams.falsePositives, - saved_id: signalParams.savedId, - meta: signalParams.meta, - max_signals: signalParams.maxSignals, - risk_score: signalParams.riskScore, - output_index: signalParams.outputIndex, - description: signalParams.description, - filter: signalParams.filter, - from: signalParams.from, - immutable: signalParams.immutable, - index: signalParams.index, + rule_id: ruleParams.ruleId, + false_positives: ruleParams.falsePositives, + saved_id: ruleParams.savedId, + meta: ruleParams.meta, + max_signals: ruleParams.maxSignals, + risk_score: ruleParams.riskScore, + output_index: ruleParams.outputIndex, + description: ruleParams.description, + filter: ruleParams.filter, + from: ruleParams.from, + immutable: ruleParams.immutable, + index: ruleParams.index, interval, - language: signalParams.language, + language: ruleParams.language, name, - query: signalParams.query, - references: signalParams.references, - severity: signalParams.severity, - tags: signalParams.tags, - type: signalParams.type, - to: signalParams.to, + query: ruleParams.query, + references: ruleParams.references, + severity: ruleParams.severity, + tags: ruleParams.tags, + type: ruleParams.type, + to: ruleParams.to, enabled, - filters: signalParams.filters, + filters: ruleParams.filters, created_by: createdBy, updated_by: updatedBy, }); }; -export const buildSignal = (doc: SignalSourceHit, rule: Partial): Signal => { +export const buildSignal = (doc: SignalSourceHit, rule: Partial): Signal => { return { parent: { id: doc._id, @@ -83,7 +83,7 @@ export const buildSignal = (doc: SignalSourceHit, rule: Partial) interface BuildBulkBodyParams { doc: SignalSourceHit; - signalParams: AlertTypeParams; + ruleParams: RuleTypeParams; id: string; name: string; createdBy: string; @@ -95,7 +95,7 @@ interface BuildBulkBodyParams { // format search_after result for signals index. export const buildBulkBody = ({ doc, - signalParams, + ruleParams, id, name, createdBy, @@ -104,7 +104,7 @@ export const buildBulkBody = ({ enabled, }: BuildBulkBodyParams): SignalHit => { const rule = buildRule({ - signalParams, + ruleParams, id, name, enabled, @@ -123,7 +123,7 @@ export const buildBulkBody = ({ interface SingleBulkCreateParams { someResult: SignalSearchResponse; - signalParams: AlertTypeParams; + ruleParams: RuleTypeParams; services: AlertServices; logger: Logger; id: string; @@ -148,7 +148,7 @@ export const generateId = ( // Bulk Index documents. export const singleBulkCreate = async ({ someResult, - signalParams, + ruleParams, services, logger, id, @@ -179,11 +179,11 @@ export const singleBulkCreate = async ({ doc._index, doc._id, doc._version ? doc._version.toString() : '', - signalParams.ruleId ?? '' + ruleParams.ruleId ?? '' ), }, }, - buildBulkBody({ doc, signalParams, id, name, createdBy, updatedBy, interval, enabled }), + buildBulkBody({ doc, ruleParams, id, name, createdBy, updatedBy, interval, enabled }), ]); const time1 = performance.now(); const firstResult: BulkResponse = await services.callCluster('bulk', { @@ -222,7 +222,7 @@ export const singleBulkCreate = async ({ interface SingleSearchAfterParams { searchAfterSortId: string | undefined; - signalParams: AlertTypeParams; + ruleParams: RuleTypeParams; services: AlertServices; logger: Logger; pageSize: number; @@ -231,7 +231,7 @@ interface SingleSearchAfterParams { // utilize search_after for paging results into bulk. export const singleSearchAfter = async ({ searchAfterSortId, - signalParams, + ruleParams, services, logger, pageSize, @@ -241,10 +241,10 @@ export const singleSearchAfter = async ({ } try { const searchAfterQuery = buildEventsSearchQuery({ - index: signalParams.index, - from: signalParams.from, - to: signalParams.to, - filter: signalParams.filter, + index: ruleParams.index, + from: ruleParams.from, + to: ruleParams.to, + filter: ruleParams.filter, size: pageSize, searchAfterSortId, }); @@ -261,7 +261,7 @@ export const singleSearchAfter = async ({ interface SearchAfterAndBulkCreateParams { someResult: SignalSearchResponse; - signalParams: AlertTypeParams; + ruleParams: RuleTypeParams; services: AlertServices; logger: Logger; id: string; @@ -277,7 +277,7 @@ interface SearchAfterAndBulkCreateParams { // search_after through documents and re-index using bulk endpoint. export const searchAfterAndBulkCreate = async ({ someResult, - signalParams, + ruleParams, services, logger, id, @@ -296,7 +296,7 @@ export const searchAfterAndBulkCreate = async ({ logger.debug('[+] starting bulk insertion'); await singleBulkCreate({ someResult, - signalParams, + ruleParams, services, logger, id, @@ -314,8 +314,7 @@ export const searchAfterAndBulkCreate = async ({ // If the total number of hits for the overall search result is greater than // maxSignals, default to requesting a total of maxSignals, otherwise use the // totalHits in the response from the searchAfter query. - const maxTotalHitsSize = - totalHits >= signalParams.maxSignals ? signalParams.maxSignals : totalHits; + const maxTotalHitsSize = totalHits >= ruleParams.maxSignals ? ruleParams.maxSignals : totalHits; // number of docs in the current search result let hitsSize = someResult.hits.hits.length; @@ -336,7 +335,7 @@ export const searchAfterAndBulkCreate = async ({ logger.debug(`sortIds: ${sortIds}`); const searchAfterResult: SignalSearchResponse = await singleSearchAfter({ searchAfterSortId: sortId, - signalParams, + ruleParams, services, logger, pageSize, // maximum number of docs to receive per search result. @@ -355,7 +354,7 @@ export const searchAfterAndBulkCreate = async ({ logger.debug('next bulk index'); await singleBulkCreate({ someResult: searchAfterResult, - signalParams, + ruleParams, services, logger, id, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts index c74d2e87a7ef6b..4c49326fbb32a9 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts @@ -6,13 +6,13 @@ import { ServerInjectOptions } from 'hapi'; import { ActionResult } from '../../../../../../actions/server/types'; -import { SignalAlertParamsRest, SignalAlertType } from '../../alerts/types'; +import { RuleAlertParamsRest, RuleAlertType } from '../../alerts/types'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; // The Omit of filter is because of a Hapi Server Typing issue that I am unclear // where it comes from. I would hope to remove the "filter" as an omit at some point // when we upgrade and Hapi Server is ok with the filter. -export const typicalPayload = (): Partial> => ({ +export const typicalPayload = (): Partial> => ({ rule_id: 'rule-1', description: 'Detecting root and admin users', index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], @@ -28,7 +28,7 @@ export const typicalPayload = (): Partial> language: 'kuery', }); -export const typicalFilterPayload = (): Partial => ({ +export const typicalFilterPayload = (): Partial => ({ rule_id: 'rule-1', description: 'Detecting root and admin users', index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], @@ -64,7 +64,7 @@ interface FindHit { page: number; perPage: number; total: number; - data: SignalAlertType[]; + data: RuleAlertType[]; } export const getFindResult = (): FindHit => ({ @@ -81,7 +81,7 @@ export const getFindResultWithSingleHit = (): FindHit => ({ data: [getResult()], }); -export const getFindResultWithMultiHits = (data: SignalAlertType[]): FindHit => ({ +export const getFindResultWithMultiHits = (data: RuleAlertType[]): FindHit => ({ page: 1, perPage: 1, total: 2, @@ -113,7 +113,7 @@ export const createActionResult = (): ActionResult => ({ config: {}, }); -export const getResult = (): SignalAlertType => ({ +export const getResult = (): RuleAlertType => ({ id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', name: 'Detect Root/Admin Users', tags: [], diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/create_signals_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/create_rules_route.test.ts similarity index 92% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/create_signals_route.test.ts rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/create_rules_route.test.ts index 1232fe3ce219d3..4c222c196300ce 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/create_signals_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/create_rules_route.test.ts @@ -10,7 +10,7 @@ import { createMockServerWithoutAlertClientDecoration, createMockServerWithoutActionOrAlertClientDecoration, } from './__mocks__/_mock_server'; -import { createSignalsRoute } from './create_signals_route'; +import { createRulesRoute } from './create_rules_route'; import { ServerInjectOptions } from 'hapi'; import { getFindResult, @@ -21,17 +21,17 @@ import { } from './__mocks__/request_responses'; import { DETECTION_ENGINE_RULES_URL } from '../../../../common/constants'; -describe('create_signals', () => { +describe('create_rules', () => { let { server, alertsClient, actionsClient } = createMockServer(); beforeEach(() => { jest.resetAllMocks(); ({ server, alertsClient, actionsClient } = createMockServer()); - createSignalsRoute(server); + createRulesRoute(server); }); describe('status codes with actionClient and alertClient', () => { - test('returns 200 when creating a single signal with a valid actionClient and alertClient', async () => { + test('returns 200 when creating a single rule with a valid actionClient and alertClient', async () => { alertsClient.find.mockResolvedValue(getFindResult()); alertsClient.get.mockResolvedValue(getResult()); actionsClient.create.mockResolvedValue(createActionResult()); @@ -42,14 +42,14 @@ describe('create_signals', () => { test('returns 404 if actionClient is not available on the route', async () => { const { serverWithoutActionClient } = createMockServerWithoutActionClientDecoration(); - createSignalsRoute(serverWithoutActionClient); + createRulesRoute(serverWithoutActionClient); const { statusCode } = await serverWithoutActionClient.inject(getCreateRequest()); expect(statusCode).toBe(404); }); test('returns 404 if alertClient is not available on the route', async () => { const { serverWithoutAlertClient } = createMockServerWithoutAlertClientDecoration(); - createSignalsRoute(serverWithoutAlertClient); + createRulesRoute(serverWithoutAlertClient); const { statusCode } = await serverWithoutAlertClient.inject(getCreateRequest()); expect(statusCode).toBe(404); }); @@ -58,7 +58,7 @@ describe('create_signals', () => { const { serverWithoutActionOrAlertClient, } = createMockServerWithoutActionOrAlertClientDecoration(); - createSignalsRoute(serverWithoutActionOrAlertClient); + createRulesRoute(serverWithoutActionOrAlertClient); const { statusCode } = await serverWithoutActionOrAlertClient.inject(getCreateRequest()); expect(statusCode).toBe(404); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/create_signals_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/create_rules_route.ts similarity index 72% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/create_signals_route.ts rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/create_rules_route.ts index fa8fd66ef2aef6..7e1ac07e1f0aaa 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/create_signals_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/create_rules_route.ts @@ -9,14 +9,14 @@ import { isFunction } from 'lodash/fp'; import Boom from 'boom'; import uuid from 'uuid'; import { DETECTION_ENGINE_RULES_URL } from '../../../../common/constants'; -import { createSignals } from '../alerts/create_signals'; -import { SignalsRequest } from '../alerts/types'; -import { createSignalsSchema } from './schemas'; +import { createRules } from '../alerts/create_rules'; +import { RulesRequest } from '../alerts/types'; +import { createRulesSchema } from './schemas'; import { ServerFacade } from '../../../types'; -import { readSignals } from '../alerts/read_signals'; +import { readRules } from '../alerts/read_rules'; import { transformOrError } from './utils'; -export const createCreateSignalsRoute: Hapi.ServerRoute = { +export const createCreateRulesRoute: Hapi.ServerRoute = { method: 'POST', path: DETECTION_ENGINE_RULES_URL, options: { @@ -25,10 +25,10 @@ export const createCreateSignalsRoute: Hapi.ServerRoute = { options: { abortEarly: false, }, - payload: createSignalsSchema, + payload: createRulesSchema, }, }, - async handler(request: SignalsRequest, headers) { + async handler(request: RulesRequest, headers) { const { description, enabled, @@ -63,13 +63,13 @@ export const createCreateSignalsRoute: Hapi.ServerRoute = { } if (ruleId != null) { - const signal = await readSignals({ alertsClient, ruleId }); - if (signal != null) { - return new Boom(`Signal rule_id ${ruleId} already exists`, { statusCode: 409 }); + const rule = await readRules({ alertsClient, ruleId }); + if (rule != null) { + return new Boom(`rule_id ${ruleId} already exists`, { statusCode: 409 }); } } - const createdSignal = await createSignals({ + const createdRule = await createRules({ alertsClient, actionsClient, description, @@ -96,10 +96,10 @@ export const createCreateSignalsRoute: Hapi.ServerRoute = { type, references, }); - return transformOrError(createdSignal); + return transformOrError(createdRule); }, }; -export const createSignalsRoute = (server: ServerFacade) => { - server.route(createCreateSignalsRoute); +export const createRulesRoute = (server: ServerFacade) => { + server.route(createCreateRulesRoute); }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/delete_signals_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/delete_rules_route.test.ts similarity index 83% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/delete_signals_route.test.ts rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/delete_rules_route.test.ts index 95816aa55d1fea..0808051964dc1d 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/delete_signals_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/delete_rules_route.test.ts @@ -11,7 +11,7 @@ import { createMockServerWithoutActionOrAlertClientDecoration, } from './__mocks__/_mock_server'; -import { deleteSignalsRoute } from './delete_signals_route'; +import { deleteRulesRoute } from './delete_rules_route'; import { ServerInjectOptions } from 'hapi'; import { getFindResult, @@ -22,12 +22,12 @@ import { } from './__mocks__/request_responses'; import { DETECTION_ENGINE_RULES_URL } from '../../../../common/constants'; -describe('delete_signals', () => { +describe('delete_rules', () => { let { server, alertsClient } = createMockServer(); beforeEach(() => { ({ server, alertsClient } = createMockServer()); - deleteSignalsRoute(server); + deleteRulesRoute(server); }); afterEach(() => { @@ -35,7 +35,7 @@ describe('delete_signals', () => { }); describe('status codes with actionClient and alertClient', () => { - test('returns 200 when deleting a single signal with a valid actionClient and alertClient by alertId', async () => { + test('returns 200 when deleting a single rule with a valid actionClient and alertClient by alertId', async () => { alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); alertsClient.get.mockResolvedValue(getResult()); alertsClient.delete.mockResolvedValue({}); @@ -43,7 +43,7 @@ describe('delete_signals', () => { expect(statusCode).toBe(200); }); - test('returns 200 when deleting a single signal with a valid actionClient and alertClient by id', async () => { + test('returns 200 when deleting a single rule with a valid actionClient and alertClient by id', async () => { alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); alertsClient.get.mockResolvedValue(getResult()); alertsClient.delete.mockResolvedValue({}); @@ -51,7 +51,7 @@ describe('delete_signals', () => { expect(statusCode).toBe(200); }); - test('returns 404 when deleting a single signal that does not exist with a valid actionClient and alertClient', async () => { + test('returns 404 when deleting a single rule that does not exist with a valid actionClient and alertClient', async () => { alertsClient.find.mockResolvedValue(getFindResult()); alertsClient.get.mockResolvedValue(getResult()); alertsClient.delete.mockResolvedValue({}); @@ -61,14 +61,14 @@ describe('delete_signals', () => { test('returns 404 if actionClient is not available on the route', async () => { const { serverWithoutActionClient } = createMockServerWithoutActionClientDecoration(); - deleteSignalsRoute(serverWithoutActionClient); + deleteRulesRoute(serverWithoutActionClient); const { statusCode } = await serverWithoutActionClient.inject(getDeleteRequest()); expect(statusCode).toBe(404); }); test('returns 404 if alertClient is not available on the route', async () => { const { serverWithoutAlertClient } = createMockServerWithoutAlertClientDecoration(); - deleteSignalsRoute(serverWithoutAlertClient); + deleteRulesRoute(serverWithoutAlertClient); const { statusCode } = await serverWithoutAlertClient.inject(getDeleteRequest()); expect(statusCode).toBe(404); }); @@ -77,7 +77,7 @@ describe('delete_signals', () => { const { serverWithoutActionOrAlertClient, } = createMockServerWithoutActionOrAlertClientDecoration(); - deleteSignalsRoute(serverWithoutActionOrAlertClient); + deleteRulesRoute(serverWithoutActionOrAlertClient); const { statusCode } = await serverWithoutActionOrAlertClient.inject(getDeleteRequest()); expect(statusCode).toBe(404); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/delete_signals_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/delete_rules_route.ts similarity index 75% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/delete_signals_route.ts rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/delete_rules_route.ts index 1f5494a54ddca8..12dff0dd60c147 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/delete_signals_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/delete_rules_route.ts @@ -8,13 +8,13 @@ import Hapi from 'hapi'; import { isFunction } from 'lodash/fp'; import { DETECTION_ENGINE_RULES_URL } from '../../../../common/constants'; -import { deleteSignals } from '../alerts/delete_signals'; +import { deleteRules } from '../alerts/delete_rules'; import { ServerFacade } from '../../../types'; -import { querySignalSchema } from './schemas'; +import { queryRulesSchema } from './schemas'; import { QueryRequest } from '../alerts/types'; import { getIdError, transformOrError } from './utils'; -export const createDeleteSignalsRoute: Hapi.ServerRoute = { +export const createDeleteRulesRoute: Hapi.ServerRoute = { method: 'DELETE', path: DETECTION_ENGINE_RULES_URL, options: { @@ -23,7 +23,7 @@ export const createDeleteSignalsRoute: Hapi.ServerRoute = { options: { abortEarly: false, }, - query: querySignalSchema, + query: queryRulesSchema, }, }, async handler(request: QueryRequest, headers) { @@ -35,21 +35,21 @@ export const createDeleteSignalsRoute: Hapi.ServerRoute = { return headers.response().code(404); } - const signal = await deleteSignals({ + const rule = await deleteRules({ actionsClient, alertsClient, id, ruleId, }); - if (signal != null) { - return transformOrError(signal); + if (rule != null) { + return transformOrError(rule); } else { return getIdError({ id, ruleId }); } }, }; -export const deleteSignalsRoute = (server: ServerFacade): void => { - server.route(createDeleteSignalsRoute); +export const deleteRulesRoute = (server: ServerFacade): void => { + server.route(createDeleteRulesRoute); }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/find_signals_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/find_rules_route.test.ts similarity index 89% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/find_signals_route.test.ts rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/find_rules_route.test.ts index be3dce36e87167..dae40f05155dc5 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/find_signals_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/find_rules_route.test.ts @@ -11,17 +11,17 @@ import { createMockServerWithoutActionOrAlertClientDecoration, } from './__mocks__/_mock_server'; -import { findSignalsRoute } from './find_signals_route'; +import { findRulesRoute } from './find_rules_route'; import { ServerInjectOptions } from 'hapi'; import { getFindResult, getResult, getFindRequest } from './__mocks__/request_responses'; import { DETECTION_ENGINE_RULES_URL } from '../../../../common/constants'; -describe('find_signals', () => { +describe('find_rules', () => { let { server, alertsClient, actionsClient } = createMockServer(); beforeEach(() => { ({ server, alertsClient, actionsClient } = createMockServer()); - findSignalsRoute(server); + findRulesRoute(server); }); afterEach(() => { @@ -29,7 +29,7 @@ describe('find_signals', () => { }); describe('status codes with actionClient and alertClient', () => { - test('returns 200 when finding a single signal with a valid actionClient and alertClient', async () => { + test('returns 200 when finding a single rule with a valid actionClient and alertClient', async () => { alertsClient.find.mockResolvedValue(getFindResult()); actionsClient.find.mockResolvedValue({ page: 1, @@ -44,14 +44,14 @@ describe('find_signals', () => { test('returns 404 if actionClient is not available on the route', async () => { const { serverWithoutActionClient } = createMockServerWithoutActionClientDecoration(); - findSignalsRoute(serverWithoutActionClient); + findRulesRoute(serverWithoutActionClient); const { statusCode } = await serverWithoutActionClient.inject(getFindRequest()); expect(statusCode).toBe(404); }); test('returns 404 if alertClient is not available on the route', async () => { const { serverWithoutAlertClient } = createMockServerWithoutAlertClientDecoration(); - findSignalsRoute(serverWithoutAlertClient); + findRulesRoute(serverWithoutAlertClient); const { statusCode } = await serverWithoutAlertClient.inject(getFindRequest()); expect(statusCode).toBe(404); }); @@ -60,7 +60,7 @@ describe('find_signals', () => { const { serverWithoutActionOrAlertClient, } = createMockServerWithoutActionOrAlertClientDecoration(); - findSignalsRoute(serverWithoutActionOrAlertClient); + findRulesRoute(serverWithoutActionOrAlertClient); const { statusCode } = await serverWithoutActionOrAlertClient.inject(getFindRequest()); expect(statusCode).toBe(404); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/find_signals_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/find_rules_route.ts similarity index 70% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/find_signals_route.ts rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/find_rules_route.ts index 120b71fab7d3ae..893fb3f689d164 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/find_signals_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/find_rules_route.ts @@ -7,13 +7,13 @@ import Hapi from 'hapi'; import { isFunction } from 'lodash/fp'; import { DETECTION_ENGINE_RULES_URL } from '../../../../common/constants'; -import { findSignals } from '../alerts/find_signals'; -import { FindSignalsRequest } from '../alerts/types'; -import { findSignalsSchema } from './schemas'; +import { findRules } from '../alerts/find_rules'; +import { FindRulesRequest } from '../alerts/types'; +import { findRulesSchema } from './schemas'; import { ServerFacade } from '../../../types'; import { transformFindAlertsOrError } from './utils'; -export const createFindSignalRoute: Hapi.ServerRoute = { +export const createFindRulesRoute: Hapi.ServerRoute = { method: 'GET', path: `${DETECTION_ENGINE_RULES_URL}/_find`, options: { @@ -22,10 +22,10 @@ export const createFindSignalRoute: Hapi.ServerRoute = { options: { abortEarly: false, }, - query: findSignalsSchema, + query: findRulesSchema, }, }, - async handler(request: FindSignalsRequest, headers) { + async handler(request: FindRulesRequest, headers) { const { query } = request; const alertsClient = isFunction(request.getAlertsClient) ? request.getAlertsClient() : null; const actionsClient = isFunction(request.getActionsClient) ? request.getActionsClient() : null; @@ -34,7 +34,7 @@ export const createFindSignalRoute: Hapi.ServerRoute = { return headers.response().code(404); } - const signals = await findSignals({ + const rules = await findRules({ alertsClient, perPage: query.per_page, page: query.page, @@ -42,10 +42,10 @@ export const createFindSignalRoute: Hapi.ServerRoute = { sortOrder: query.sort_order, filter: query.filter, }); - return transformFindAlertsOrError(signals); + return transformFindAlertsOrError(rules); }, }; -export const findSignalsRoute = (server: ServerFacade) => { - server.route(createFindSignalRoute); +export const findRulesRoute = (server: ServerFacade) => { + server.route(createFindRulesRoute); }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/read_signals_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/read_rules_route.test.ts similarity index 88% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/read_signals_route.test.ts rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/read_rules_route.test.ts index 021bcc7b8b48e0..47ecf62f41be9b 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/read_signals_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/read_rules_route.test.ts @@ -11,7 +11,7 @@ import { createMockServerWithoutActionOrAlertClientDecoration, } from './__mocks__/_mock_server'; -import { readSignalsRoute } from './read_signals_route'; +import { readRulesRoute } from './read_rules_route'; import { ServerInjectOptions } from 'hapi'; import { getFindResult, @@ -26,7 +26,7 @@ describe('read_signals', () => { beforeEach(() => { ({ server, alertsClient } = createMockServer()); - readSignalsRoute(server); + readRulesRoute(server); }); afterEach(() => { @@ -34,7 +34,7 @@ describe('read_signals', () => { }); describe('status codes with actionClient and alertClient', () => { - test('returns 200 when reading a single signal with a valid actionClient and alertClient', async () => { + test('returns 200 when reading a single rule with a valid actionClient and alertClient', async () => { alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); alertsClient.get.mockResolvedValue(getResult()); const { statusCode } = await server.inject(getReadRequest()); @@ -43,14 +43,14 @@ describe('read_signals', () => { test('returns 404 if actionClient is not available on the route', async () => { const { serverWithoutActionClient } = createMockServerWithoutActionClientDecoration(); - readSignalsRoute(serverWithoutActionClient); + readRulesRoute(serverWithoutActionClient); const { statusCode } = await serverWithoutActionClient.inject(getReadRequest()); expect(statusCode).toBe(404); }); test('returns 404 if alertClient is not available on the route', async () => { const { serverWithoutAlertClient } = createMockServerWithoutAlertClientDecoration(); - readSignalsRoute(serverWithoutAlertClient); + readRulesRoute(serverWithoutAlertClient); const { statusCode } = await serverWithoutAlertClient.inject(getReadRequest()); expect(statusCode).toBe(404); }); @@ -59,7 +59,7 @@ describe('read_signals', () => { const { serverWithoutActionOrAlertClient, } = createMockServerWithoutActionOrAlertClientDecoration(); - readSignalsRoute(serverWithoutActionOrAlertClient); + readRulesRoute(serverWithoutActionOrAlertClient); const { statusCode } = await serverWithoutActionOrAlertClient.inject(getReadRequest()); expect(statusCode).toBe(404); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/read_signals_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/read_rules_route.ts similarity index 75% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/read_signals_route.ts rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/read_rules_route.ts index 2d662f9049cce2..4642c34fbe3393 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/read_signals_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/read_rules_route.ts @@ -9,12 +9,12 @@ import { isFunction } from 'lodash/fp'; import { DETECTION_ENGINE_RULES_URL } from '../../../../common/constants'; import { getIdError, transformOrError } from './utils'; -import { readSignals } from '../alerts/read_signals'; +import { readRules } from '../alerts/read_rules'; import { ServerFacade } from '../../../types'; -import { querySignalSchema } from './schemas'; +import { queryRulesSchema } from './schemas'; import { QueryRequest } from '../alerts/types'; -export const createReadSignalsRoute: Hapi.ServerRoute = { +export const createReadRulesRoute: Hapi.ServerRoute = { method: 'GET', path: DETECTION_ENGINE_RULES_URL, options: { @@ -23,7 +23,7 @@ export const createReadSignalsRoute: Hapi.ServerRoute = { options: { abortEarly: false, }, - query: querySignalSchema, + query: queryRulesSchema, }, }, async handler(request: QueryRequest, headers) { @@ -34,19 +34,19 @@ export const createReadSignalsRoute: Hapi.ServerRoute = { if (!alertsClient || !actionsClient) { return headers.response().code(404); } - const signal = await readSignals({ + const rule = await readRules({ alertsClient, id, ruleId, }); - if (signal != null) { - return transformOrError(signal); + if (rule != null) { + return transformOrError(rule); } else { return getIdError({ id, ruleId }); } }, }; -export const readSignalsRoute = (server: ServerFacade) => { - server.route(createReadSignalsRoute); +export const readRulesRoute = (server: ServerFacade) => { + server.route(createReadRulesRoute); }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas.test.ts index 6639dc6a3dfd65..6c7e5c4054326d 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas.test.ts @@ -4,27 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - createSignalsSchema, - updateSignalSchema, - findSignalsSchema, - querySignalSchema, -} from './schemas'; -import { - SignalAlertParamsRest, - FindParamsRest, - UpdateSignalAlertParamsRest, -} from '../alerts/types'; +import { createRulesSchema, updateRulesSchema, findRulesSchema, queryRulesSchema } from './schemas'; +import { RuleAlertParamsRest, FindParamsRest, UpdateRuleAlertParamsRest } from '../alerts/types'; describe('schemas', () => { - describe('create signals schema', () => { + describe('create rules schema', () => { test('empty objects do not validate', () => { - expect(createSignalsSchema.validate>({}).error).toBeTruthy(); + expect(createRulesSchema.validate>({}).error).toBeTruthy(); }); test('made up values do not validate', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ madeUp: 'hi', }).error ).toBeTruthy(); @@ -32,7 +23,7 @@ describe('schemas', () => { test('[rule_id] does not validate', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', }).error ).toBeTruthy(); @@ -40,7 +31,7 @@ describe('schemas', () => { test('[rule_id, description] does not validate', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', }).error @@ -49,7 +40,7 @@ describe('schemas', () => { test('[rule_id, description, from] does not validate', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -59,7 +50,7 @@ describe('schemas', () => { test('[rule_id, description, from, to] does not validate', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -70,7 +61,7 @@ describe('schemas', () => { test('[rule_id, description, from, to, name] does not validate', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -82,7 +73,7 @@ describe('schemas', () => { test('[rule_id, description, from, to, name, severity] does not validate', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -95,7 +86,7 @@ describe('schemas', () => { test('[rule_id, description, from, to, name, severity, type] does not validate', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -109,7 +100,7 @@ describe('schemas', () => { test('[rule_id, description, from, to, name, severity, type, interval] does not validate', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -124,7 +115,7 @@ describe('schemas', () => { test('[rule_id, description, from, to, name, severity, type, interval, index] does not validate', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -140,7 +131,7 @@ describe('schemas', () => { test('[rule_id, description, from, to, name, severity, type, query, index, interval] does not validate', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', risk_score: 50, description: 'some description', @@ -158,7 +149,7 @@ describe('schemas', () => { test('[rule_id, description, from, to, index, name, severity, interval, type, query, language] does not validate', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -176,7 +167,7 @@ describe('schemas', () => { test('[rule_id, description, from, to, index, name, severity, interval, type, query, language, risk_score] does validate', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', risk_score: 50, description: 'some description', @@ -195,7 +186,7 @@ describe('schemas', () => { test('[rule_id, description, from, to, index, name, severity, interval, type, query, language, risk_score, output_index] does validate', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -215,7 +206,7 @@ describe('schemas', () => { test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score] does validate', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -233,7 +224,7 @@ describe('schemas', () => { test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, output_index] does validate', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -252,7 +243,7 @@ describe('schemas', () => { test('If filter type is set then filter is required', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -270,7 +261,7 @@ describe('schemas', () => { test('If filter type is set then query is not allowed', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -290,7 +281,7 @@ describe('schemas', () => { test('If filter type is set then language is not allowed', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -310,7 +301,7 @@ describe('schemas', () => { test('If filter type is set then filters are not allowed', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -330,7 +321,7 @@ describe('schemas', () => { test('allows references to be sent as valid', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -351,7 +342,7 @@ describe('schemas', () => { test('defaults references to an array', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -371,8 +362,8 @@ describe('schemas', () => { test('references cannot be numbers', () => { expect( - createSignalsSchema.validate< - Partial> & { references: number[] } + createRulesSchema.validate< + Partial> & { references: number[] } >({ rule_id: 'rule-1', output_index: '.siem-signals', @@ -394,8 +385,8 @@ describe('schemas', () => { test('indexes cannot be numbers', () => { expect( - createSignalsSchema.validate< - Partial> & { index: number[] } + createRulesSchema.validate< + Partial> & { index: number[] } >({ rule_id: 'rule-1', output_index: '.siem-signals', @@ -416,7 +407,7 @@ describe('schemas', () => { test('defaults interval to 5 min', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -433,7 +424,7 @@ describe('schemas', () => { test('defaults max signals to 100', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -451,7 +442,7 @@ describe('schemas', () => { test('filter and filters cannot exist together', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -471,7 +462,7 @@ describe('schemas', () => { test('saved_id is required when type is saved_query and will not validate without out', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -489,7 +480,7 @@ describe('schemas', () => { test('saved_id is required when type is saved_query and validates with it', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', risk_score: 50, output_index: '.siem-signals', @@ -508,7 +499,7 @@ describe('schemas', () => { test('saved_query type can have filters with it', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -528,8 +519,8 @@ describe('schemas', () => { test('filters cannot be a string', () => { expect( - createSignalsSchema.validate< - Partial & { filters: string }> + createRulesSchema.validate< + Partial & { filters: string }> >({ rule_id: 'rule-1', output_index: '.siem-signals', @@ -550,7 +541,7 @@ describe('schemas', () => { test('saved_query type cannot have filter with it', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', risk_score: 50, output_index: '.siem-signals', @@ -570,7 +561,7 @@ describe('schemas', () => { test('language validates with kuery', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -591,7 +582,7 @@ describe('schemas', () => { test('language validates with lucene', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', risk_score: 50, output_index: '.siem-signals', @@ -612,7 +603,7 @@ describe('schemas', () => { test('language does not validate with something made up', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -633,7 +624,7 @@ describe('schemas', () => { test('max_signals cannot be negative', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -655,7 +646,7 @@ describe('schemas', () => { test('max_signals cannot be zero', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -677,7 +668,7 @@ describe('schemas', () => { test('max_signals can be 1', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -699,7 +690,7 @@ describe('schemas', () => { test('You can optionally send in an array of tags', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -722,32 +713,32 @@ describe('schemas', () => { test('You cannot send in an array of tags that are numbers', () => { expect( - createSignalsSchema.validate< - Partial> & { tags: number[] } - >({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'severity', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - tags: [0, 1, 2], - }).error + createRulesSchema.validate> & { tags: number[] }>( + { + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: 1, + tags: [0, 1, 2], + } + ).error ).toBeTruthy(); }); test('You can optionally send in an array of false positives', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -770,8 +761,8 @@ describe('schemas', () => { test('You cannot send in an array of false positives that are numbers', () => { expect( - createSignalsSchema.validate< - Partial> & { false_positives: number[] } + createRulesSchema.validate< + Partial> & { false_positives: number[] } >({ rule_id: 'rule-1', output_index: '.siem-signals', @@ -795,7 +786,7 @@ describe('schemas', () => { test('You can optionally set the immutable to be true', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -818,8 +809,8 @@ describe('schemas', () => { test('You cannot set the immutable to be a number', () => { expect( - createSignalsSchema.validate< - Partial> & { immutable: number } + createRulesSchema.validate< + Partial> & { immutable: number } >({ rule_id: 'rule-1', output_index: '.siem-signals', @@ -843,7 +834,7 @@ describe('schemas', () => { test('You cannot set the risk_score to 101', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 101, @@ -866,7 +857,7 @@ describe('schemas', () => { test('You cannot set the risk_score to -1', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: -1, @@ -889,7 +880,7 @@ describe('schemas', () => { test('You can set the risk_score to 0', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 0, @@ -912,7 +903,7 @@ describe('schemas', () => { test('You can set the risk_score to 100', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 100, @@ -935,7 +926,7 @@ describe('schemas', () => { test('You can set meta to any object you want', () => { expect( - createSignalsSchema.validate>({ + createRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -961,9 +952,7 @@ describe('schemas', () => { test('You cannot create meta as a string', () => { expect( - createSignalsSchema.validate< - Partial & { meta: string }> - >({ + createRulesSchema.validate & { meta: string }>>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -987,9 +976,7 @@ describe('schemas', () => { test('You can have an empty query string when filters are present', () => { expect( - createSignalsSchema.validate< - Partial & { meta: string }> - >({ + createRulesSchema.validate & { meta: string }>>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -1013,9 +1000,7 @@ describe('schemas', () => { test('You can omit the query string when filters are present', () => { expect( - createSignalsSchema.validate< - Partial & { meta: string }> - >({ + createRulesSchema.validate & { meta: string }>>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -1038,9 +1023,7 @@ describe('schemas', () => { test('query string defaults to empty string when present with filters', () => { expect( - createSignalsSchema.validate< - Partial & { meta: string }> - >({ + createRulesSchema.validate & { meta: string }>>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -1062,16 +1045,14 @@ describe('schemas', () => { }); }); - describe('update signals schema', () => { + describe('update rules schema', () => { test('empty objects do not validate as they require at least id or rule_id', () => { - expect( - updateSignalSchema.validate>({}).error - ).toBeTruthy(); + expect(updateRulesSchema.validate>({}).error).toBeTruthy(); }); test('made up values do not validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ madeUp: 'hi', }).error ).toBeTruthy(); @@ -1079,7 +1060,7 @@ describe('schemas', () => { test('[id] does validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', }).error ).toBeFalsy(); @@ -1087,7 +1068,7 @@ describe('schemas', () => { test('[rule_id] does validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ rule_id: 'rule-1', }).error ).toBeFalsy(); @@ -1095,7 +1076,7 @@ describe('schemas', () => { test('[id and rule_id] does not validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'id-1', rule_id: 'rule-1', }).error @@ -1104,7 +1085,7 @@ describe('schemas', () => { test('[rule_id, description] does validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', }).error @@ -1113,7 +1094,7 @@ describe('schemas', () => { test('[id, description] does validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', }).error @@ -1122,7 +1103,7 @@ describe('schemas', () => { test('[id, risk_score] does validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', risk_score: 10, }).error @@ -1131,7 +1112,7 @@ describe('schemas', () => { test('[rule_id, description, from] does validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1141,7 +1122,7 @@ describe('schemas', () => { test('[id, description, from] does validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1151,7 +1132,7 @@ describe('schemas', () => { test('[rule_id, description, from, to] does validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1162,7 +1143,7 @@ describe('schemas', () => { test('[id, description, from, to] does validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1173,7 +1154,7 @@ describe('schemas', () => { test('[rule_id, description, from, to, name] does validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1185,7 +1166,7 @@ describe('schemas', () => { test('[id, description, from, to, name] does validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1197,7 +1178,7 @@ describe('schemas', () => { test('[rule_id, description, from, to, name, severity] does validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1210,7 +1191,7 @@ describe('schemas', () => { test('[id, description, from, to, name, severity] does validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1223,7 +1204,7 @@ describe('schemas', () => { test('[rule_id, description, from, to, name, severity, type] does validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1237,7 +1218,7 @@ describe('schemas', () => { test('[id, description, from, to, name, severity, type] does validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1251,7 +1232,7 @@ describe('schemas', () => { test('[rule_id, description, from, to, name, severity, type, interval] does validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1266,7 +1247,7 @@ describe('schemas', () => { test('[id, description, from, to, name, severity, type, interval] does validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1281,7 +1262,7 @@ describe('schemas', () => { test('[rule_id, description, from, to, index, name, severity, interval, type] does validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1297,7 +1278,7 @@ describe('schemas', () => { test('[id, description, from, to, index, name, severity, interval, type] does validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1313,7 +1294,7 @@ describe('schemas', () => { test('[rule_id, description, from, to, index, name, severity, interval, type, query] does validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1330,7 +1311,7 @@ describe('schemas', () => { test('[id, description, from, to, index, name, severity, interval, type, query] does validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1347,7 +1328,7 @@ describe('schemas', () => { test('[rule_id, description, from, to, index, name, severity, interval, type, query, language] does validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1365,7 +1346,7 @@ describe('schemas', () => { test('[id, description, from, to, index, name, severity, interval, type, query, language] does validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1383,7 +1364,7 @@ describe('schemas', () => { test('[rule_id, description, from, to, index, name, severity, type, filter] does validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1400,7 +1381,7 @@ describe('schemas', () => { test('[id, description, from, to, index, name, severity, type, filter] does validate', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1417,7 +1398,7 @@ describe('schemas', () => { test('If filter type is set then filter is still not required', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1433,7 +1414,7 @@ describe('schemas', () => { test('If filter type is set then query is not allowed', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1451,7 +1432,7 @@ describe('schemas', () => { test('If filter type is set then language is not allowed', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1469,7 +1450,7 @@ describe('schemas', () => { test('If filter type is set then filters are not allowed', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1487,7 +1468,7 @@ describe('schemas', () => { test('allows references to be sent as a valid value to update with', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1506,7 +1487,7 @@ describe('schemas', () => { test('does not default references to an array', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1524,7 +1505,7 @@ describe('schemas', () => { test('does not default interval', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1539,7 +1520,7 @@ describe('schemas', () => { test('does not default max signal', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1555,8 +1536,8 @@ describe('schemas', () => { test('references cannot be numbers', () => { expect( - updateSignalSchema.validate< - Partial> & { references: number[] } + updateRulesSchema.validate< + Partial> & { references: number[] } >({ id: 'rule-1', description: 'some description', @@ -1576,8 +1557,8 @@ describe('schemas', () => { test('indexes cannot be numbers', () => { expect( - updateSignalSchema.validate< - Partial> & { index: number[] } + updateRulesSchema.validate< + Partial> & { index: number[] } >({ id: 'rule-1', description: 'some description', @@ -1596,7 +1577,7 @@ describe('schemas', () => { test('filter and filters cannot exist together', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1614,7 +1595,7 @@ describe('schemas', () => { test('saved_id is not required when type is saved_query and will validate without it', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1630,7 +1611,7 @@ describe('schemas', () => { test('saved_id validates with saved_query', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1647,7 +1628,7 @@ describe('schemas', () => { test('saved_query type can have filters with it', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1665,7 +1646,7 @@ describe('schemas', () => { test('saved_query type cannot have filter with it', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1683,7 +1664,7 @@ describe('schemas', () => { test('language validates with kuery', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1702,7 +1683,7 @@ describe('schemas', () => { test('language validates with lucene', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1721,7 +1702,7 @@ describe('schemas', () => { test('language does not validate with something made up', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1740,7 +1721,7 @@ describe('schemas', () => { test('max_signals cannot be negative', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1760,7 +1741,7 @@ describe('schemas', () => { test('max_signals cannot be zero', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1780,7 +1761,7 @@ describe('schemas', () => { test('max_signals can be 1', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1800,7 +1781,7 @@ describe('schemas', () => { test('meta can be updated', () => { expect( - updateSignalSchema.validate>({ + updateRulesSchema.validate>({ id: 'rule-1', meta: { whateverYouWant: 'anything_at_all' }, }).error @@ -1809,8 +1790,8 @@ describe('schemas', () => { test('You update meta as a string', () => { expect( - updateSignalSchema.validate< - Partial & { meta: string }> + updateRulesSchema.validate< + Partial & { meta: string }> >({ id: 'rule-1', meta: 'should not work', @@ -1820,8 +1801,8 @@ describe('schemas', () => { test('filters cannot be a string', () => { expect( - updateSignalSchema.validate< - Partial & { filters: string }> + updateRulesSchema.validate< + Partial & { filters: string }> >({ rule_id: 'rule-1', type: 'query', @@ -1831,14 +1812,14 @@ describe('schemas', () => { }); }); - describe('find signals schema', () => { + describe('find rules schema', () => { test('empty objects do validate', () => { - expect(findSignalsSchema.validate>({}).error).toBeFalsy(); + expect(findRulesSchema.validate>({}).error).toBeFalsy(); }); test('all values validate', () => { expect( - findSignalsSchema.validate>({ + findRulesSchema.validate>({ per_page: 5, page: 1, sort_field: 'some field', @@ -1851,7 +1832,7 @@ describe('schemas', () => { test('made up parameters do not validate', () => { expect( - findSignalsSchema.validate>({ + findRulesSchema.validate>({ madeUp: 'hi', }).error ).toBeTruthy(); @@ -1859,31 +1840,31 @@ describe('schemas', () => { test('per_page validates', () => { expect( - findSignalsSchema.validate>({ per_page: 5 }).error + findRulesSchema.validate>({ per_page: 5 }).error ).toBeFalsy(); }); test('page validates', () => { expect( - findSignalsSchema.validate>({ page: 5 }).error + findRulesSchema.validate>({ page: 5 }).error ).toBeFalsy(); }); test('sort_field validates', () => { expect( - findSignalsSchema.validate>({ sort_field: 'some value' }).error + findRulesSchema.validate>({ sort_field: 'some value' }).error ).toBeFalsy(); }); test('fields validates with a string', () => { expect( - findSignalsSchema.validate>({ fields: ['some value'] }).error + findRulesSchema.validate>({ fields: ['some value'] }).error ).toBeFalsy(); }); test('fields validates with multiple strings', () => { expect( - findSignalsSchema.validate>({ + findRulesSchema.validate>({ fields: ['some value 1', 'some value 2'], }).error ).toBeFalsy(); @@ -1891,23 +1872,23 @@ describe('schemas', () => { test('fields does not validate with a number', () => { expect( - findSignalsSchema.validate> & { fields: number[] }>({ + findRulesSchema.validate> & { fields: number[] }>({ fields: [5], }).error ).toBeTruthy(); }); test('per page has a default of 20', () => { - expect(findSignalsSchema.validate>({}).value.per_page).toEqual(20); + expect(findRulesSchema.validate>({}).value.per_page).toEqual(20); }); test('page has a default of 1', () => { - expect(findSignalsSchema.validate>({}).value.page).toEqual(1); + expect(findRulesSchema.validate>({}).value.page).toEqual(1); }); test('filter works with a string', () => { expect( - findSignalsSchema.validate>({ + findRulesSchema.validate>({ filter: 'some value 1', }).error ).toBeFalsy(); @@ -1915,7 +1896,7 @@ describe('schemas', () => { test('filter does not work with a number', () => { expect( - findSignalsSchema.validate> & { filter: number }>({ + findRulesSchema.validate> & { filter: number }>({ filter: 5, }).error ).toBeTruthy(); @@ -1923,7 +1904,7 @@ describe('schemas', () => { test('sort_order requires sort_field to work', () => { expect( - findSignalsSchema.validate>({ + findRulesSchema.validate>({ sort_order: 'asc', }).error ).toBeTruthy(); @@ -1931,7 +1912,7 @@ describe('schemas', () => { test('sort_order and sort_field validate together', () => { expect( - findSignalsSchema.validate>({ + findRulesSchema.validate>({ sort_order: 'asc', sort_field: 'some field', }).error @@ -1940,7 +1921,7 @@ describe('schemas', () => { test('sort_order validates with desc and sort_field', () => { expect( - findSignalsSchema.validate>({ + findRulesSchema.validate>({ sort_order: 'desc', sort_field: 'some field', }).error @@ -1949,7 +1930,7 @@ describe('schemas', () => { test('sort_order does not validate with a string other than asc and desc', () => { expect( - findSignalsSchema.validate< + findRulesSchema.validate< Partial> & { sort_order: string } >({ sort_order: 'some other string', @@ -1959,29 +1940,27 @@ describe('schemas', () => { }); }); - describe('querySignalSchema', () => { + describe('queryRulesSchema', () => { test('empty objects do not validate', () => { - expect( - querySignalSchema.validate>({}).error - ).toBeTruthy(); + expect(queryRulesSchema.validate>({}).error).toBeTruthy(); }); test('both rule_id and id being supplied dot not validate', () => { expect( - querySignalSchema.validate>({ rule_id: '1', id: '1' }) + queryRulesSchema.validate>({ rule_id: '1', id: '1' }) .error ).toBeTruthy(); }); test('only id validates', () => { expect( - querySignalSchema.validate>({ id: '1' }).error + queryRulesSchema.validate>({ id: '1' }).error ).toBeFalsy(); }); test('only rule_id validates', () => { expect( - querySignalSchema.validate>({ rule_id: '1' }).error + queryRulesSchema.validate>({ rule_id: '1' }).error ).toBeFalsy(); }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas.ts index 177e7cbebc2130..664a98ad7d7ddd 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas.ts @@ -52,7 +52,7 @@ const fields = Joi.array() .single(); /* eslint-enable @typescript-eslint/camelcase */ -export const createSignalsSchema = Joi.object({ +export const createRulesSchema = Joi.object({ description: description.required(), enabled: enabled.default(true), false_positives: false_positives.default([]), @@ -113,7 +113,7 @@ export const createSignalsSchema = Joi.object({ references: references.default([]), }); -export const updateSignalSchema = Joi.object({ +export const updateRulesSchema = Joi.object({ description, enabled, false_positives, @@ -168,12 +168,12 @@ export const updateSignalSchema = Joi.object({ references, }).xor('id', 'rule_id'); -export const querySignalSchema = Joi.object({ +export const queryRulesSchema = Joi.object({ rule_id, id, }).xor('id', 'rule_id'); -export const findSignalsSchema = Joi.object({ +export const findRulesSchema = Joi.object({ fields, filter: queryFilter, per_page, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/update_signals_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/update_rules_route.test.ts similarity index 92% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/update_signals_route.test.ts rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/update_rules_route.test.ts index 7288d18628316b..d03d68417dd5d5 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/update_signals_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/update_rules_route.test.ts @@ -11,7 +11,7 @@ import { createMockServerWithoutActionOrAlertClientDecoration, } from './__mocks__/_mock_server'; -import { updateSignalsRoute } from './update_signals_route'; +import { updateRulesRoute } from './update_rules_route'; import { ServerInjectOptions } from 'hapi'; import { getFindResult, @@ -24,17 +24,17 @@ import { } from './__mocks__/request_responses'; import { DETECTION_ENGINE_RULES_URL } from '../../../../common/constants'; -describe('update_signals', () => { +describe('update_rules', () => { let { server, alertsClient, actionsClient } = createMockServer(); beforeEach(() => { jest.resetAllMocks(); ({ server, alertsClient, actionsClient } = createMockServer()); - updateSignalsRoute(server); + updateRulesRoute(server); }); describe('status codes with actionClient and alertClient', () => { - test('returns 200 when updating a single signal with a valid actionClient and alertClient', async () => { + test('returns 200 when updating a single rule with a valid actionClient and alertClient', async () => { alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); alertsClient.get.mockResolvedValue(getResult()); actionsClient.update.mockResolvedValue(updateActionResult()); @@ -43,7 +43,7 @@ describe('update_signals', () => { expect(statusCode).toBe(200); }); - test('returns 404 when updating a single signal that does not exist', async () => { + test('returns 404 when updating a single rule that does not exist', async () => { alertsClient.find.mockResolvedValue(getFindResult()); alertsClient.get.mockResolvedValue(getResult()); actionsClient.update.mockResolvedValue(updateActionResult()); @@ -54,14 +54,14 @@ describe('update_signals', () => { test('returns 404 if actionClient is not available on the route', async () => { const { serverWithoutActionClient } = createMockServerWithoutActionClientDecoration(); - updateSignalsRoute(serverWithoutActionClient); + updateRulesRoute(serverWithoutActionClient); const { statusCode } = await serverWithoutActionClient.inject(getUpdateRequest()); expect(statusCode).toBe(404); }); test('returns 404 if alertClient is not available on the route', async () => { const { serverWithoutAlertClient } = createMockServerWithoutAlertClientDecoration(); - updateSignalsRoute(serverWithoutAlertClient); + updateRulesRoute(serverWithoutAlertClient); const { statusCode } = await serverWithoutAlertClient.inject(getUpdateRequest()); expect(statusCode).toBe(404); }); @@ -70,7 +70,7 @@ describe('update_signals', () => { const { serverWithoutActionOrAlertClient, } = createMockServerWithoutActionOrAlertClientDecoration(); - updateSignalsRoute(serverWithoutActionOrAlertClient); + updateRulesRoute(serverWithoutActionOrAlertClient); const { statusCode } = await serverWithoutActionOrAlertClient.inject(getUpdateRequest()); expect(statusCode).toBe(404); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/update_signals_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/update_rules_route.ts similarity index 78% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/update_signals_route.ts rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/update_rules_route.ts index 1dc54f34bd1f7b..1cc65054527c09 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/update_signals_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/update_rules_route.ts @@ -7,13 +7,13 @@ import Hapi from 'hapi'; import { isFunction } from 'lodash/fp'; import { DETECTION_ENGINE_RULES_URL } from '../../../../common/constants'; -import { updateSignal } from '../alerts/update_signals'; -import { UpdateSignalsRequest } from '../alerts/types'; -import { updateSignalSchema } from './schemas'; +import { updateRules } from '../alerts/update_rules'; +import { UpdateRulesRequest } from '../alerts/types'; +import { updateRulesSchema } from './schemas'; import { ServerFacade } from '../../../types'; import { getIdError, transformOrError } from './utils'; -export const createUpdateSignalsRoute: Hapi.ServerRoute = { +export const createUpdateRulesRoute: Hapi.ServerRoute = { method: 'PUT', path: DETECTION_ENGINE_RULES_URL, options: { @@ -22,10 +22,10 @@ export const createUpdateSignalsRoute: Hapi.ServerRoute = { options: { abortEarly: false, }, - payload: updateSignalSchema, + payload: updateRulesSchema, }, }, - async handler(request: UpdateSignalsRequest, headers) { + async handler(request: UpdateRulesRequest, headers) { const { description, enabled, @@ -60,7 +60,7 @@ export const createUpdateSignalsRoute: Hapi.ServerRoute = { return headers.response().code(404); } - const signal = await updateSignal({ + const rule = await updateRules({ alertsClient, actionsClient, description, @@ -88,14 +88,14 @@ export const createUpdateSignalsRoute: Hapi.ServerRoute = { type, references, }); - if (signal != null) { - return transformOrError(signal); + if (rule != null) { + return transformOrError(rule); } else { return getIdError({ id, ruleId }); } }, }; -export const updateSignalsRoute = (server: ServerFacade) => { - server.route(createUpdateSignalsRoute); +export const updateRulesRoute = (server: ServerFacade) => { + server.route(createUpdateRulesRoute); }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.test.ts index ed9e00735c7041..632778d78dab7d 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.test.ts @@ -6,7 +6,7 @@ import Boom from 'boom'; import { - transformAlertToSignal, + transformAlertToRule, getIdError, transformFindAlertsOrError, transformOrError, @@ -14,11 +14,11 @@ import { import { getResult } from './__mocks__/request_responses'; describe('utils', () => { - describe('transformAlertToSignal', () => { + describe('transformAlertToRule', () => { test('should work with a full data set', () => { - const fullSignal = getResult(); - const signal = transformAlertToSignal(fullSignal); - expect(signal).toEqual({ + const fullRule = getResult(); + const rule = transformAlertToRule(fullRule); + expect(rule).toEqual({ created_by: 'elastic', description: 'Detecting root and admin users', enabled: true, @@ -45,8 +45,8 @@ describe('utils', () => { }); test('should work with a partial data set missing data', () => { - const fullSignal = getResult(); - const { from, language, ...omitData } = transformAlertToSignal(fullSignal); + const fullRule = getResult(); + const { from, language, ...omitData } = transformAlertToRule(fullRule); expect(omitData).toEqual({ created_by: 'elastic', description: 'Detecting root and admin users', @@ -72,10 +72,10 @@ describe('utils', () => { }); test('should omit query if query is null', () => { - const fullSignal = getResult(); - fullSignal.params.query = null; - const signal = transformAlertToSignal(fullSignal); - expect(signal).toEqual({ + const fullRule = getResult(); + fullRule.params.query = null; + const rule = transformAlertToRule(fullRule); + expect(rule).toEqual({ created_by: 'elastic', description: 'Detecting root and admin users', enabled: true, @@ -101,10 +101,10 @@ describe('utils', () => { }); test('should omit query if query is undefined', () => { - const fullSignal = getResult(); - fullSignal.params.query = undefined; - const signal = transformAlertToSignal(fullSignal); - expect(signal).toEqual({ + const fullRule = getResult(); + fullRule.params.query = undefined; + const rule = transformAlertToRule(fullRule); + expect(rule).toEqual({ created_by: 'elastic', description: 'Detecting root and admin users', enabled: true, @@ -130,10 +130,10 @@ describe('utils', () => { }); test('should omit a mix of undefined, null, and missing fields', () => { - const fullSignal = getResult(); - fullSignal.params.query = undefined; - fullSignal.params.language = null; - const { from, enabled, ...omitData } = transformAlertToSignal(fullSignal); + const fullRule = getResult(); + fullRule.params.query = undefined; + fullRule.params.language = null; + const { from, enabled, ...omitData } = transformAlertToRule(fullRule); expect(omitData).toEqual({ created_by: 'elastic', description: 'Detecting root and admin users', @@ -157,10 +157,10 @@ describe('utils', () => { }); test('should return enabled is equal to false', () => { - const fullSignal = getResult(); - fullSignal.enabled = false; - const signalWithEnabledFalse = transformAlertToSignal(fullSignal); - expect(signalWithEnabledFalse).toEqual({ + const fullRule = getResult(); + fullRule.enabled = false; + const ruleWithEnabledFalse = transformAlertToRule(fullRule); + expect(ruleWithEnabledFalse).toEqual({ created_by: 'elastic', description: 'Detecting root and admin users', enabled: false, @@ -187,10 +187,10 @@ describe('utils', () => { }); test('should return immutable is equal to false', () => { - const fullSignal = getResult(); - fullSignal.params.immutable = false; - const signalWithEnabledFalse = transformAlertToSignal(fullSignal); - expect(signalWithEnabledFalse).toEqual({ + const fullRule = getResult(); + fullRule.params.immutable = false; + const ruleWithEnabledFalse = transformAlertToRule(fullRule); + expect(ruleWithEnabledFalse).toEqual({ created_by: 'elastic', description: 'Detecting root and admin users', enabled: true, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts index 7b9921b0375d8c..eb0ae49436bcaf 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts @@ -6,7 +6,7 @@ import Boom from 'boom'; import { pickBy } from 'lodash/fp'; -import { SignalAlertType, isAlertType, OutputSignalAlertRest, isAlertTypes } from '../alerts/types'; +import { RuleAlertType, isAlertType, OutputRuleAlertRest, isAlertTypes } from '../alerts/types'; export const getIdError = ({ id, @@ -26,49 +26,49 @@ export const getIdError = ({ // Transforms the data but will remove any null or undefined it encounters and not include // those on the export -export const transformAlertToSignal = (signal: SignalAlertType): Partial => { - return pickBy((value: unknown) => value != null, { - created_by: signal.createdBy, - description: signal.params.description, - enabled: signal.enabled, - false_positives: signal.params.falsePositives, - filter: signal.params.filter, - filters: signal.params.filters, - from: signal.params.from, - id: signal.id, - immutable: signal.params.immutable, - index: signal.params.index, - interval: signal.interval, - rule_id: signal.params.ruleId, - language: signal.params.language, - output_index: signal.params.outputIndex, - max_signals: signal.params.maxSignals, - risk_score: signal.params.riskScore, - name: signal.name, - query: signal.params.query, - references: signal.params.references, - saved_id: signal.params.savedId, - meta: signal.params.meta, - severity: signal.params.severity, - updated_by: signal.updatedBy, - tags: signal.params.tags, - to: signal.params.to, - type: signal.params.type, +export const transformAlertToRule = (alert: RuleAlertType): Partial => { + return pickBy((value: unknown) => value != null, { + created_by: alert.createdBy, + description: alert.params.description, + enabled: alert.enabled, + false_positives: alert.params.falsePositives, + filter: alert.params.filter, + filters: alert.params.filters, + from: alert.params.from, + id: alert.id, + immutable: alert.params.immutable, + index: alert.params.index, + interval: alert.interval, + rule_id: alert.params.ruleId, + language: alert.params.language, + output_index: alert.params.outputIndex, + max_signals: alert.params.maxSignals, + risk_score: alert.params.riskScore, + name: alert.name, + query: alert.params.query, + references: alert.params.references, + saved_id: alert.params.savedId, + meta: alert.params.meta, + severity: alert.params.severity, + updated_by: alert.updatedBy, + tags: alert.params.tags, + to: alert.params.to, + type: alert.params.type, }); }; export const transformFindAlertsOrError = (findResults: { data: unknown[] }): unknown | Boom => { if (isAlertTypes(findResults.data)) { - findResults.data = findResults.data.map(signal => transformAlertToSignal(signal)); + findResults.data = findResults.data.map(alert => transformAlertToRule(alert)); return findResults; } else { return new Boom('Internal error transforming', { statusCode: 500 }); } }; -export const transformOrError = (signal: unknown): Partial | Boom => { - if (isAlertType(signal)) { - return transformAlertToSignal(signal); +export const transformOrError = (alert: unknown): Partial | Boom => { + if (isAlertType(alert)) { + return transformAlertToRule(alert); } else { return new Boom('Internal error transforming', { statusCode: 500 }); } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/README.md b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/README.md index b3ab0011e1f8fc..8d617a8de3fcde 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/README.md +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/README.md @@ -4,6 +4,7 @@ search which is not available in the DEV console for the detection engine. Before beginning ensure in your .zshrc/.bashrc you have your user, password, and url set: Open up your .zshrc/.bashrc and add these lines with the variables filled in: + ``` export ELASTICSEARCH_USERNAME=${user} export ELASTICSEARCH_PASSWORD=${password} @@ -21,6 +22,7 @@ And that you have the latest version of [NodeJS](https://nodejs.org/en/), [CURL](https://curl.haxx.se), and [jq](https://stedolan.github.io/jq/) installed. If you have homebrew you can install using brew like so + ``` brew install jq ``` @@ -29,10 +31,9 @@ After that you can execute scripts within this folder by first ensuring your current working directory is `./scripts` and then running any scripts within that folder. -Example to add a signal to the system +Example to add a rule to the system ``` cd ./scripts -./post_signal.sh ./signals/root_or_admin_1.json +./post_rule.sh ./rules/root_or_admin_1.json ``` - diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/convert_saved_search_to_signals.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/convert_saved_search_to_rules.sh similarity index 80% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/convert_saved_search_to_signals.sh rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/convert_saved_search_to_rules.sh index 802273c67849df..e4d345eec0b656 100755 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/convert_saved_search_to_signals.sh +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/convert_saved_search_to_rules.sh @@ -9,4 +9,4 @@ set -e ./check_env_variables.sh -node ../../../../scripts/convert_saved_search_to_signals.js $1 $2 +node ../../../../scripts/convert_saved_search_to_rules.js $1 $2 diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/delete_signal_by_id.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/delete_rule_by_id.sh similarity index 91% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/delete_signal_by_id.sh rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/delete_rule_by_id.sh index 25cd4bfd336284..2db5740c79bb84 100755 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/delete_signal_by_id.sh +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/delete_rule_by_id.sh @@ -9,7 +9,7 @@ set -e ./check_env_variables.sh -# Example: ./delete_signal_by_id.sh ${id} +# Example: ./delete_rule_by_id.sh ${id} curl -s -k \ -H 'kbn-xsrf: 123' \ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/delete_signal_by_rule_id.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/delete_rule_by_rule_id.sh similarity index 89% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/delete_signal_by_rule_id.sh rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/delete_rule_by_rule_id.sh index b74ee260ad8adb..80ef849828b781 100755 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/delete_signal_by_rule_id.sh +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/delete_rule_by_rule_id.sh @@ -9,7 +9,7 @@ set -e ./check_env_variables.sh -# Example: ./delete_signal_by_rule_id.sh ${rule_id} +# Example: ./delete_rule_by_rule_id.sh ${rule_id} curl -s -k \ -H 'kbn-xsrf: 123' \ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/find_signal_by_filter.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/find_rule_by_filter.sh similarity index 81% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/find_signal_by_filter.sh rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/find_rule_by_filter.sh index 34c3c401b41120..34b6208947c573 100755 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/find_signal_by_filter.sh +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/find_rule_by_filter.sh @@ -11,8 +11,8 @@ set -e FILTER=${1:-'alert.attributes.enabled:%20true'} -# Example: ./find_signal_by_filter.sh "alert.attributes.enabled:%20true" -# Example: ./find_signal_by_filter.sh "alert.attributes.name:%20Detect*" +# Example: ./find_rule_by_filter.sh "alert.attributes.enabled:%20true" +# Example: ./find_rule_by_filter.sh "alert.attributes.name:%20Detect*" # The %20 is just an encoded space that is typical of URL's. # Table of them for testing if needed: https://www.w3schools.com/tags/ref_urlencode.asp curl -s -k \ diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/find_signals.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/find_rules.sh similarity index 93% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/find_signals.sh rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/find_rules.sh index 4542eb7c9a8273..520b4afa24cd2f 100755 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/find_signals.sh +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/find_rules.sh @@ -9,7 +9,7 @@ set -e ./check_env_variables.sh -# Example: ./find_signals.sh +# Example: ./find_rules.sh curl -s -k \ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ -X GET ${KIBANA_URL}${SPACE_URL}/api/detection_engine/rules/_find | jq . diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/find_signals_sort.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/find_rules_sort.sh similarity index 91% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/find_signals_sort.sh rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/find_rules_sort.sh index 122f18bbb80e5b..8e6690d848db42 100755 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/find_signals_sort.sh +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/find_rules_sort.sh @@ -12,7 +12,7 @@ set -e SORT=${1:-'enabled'} ORDER=${2:-'asc'} -# Example: ./find_signals_sort.sh enabled asc +# Example: ./find_rules_sort.sh enabled asc curl -s -k \ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ -X GET "${KIBANA_URL}${SPACE_URL}/api/detection_engine/rules/_find?sort_field=$SORT&sort_order=$ORDER" \ diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_signal_by_id.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_rule_by_id.sh similarity index 90% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_signal_by_id.sh rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_rule_by_id.sh index 239a04846b11ab..dba5652390ea97 100755 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_signal_by_id.sh +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_rule_by_id.sh @@ -9,7 +9,7 @@ set -e ./check_env_variables.sh -# Example: ./get_signal_by_id.sh {rule_id} +# Example: ./get_rule_by_id.sh {rule_id} curl -s -k \ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ -X GET ${KIBANA_URL}${SPACE_URL}/api/detection_engine/rules?id="$1" | jq . diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_signal_by_rule_id.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_rule_by_rule_id.sh similarity index 90% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_signal_by_rule_id.sh rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_rule_by_rule_id.sh index 5100caac32491b..114b6570a03e22 100755 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_signal_by_rule_id.sh +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_rule_by_rule_id.sh @@ -9,7 +9,7 @@ set -e ./check_env_variables.sh -# Example: ./get_signal_by_rule_id.sh {rule_id} +# Example: ./get_rule_by_rule_id.sh {rule_id} curl -s -k \ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ -X GET ${KIBANA_URL}${SPACE_URL}/api/detection_engine/rules?rule_id="$1" | jq . diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/post_signal.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/post_rule.sh similarity index 68% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/post_signal.sh rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/post_rule.sh index b8bd0e0e0361fa..591cf7625e2e3b 100755 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/post_signal.sh +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/post_rule.sh @@ -10,20 +10,20 @@ set -e ./check_env_variables.sh # Uses a default if no argument is specified -SIGNALS=(${@:-./signals/root_or_admin_1.json}) +RULES=(${@:-./rules/root_or_admin_1.json}) -# Example: ./post_signal.sh -# Example: ./post_signal.sh ./signals/root_or_admin_1.json -# Example glob: ./post_signal.sh ./signals/* -for SIGNAL in "${SIGNALS[@]}" +# Example: ./post_rule.sh +# Example: ./post_rule.sh ./rules/root_or_admin_1.json +# Example glob: ./post_rule.sh ./rules/* +for RULE in "${RULES[@]}" do { - [ -e "$SIGNAL" ] || continue + [ -e "$RULE" ] || continue curl -s -k \ -H 'Content-Type: application/json' \ -H 'kbn-xsrf: 123' \ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ -X POST ${KIBANA_URL}${SPACE_URL}/api/detection_engine/rules \ - -d @${SIGNAL} \ + -d @${RULE} \ | jq .; } & done diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/post_x_signals.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/post_x_rules.sh similarity index 94% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/post_x_signals.sh rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/post_x_rules.sh index abb2111a91c1b5..53e7bb504746d9 100755 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/post_x_signals.sh +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/post_x_rules.sh @@ -12,8 +12,8 @@ set -e # Uses a default of 100 if no argument is specified NUMBER=${1:-100} -# Example: ./post_x_signals.sh -# Example: ./post_x_signals.sh 200 +# Example: ./post_x_rules.sh +# Example: ./post_x_rules.sh 200 for i in $(seq 1 $NUMBER); do { curl -s -k \ diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/filter_with_empty_query.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/filter_with_empty_query.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/filter_with_empty_query.json rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/filter_with_empty_query.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/filter_without_query.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/filter_without_query.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/filter_without_query.json rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/filter_without_query.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_1.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_1.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_1.json rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_1.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_10.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_10.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_10.json rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_10.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_2.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_2.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_2.json rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_2.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_3.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_3.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_3.json rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_3.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_4.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_4.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_4.json rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_4.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_5.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_5.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_5.json rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_5.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_6.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_6.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_6.json rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_6.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_7.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_7.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_7.json rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_7.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_8.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_8.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_8.json rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_8.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_9.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_9.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_9.json rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_9.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_filter_9998.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_filter_9998.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_filter_9998.json rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_filter_9998.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_filter_9999.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_filter_9999.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_filter_9999.json rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_filter_9999.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_meta.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_meta.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_meta.json rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_meta.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_saved_query_1.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_saved_query_1.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_saved_query_1.json rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_saved_query_1.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_saved_query_2.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_saved_query_2.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_saved_query_2.json rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_saved_query_2.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_saved_query_3.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_saved_query_3.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_saved_query_3.json rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_saved_query_3.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_update_1.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_update_1.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_update_1.json rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_update_1.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_update_2.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_update_2.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_update_2.json rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/root_or_admin_update_2.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/watch_longmont.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/watch_longmont.json similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/watch_longmont.json rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/watch_longmont.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/update_signal.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/update_rule.sh similarity index 67% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/update_signal.sh rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/update_rule.sh index 04541e1df1fa16..8e1abc70456020 100755 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/update_signal.sh +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/update_rule.sh @@ -10,20 +10,20 @@ set -e ./check_env_variables.sh # Uses a default if no argument is specified -SIGNALS=(${@:-./signals/root_or_admin_update_1.json}) +RULES=(${@:-./rules/root_or_admin_update_1.json}) -# Example: ./update_signal.sh -# Example: ./update_signal.sh ./signals/root_or_admin_1.json -# Example glob: ./post_signal.sh ./signals/* -for SIGNAL in "${SIGNALS[@]}" +# Example: ./update_rule.sh +# Example: ./update_rule.sh ./rules/root_or_admin_1.json +# Example glob: ./post_rule.sh ./rules/* +for RULE in "${RULES[@]}" do { - [ -e "$SIGNAL" ] || continue + [ -e "$RULE" ] || continue curl -s -k \ -H 'Content-Type: application/json' \ -H 'kbn-xsrf: 123' \ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ -X PUT ${KIBANA_URL}${SPACE_URL}/api/detection_engine/rules \ - -d @${SIGNAL} \ + -d @${RULE} \ | jq .; } & done diff --git a/x-pack/legacy/plugins/siem/server/lib/types.ts b/x-pack/legacy/plugins/siem/server/lib/types.ts index 13d040b969545a..9c0059d0d109db 100644 --- a/x-pack/legacy/plugins/siem/server/lib/types.ts +++ b/x-pack/legacy/plugins/siem/server/lib/types.ts @@ -23,7 +23,7 @@ import { Note } from './note/saved_object'; import { PinnedEvent } from './pinned_event/saved_object'; import { Timeline } from './timeline/saved_object'; import { TLS } from './tls'; -import { SignalAlertParamsRest } from './detection_engine/alerts/types'; +import { RuleAlertParamsRest } from './detection_engine/alerts/types'; export * from './hosts'; @@ -66,7 +66,7 @@ export interface SiemContext { } export interface Signal { - rule: Partial; + rule: Partial; parent: { id: string; type: string; From e721ec4ca8d5089d0778579893be0e3c77081f47 Mon Sep 17 00:00:00 2001 From: Nathan L Smith Date: Tue, 26 Nov 2019 09:17:06 -0600 Subject: [PATCH 03/12] [APM] Replace StaticIndexPattern with IIndexPattern (#51689) --- .../app/Main/__test__/UpdateBreadcrumbs.test.js | 1 - .../apm/public/components/shared/KueryBar/index.tsx | 11 ++++------- .../plugins/apm/server/lib/helpers/setup_request.ts | 3 +-- .../lib/index_pattern/get_dynamic_index_pattern.ts | 3 +-- 4 files changed, 6 insertions(+), 12 deletions(-) diff --git a/x-pack/legacy/plugins/apm/public/components/app/Main/__test__/UpdateBreadcrumbs.test.js b/x-pack/legacy/plugins/apm/public/components/app/Main/__test__/UpdateBreadcrumbs.test.js index 8ddf48e79f911d..41fb12be284ad4 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/Main/__test__/UpdateBreadcrumbs.test.js +++ b/x-pack/legacy/plugins/apm/public/components/app/Main/__test__/UpdateBreadcrumbs.test.js @@ -10,7 +10,6 @@ import { MemoryRouter } from 'react-router-dom'; import { UpdateBreadcrumbs } from '../UpdateBreadcrumbs'; import * as kibanaCore from '../../../../../../observability/public/context/kibana_core'; -jest.mock('ui/index_patterns'); jest.mock('ui/new_platform'); const coreMock = { diff --git a/x-pack/legacy/plugins/apm/public/components/shared/KueryBar/index.tsx b/x-pack/legacy/plugins/apm/public/components/shared/KueryBar/index.tsx index 24d320505c9946..52be4d4fba7748 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/KueryBar/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/KueryBar/index.tsx @@ -7,7 +7,6 @@ import React, { useState } from 'react'; import { uniqueId, startsWith } from 'lodash'; import styled from 'styled-components'; -import { StaticIndexPattern } from 'ui/index_patterns'; import { fromKueryExpression, toElasticsearchQuery } from '@kbn/es-query'; import { i18n } from '@kbn/i18n'; import { fromQuery, toQuery } from '../Links/url_helpers'; @@ -19,7 +18,8 @@ import { useUrlParams } from '../../../hooks/useUrlParams'; import { history } from '../../../utils/history'; import { AutocompleteSuggestion, - AutocompleteProvider + AutocompleteProvider, + IIndexPattern } from '../../../../../../../../src/plugins/data/public'; import { useDynamicIndexPattern } from '../../../hooks/useDynamicIndexPattern'; import { usePlugins } from '../../../new-platform/plugin'; @@ -33,10 +33,7 @@ interface State { isLoadingSuggestions: boolean; } -function convertKueryToEsQuery( - kuery: string, - indexPattern: StaticIndexPattern -) { +function convertKueryToEsQuery(kuery: string, indexPattern: IIndexPattern) { const ast = fromKueryExpression(kuery); return toElasticsearchQuery(ast, indexPattern); } @@ -44,7 +41,7 @@ function convertKueryToEsQuery( function getSuggestions( query: string, selectionStart: number, - indexPattern: StaticIndexPattern, + indexPattern: IIndexPattern, boolFilter: unknown, autocompleteProvider?: AutocompleteProvider ) { diff --git a/x-pack/legacy/plugins/apm/server/lib/helpers/setup_request.ts b/x-pack/legacy/plugins/apm/server/lib/helpers/setup_request.ts index 8f19f4baed7ee9..a09cdbf91ec6ed 100644 --- a/x-pack/legacy/plugins/apm/server/lib/helpers/setup_request.ts +++ b/x-pack/legacy/plugins/apm/server/lib/helpers/setup_request.ts @@ -6,7 +6,6 @@ import moment from 'moment'; import { KibanaRequest } from 'src/core/server'; -import { StaticIndexPattern } from 'ui/index_patterns'; import { IIndexPattern } from 'src/plugins/data/common'; import { APMConfig } from '../../../../../../plugins/apm/server'; import { @@ -22,7 +21,7 @@ import { ProcessorEvent } from '../../../common/processor_event'; import { getDynamicIndexPattern } from '../index_pattern/get_dynamic_index_pattern'; function decodeUiFilters( - indexPattern: StaticIndexPattern | undefined, + indexPattern: IIndexPattern | undefined, uiFiltersEncoded?: string ) { if (!uiFiltersEncoded || !indexPattern) { diff --git a/x-pack/legacy/plugins/apm/server/lib/index_pattern/get_dynamic_index_pattern.ts b/x-pack/legacy/plugins/apm/server/lib/index_pattern/get_dynamic_index_pattern.ts index f113e645ed95fb..9eb99b7c21e751 100644 --- a/x-pack/legacy/plugins/apm/server/lib/index_pattern/get_dynamic_index_pattern.ts +++ b/x-pack/legacy/plugins/apm/server/lib/index_pattern/get_dynamic_index_pattern.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { StaticIndexPattern } from 'ui/index_patterns'; import { APICaller } from 'src/core/server'; import LRU from 'lru-cache'; import { @@ -51,7 +50,7 @@ export const getDynamicIndexPattern = async ({ pattern: patternIndices }); - const indexPattern: StaticIndexPattern = { + const indexPattern: IIndexPattern = { fields, title: indexPatternTitle }; From 38c17d6c7d03c2697f92f95acceb828fd513bb0c Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Tue, 26 Nov 2019 10:47:40 -0500 Subject: [PATCH 04/12] Improve session idle timeout, add session lifespan (#49855) This adds an absolute session timeout (lifespan) to user sessions. It also improves the existing session timeout toast and the overall user experience in several ways. --- docs/settings/security-settings.asciidoc | 12 +- .../security/authentication/index.asciidoc | 7 +- docs/user/security/securing-kibana.asciidoc | 27 +- .../resources/bin/kibana-docker | 2 + src/legacy/core_plugins/status_page/index.js | 5 + src/plugins/status_page/kibana.json | 6 + src/plugins/status_page/public/index.ts | 24 + src/plugins/status_page/public/plugin.ts | 39 ++ x-pack/legacy/plugins/security/index.js | 14 +- .../public/hacks/on_session_timeout.js | 14 +- .../public/views/logged_out/logged_out.tsx | 2 +- x-pack/package.json | 1 + x-pack/plugins/security/public/plugin.ts | 24 +- .../public/session/session_expired.test.ts | 97 ++-- .../public/session/session_expired.ts | 9 +- ... => session_idle_timeout_warning.test.tsx} | 8 +- .../session/session_idle_timeout_warning.tsx | 64 +++ .../session/session_lifespan_warning.tsx | 48 ++ .../public/session/session_timeout.mock.ts | 2 + .../public/session/session_timeout.test.tsx | 429 +++++++++++++----- .../public/session/session_timeout.tsx | 200 ++++++-- .../session_timeout_http_interceptor.ts | 4 +- .../session/session_timeout_warning.tsx | 39 -- ...thorized_response_http_interceptor.test.ts | 9 +- x-pack/plugins/security/public/types.ts | 12 + .../authentication/authenticator.test.ts | 340 ++++++++++++-- .../server/authentication/authenticator.ts | 82 +++- .../server/authentication/index.mock.ts | 1 + .../security/server/authentication/index.ts | 14 +- .../authentication/providers/token.test.ts | 10 +- .../server/authentication/providers/token.ts | 20 +- x-pack/plugins/security/server/config.test.ts | 97 ++-- x-pack/plugins/security/server/config.ts | 20 +- x-pack/plugins/security/server/plugin.test.ts | 11 +- x-pack/plugins/security/server/plugin.ts | 10 +- .../server/routes/authentication/index.ts | 2 + .../server/routes/authentication/session.ts | 46 ++ .../translations/translations/ja-JP.json | 5 +- .../translations/translations/zh-CN.json | 5 +- .../api_integration/apis/security/index.js | 1 + .../api_integration/apis/security/session.ts | 87 ++++ x-pack/test/api_integration/config.js | 1 + yarn.lock | 59 ++- 43 files changed, 1524 insertions(+), 385 deletions(-) create mode 100644 src/plugins/status_page/kibana.json create mode 100644 src/plugins/status_page/public/index.ts create mode 100644 src/plugins/status_page/public/plugin.ts rename x-pack/plugins/security/public/session/{session_timeout_warning.test.tsx => session_idle_timeout_warning.test.tsx} (71%) create mode 100644 x-pack/plugins/security/public/session/session_idle_timeout_warning.tsx create mode 100644 x-pack/plugins/security/public/session/session_lifespan_warning.tsx delete mode 100644 x-pack/plugins/security/public/session/session_timeout_warning.tsx create mode 100644 x-pack/plugins/security/public/types.ts create mode 100644 x-pack/plugins/security/server/routes/authentication/session.ts create mode 100644 x-pack/test/api_integration/apis/security/session.ts diff --git a/docs/settings/security-settings.asciidoc b/docs/settings/security-settings.asciidoc index 805d991a9a0f34..a2c05e4d873250 100644 --- a/docs/settings/security-settings.asciidoc +++ b/docs/settings/security-settings.asciidoc @@ -49,10 +49,16 @@ is set to `true` if `server.ssl.certificate` and `server.ssl.key` are set. Set this to `true` if SSL is configured outside of {kib} (for example, you are routing requests through a load balancer or proxy). -`xpack.security.sessionTimeout`:: +`xpack.security.session.idleTimeout`:: Sets the session duration (in milliseconds). By default, sessions stay active -until the browser is closed. When this is set to an explicit timeout, closing the -browser still requires the user to log back in to {kib}. +until the browser is closed. When this is set to an explicit idle timeout, closing +the browser still requires the user to log back in to {kib}. + +`xpack.security.session.lifespan`:: +Sets the maximum duration (in milliseconds), also known as "absolute timeout". By +default, a session can be renewed indefinitely. When this value is set, a session +will end once its lifespan is exceeded, even if the user is not idle. NOTE: if +`idleTimeout` is not set, this setting will still cause sessions to expire. `xpack.security.loginAssistanceMessage`:: Adds a message to the login screen. Useful for displaying information about maintenance windows, links to corporate sign up pages etc. diff --git a/docs/user/security/authentication/index.asciidoc b/docs/user/security/authentication/index.asciidoc index c2b1adc5e1b921..e6b70fa059fc28 100644 --- a/docs/user/security/authentication/index.asciidoc +++ b/docs/user/security/authentication/index.asciidoc @@ -188,9 +188,10 @@ The following sections apply both to <> and <> Once the user logs in to {kib} Single Sign-On, either using SAML or OpenID Connect, {es} issues access and refresh tokens that {kib} encrypts and stores them in its own session cookie. This way, the user isn't redirected to the Identity Provider -for every request that requires authentication. It also means that the {kib} session depends on the `xpack.security.sessionTimeout` -setting and the user is automatically logged out if the session expires. An access token that is stored in the session cookie -can expire, in which case {kib} will automatically renew it with a one-time-use refresh token and store it in the same cookie. +for every request that requires authentication. It also means that the {kib} session depends on the <> settings, and the user is automatically logged +out if the session expires. An access token that is stored in the session cookie can expire, in which case {kib} will +automatically renew it with a one-time-use refresh token and store it in the same cookie. {kib} can only determine if an access token has expired if it receives a request that requires authentication. If both access and refresh tokens have already expired (for example, after 24 hours of inactivity), {kib} initiates a new "handshake" and diff --git a/docs/user/security/securing-kibana.asciidoc b/docs/user/security/securing-kibana.asciidoc index 1c74bd98642a7c..2fbc6ba4f1ee64 100644 --- a/docs/user/security/securing-kibana.asciidoc +++ b/docs/user/security/securing-kibana.asciidoc @@ -56,16 +56,31 @@ xpack.security.encryptionKey: "something_at_least_32_characters" For more information, see <>. -- -. Optional: Change the default session duration. By default, sessions stay -active until the browser is closed. To change the duration, set the -`xpack.security.sessionTimeout` property in the `kibana.yml` configuration file. -The timeout is specified in milliseconds. For example, set the timeout to 600000 -to expire sessions after 10 minutes: +. Optional: Set a timeout to expire idle sessions. By default, a session stays +active until the browser is closed. To define a sliding session expiration, set +the `xpack.security.session.idleTimeout` property in the `kibana.yml` +configuration file. The idle timeout is specified in milliseconds. For example, +set the idle timeout to 600000 to expire idle sessions after 10 minutes: + -- [source,yaml] -------------------------------------------------------------------------------- -xpack.security.sessionTimeout: 600000 +xpack.security.session.idleTimeout: 600000 +-------------------------------------------------------------------------------- +-- + +. Optional: Change the maximum session duration or "lifespan" -- also known as +the "absolute timeout". By default, a session stays active until the browser is +closed. If an idle timeout is defined, a session can still be extended +indefinitely. To define a maximum session lifespan, set the +`xpack.security.session.lifespan` property in the `kibana.yml` configuration +file. The lifespan is specified in milliseconds. For example, set the lifespan +to 28800000 to expire sessions after 8 hours: ++ +-- +[source,yaml] +-------------------------------------------------------------------------------- +xpack.security.session.lifespan: 28800000 -------------------------------------------------------------------------------- -- diff --git a/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker b/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker index 497307fa4124b3..0c8faf47411d4e 100755 --- a/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker +++ b/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker @@ -180,6 +180,8 @@ kibana_vars=( xpack.security.encryptionKey xpack.security.secureCookies xpack.security.sessionTimeout + xpack.security.session.idleTimeout + xpack.security.session.lifespan xpack.security.loginAssistanceMessage telemetry.enabled telemetry.sendUsageFrom diff --git a/src/legacy/core_plugins/status_page/index.js b/src/legacy/core_plugins/status_page/index.js index 34de58048b887a..9f0ad632fd5b16 100644 --- a/src/legacy/core_plugins/status_page/index.js +++ b/src/legacy/core_plugins/status_page/index.js @@ -26,6 +26,11 @@ export default function (kibana) { hidden: true, url: '/status', }, + injectDefaultVars(server) { + return { + isStatusPageAnonymous: server.config().get('status.allowAnonymous'), + }; + } } }); } diff --git a/src/plugins/status_page/kibana.json b/src/plugins/status_page/kibana.json new file mode 100644 index 00000000000000..edebf8cb122391 --- /dev/null +++ b/src/plugins/status_page/kibana.json @@ -0,0 +1,6 @@ +{ + "id": "status_page", + "version": "kibana", + "server": false, + "ui": true +} diff --git a/src/plugins/status_page/public/index.ts b/src/plugins/status_page/public/index.ts new file mode 100644 index 00000000000000..db1f05cac076fb --- /dev/null +++ b/src/plugins/status_page/public/index.ts @@ -0,0 +1,24 @@ +/* + * 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 { PluginInitializer } from 'kibana/public'; +import { StatusPagePlugin, StatusPagePluginSetup, StatusPagePluginStart } from './plugin'; + +export const plugin: PluginInitializer = () => + new StatusPagePlugin(); diff --git a/src/plugins/status_page/public/plugin.ts b/src/plugins/status_page/public/plugin.ts new file mode 100644 index 00000000000000..d072fd4a67c306 --- /dev/null +++ b/src/plugins/status_page/public/plugin.ts @@ -0,0 +1,39 @@ +/* + * 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 { Plugin, CoreSetup } from 'kibana/public'; + +export class StatusPagePlugin implements Plugin { + public setup(core: CoreSetup) { + const isStatusPageAnonymous = core.injectedMetadata.getInjectedVar( + 'isStatusPageAnonymous' + ) as boolean; + + if (isStatusPageAnonymous) { + core.http.anonymousPaths.register('/status'); + } + } + + public start() {} + + public stop() {} +} + +export type StatusPagePluginSetup = ReturnType; +export type StatusPagePluginStart = ReturnType; diff --git a/x-pack/legacy/plugins/security/index.js b/x-pack/legacy/plugins/security/index.js index d147c2572ceeb5..60374d562f96c5 100644 --- a/x-pack/legacy/plugins/security/index.js +++ b/x-pack/legacy/plugins/security/index.js @@ -29,7 +29,10 @@ export const security = (kibana) => new kibana.Plugin({ enabled: Joi.boolean().default(true), cookieName: Joi.any().description('This key is handled in the new platform security plugin ONLY'), encryptionKey: Joi.any().description('This key is handled in the new platform security plugin ONLY'), - sessionTimeout: Joi.any().description('This key is handled in the new platform security plugin ONLY'), + session: Joi.object({ + idleTimeout: Joi.any().description('This key is handled in the new platform security plugin ONLY'), + lifespan: Joi.any().description('This key is handled in the new platform security plugin ONLY'), + }).default(), secureCookies: Joi.any().description('This key is handled in the new platform security plugin ONLY'), loginAssistanceMessage: Joi.string().default(), authorization: Joi.object({ @@ -44,9 +47,10 @@ export const security = (kibana) => new kibana.Plugin({ }).default(); }, - deprecations: function ({ unused }) { + deprecations: function ({ rename, unused }) { return [ unused('authorization.legacyFallback.enabled'), + rename('sessionTimeout', 'session.idleTimeout'), ]; }, @@ -89,7 +93,11 @@ export const security = (kibana) => new kibana.Plugin({ return { secureCookies: securityPlugin.__legacyCompat.config.secureCookies, - sessionTimeout: securityPlugin.__legacyCompat.config.sessionTimeout, + session: { + tenant: server.newPlatform.setup.core.http.basePath.serverBasePath, + idleTimeout: securityPlugin.__legacyCompat.config.session.idleTimeout, + lifespan: securityPlugin.__legacyCompat.config.session.lifespan, + }, enableSpaceAwarePrivileges: server.config().get('xpack.spaces.enabled'), }; }, diff --git a/x-pack/legacy/plugins/security/public/hacks/on_session_timeout.js b/x-pack/legacy/plugins/security/public/hacks/on_session_timeout.js index 81b14ee7d8bf41..d9fb4507794113 100644 --- a/x-pack/legacy/plugins/security/public/hacks/on_session_timeout.js +++ b/x-pack/legacy/plugins/security/public/hacks/on_session_timeout.js @@ -7,28 +7,20 @@ import _ from 'lodash'; import { uiModules } from 'ui/modules'; import { isSystemApiRequest } from 'ui/system_api'; -import { Path } from 'plugins/xpack_main/services/path'; import { npSetup } from 'ui/new_platform'; -/** - * Client session timeout is decreased by this number so that Kibana server - * can still access session content during logout request to properly clean - * user session up (invalidate access tokens, redirect to logout portal etc.). - * @type {number} - */ - const module = uiModules.get('security', []); module.config(($httpProvider) => { $httpProvider.interceptors.push(( $q, ) => { - const isUnauthenticated = Path.isUnauthenticated(); + const isAnonymous = npSetup.core.http.anonymousPaths.isAnonymous(window.location.pathname); function interceptorFactory(responseHandler) { return function interceptor(response) { - if (!isUnauthenticated && !isSystemApiRequest(response.config)) { - npSetup.plugins.security.sessionTimeout.extend(); + if (!isAnonymous && !isSystemApiRequest(response.config)) { + npSetup.plugins.security.sessionTimeout.extend(response.config.url); } return responseHandler(response); }; diff --git a/x-pack/legacy/plugins/security/public/views/logged_out/logged_out.tsx b/x-pack/legacy/plugins/security/public/views/logged_out/logged_out.tsx index 369b531e8ddf80..dbeb68875c1a9b 100644 --- a/x-pack/legacy/plugins/security/public/views/logged_out/logged_out.tsx +++ b/x-pack/legacy/plugins/security/public/views/logged_out/logged_out.tsx @@ -31,7 +31,7 @@ chrome } > - + , diff --git a/x-pack/package.json b/x-pack/package.json index bc7b220bf81f50..eccc5918e6d506 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -210,6 +210,7 @@ "bluebird": "3.5.5", "boom": "^7.2.0", "brace": "0.11.1", + "broadcast-channel": "^3.0.3", "chroma-js": "^1.4.1", "classnames": "2.2.6", "concat-stream": "1.6.2", diff --git a/x-pack/plugins/security/public/plugin.ts b/x-pack/plugins/security/public/plugin.ts index 55d125bf993ec6..7b1a554e1d3f12 100644 --- a/x-pack/plugins/security/public/plugin.ts +++ b/x-pack/plugins/security/public/plugin.ts @@ -13,6 +13,8 @@ import { } from './session'; export class SecurityPlugin implements Plugin { + private sessionTimeout!: SessionTimeout; + public setup(core: CoreSetup) { const { http, notifications, injectedMetadata } = core; const { basePath, anonymousPaths } = http; @@ -20,23 +22,25 @@ export class SecurityPlugin implements Plugin; diff --git a/x-pack/plugins/security/public/session/session_expired.test.ts b/x-pack/plugins/security/public/session/session_expired.test.ts index 9c0e4cd8036cc9..678c397dfbc64d 100644 --- a/x-pack/plugins/security/public/session/session_expired.test.ts +++ b/x-pack/plugins/security/public/session/session_expired.test.ts @@ -7,40 +7,81 @@ import { coreMock } from 'src/core/public/mocks'; import { SessionExpired } from './session_expired'; -const mockCurrentUrl = (url: string) => window.history.pushState({}, '', url); - -it('redirects user to "/logout" when there is no basePath', async () => { - const { basePath } = coreMock.createSetup().http; - mockCurrentUrl('/foo/bar?baz=quz#quuz'); - const sessionExpired = new SessionExpired(basePath); - const newUrlPromise = new Promise(resolve => { - jest.spyOn(window.location, 'assign').mockImplementation(url => { - resolve(url); +describe('Session Expiration', () => { + const mockGetItem = jest.fn().mockReturnValue(null); + + beforeAll(() => { + Object.defineProperty(window, 'sessionStorage', { + value: { + getItem: mockGetItem, + }, + writable: true, }); }); - sessionExpired.logout(); + afterAll(() => { + delete (window as any).sessionStorage; + }); + + describe('logout', () => { + const mockCurrentUrl = (url: string) => window.history.pushState({}, '', url); + const tenant = ''; - const url = await newUrlPromise; - expect(url).toBe( - `/logout?next=${encodeURIComponent('/foo/bar?baz=quz#quuz')}&msg=SESSION_EXPIRED` - ); -}); + it('redirects user to "/logout" when there is no basePath', async () => { + const { basePath } = coreMock.createSetup().http; + mockCurrentUrl('/foo/bar?baz=quz#quuz'); + const sessionExpired = new SessionExpired(basePath, tenant); + const newUrlPromise = new Promise(resolve => { + jest.spyOn(window.location, 'assign').mockImplementation(url => { + resolve(url); + }); + }); -it('redirects user to "/${basePath}/logout" and removes basePath from next parameter when there is a basePath', async () => { - const { basePath } = coreMock.createSetup({ basePath: '/foo' }).http; - mockCurrentUrl('/foo/bar?baz=quz#quuz'); - const sessionExpired = new SessionExpired(basePath); - const newUrlPromise = new Promise(resolve => { - jest.spyOn(window.location, 'assign').mockImplementation(url => { - resolve(url); + sessionExpired.logout(); + + const url = await newUrlPromise; + expect(url).toBe( + `/logout?next=${encodeURIComponent('/foo/bar?baz=quz#quuz')}&msg=SESSION_EXPIRED` + ); }); - }); - sessionExpired.logout(); + it('adds a provider parameter when an auth provider is saved in sessionStorage', async () => { + const { basePath } = coreMock.createSetup().http; + mockCurrentUrl('/foo/bar?baz=quz#quuz'); + const sessionExpired = new SessionExpired(basePath, tenant); + const newUrlPromise = new Promise(resolve => { + jest.spyOn(window.location, 'assign').mockImplementation(url => { + resolve(url); + }); + }); + mockGetItem.mockReturnValueOnce('basic'); + + sessionExpired.logout(); - const url = await newUrlPromise; - expect(url).toBe( - `/foo/logout?next=${encodeURIComponent('/bar?baz=quz#quuz')}&msg=SESSION_EXPIRED` - ); + const url = await newUrlPromise; + expect(url).toBe( + `/logout?next=${encodeURIComponent( + '/foo/bar?baz=quz#quuz' + )}&msg=SESSION_EXPIRED&provider=basic` + ); + }); + + it('redirects user to "/${basePath}/logout" and removes basePath from next parameter when there is a basePath', async () => { + const { basePath } = coreMock.createSetup({ basePath: '/foo' }).http; + mockCurrentUrl('/foo/bar?baz=quz#quuz'); + const sessionExpired = new SessionExpired(basePath, tenant); + const newUrlPromise = new Promise(resolve => { + jest.spyOn(window.location, 'assign').mockImplementation(url => { + resolve(url); + }); + }); + + sessionExpired.logout(); + + const url = await newUrlPromise; + expect(url).toBe( + `/foo/logout?next=${encodeURIComponent('/bar?baz=quz#quuz')}&msg=SESSION_EXPIRED` + ); + }); + }); }); diff --git a/x-pack/plugins/security/public/session/session_expired.ts b/x-pack/plugins/security/public/session/session_expired.ts index 3ef15088bb2889..a43da855267570 100644 --- a/x-pack/plugins/security/public/session/session_expired.ts +++ b/x-pack/plugins/security/public/session/session_expired.ts @@ -11,14 +11,19 @@ export interface ISessionExpired { } export class SessionExpired { - constructor(private basePath: HttpSetup['basePath']) {} + constructor(private basePath: HttpSetup['basePath'], private tenant: string) {} logout() { const next = this.basePath.remove( `${window.location.pathname}${window.location.search}${window.location.hash}` ); + const key = `${this.tenant}/session_provider`; + const providerName = sessionStorage.getItem(key); + const provider = providerName ? `&provider=${encodeURIComponent(providerName)}` : ''; window.location.assign( - this.basePath.prepend(`/logout?next=${encodeURIComponent(next)}&msg=SESSION_EXPIRED`) + this.basePath.prepend( + `/logout?next=${encodeURIComponent(next)}&msg=SESSION_EXPIRED${provider}` + ) ); } } diff --git a/x-pack/plugins/security/public/session/session_timeout_warning.test.tsx b/x-pack/plugins/security/public/session/session_idle_timeout_warning.test.tsx similarity index 71% rename from x-pack/plugins/security/public/session/session_timeout_warning.test.tsx rename to x-pack/plugins/security/public/session/session_idle_timeout_warning.test.tsx index a52e7ce4e94b52..bb4116420f15da 100644 --- a/x-pack/plugins/security/public/session/session_timeout_warning.test.tsx +++ b/x-pack/plugins/security/public/session/session_idle_timeout_warning.test.tsx @@ -5,12 +5,14 @@ */ import React from 'react'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; -import { SessionTimeoutWarning } from './session_timeout_warning'; +import { SessionIdleTimeoutWarning } from './session_idle_timeout_warning'; -describe('SessionTimeoutWarning', () => { +describe('SessionIdleTimeoutWarning', () => { it('fires its callback when the OK button is clicked', () => { const handler = jest.fn(); - const wrapper = mountWithIntl(); + const wrapper = mountWithIntl( + + ); expect(handler).toBeCalledTimes(0); wrapper.find('EuiButton[data-test-subj="refreshSessionButton"]').simulate('click'); diff --git a/x-pack/plugins/security/public/session/session_idle_timeout_warning.tsx b/x-pack/plugins/security/public/session/session_idle_timeout_warning.tsx new file mode 100644 index 00000000000000..32e4dcc5c6b531 --- /dev/null +++ b/x-pack/plugins/security/public/session/session_idle_timeout_warning.tsx @@ -0,0 +1,64 @@ +/* + * 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 { ToastInput } from 'src/core/public'; +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiButton, EuiProgress } from '@elastic/eui'; +import { FormattedMessage, FormattedRelative } from '@kbn/i18n/react'; +import { toMountPoint } from '../../../../../src/plugins/kibana_react/public'; + +interface Props { + onRefreshSession: () => void; + timeout: number; +} + +export const SessionIdleTimeoutWarning = (props: Props) => { + return ( + <> + +

+ + ), + }} + /> +

+
+ + + +
+ + ); +}; + +export const createToast = (toastLifeTimeMs: number, onRefreshSession: () => void): ToastInput => { + const timeout = toastLifeTimeMs + Date.now(); + return { + color: 'warning', + text: toMountPoint( + + ), + title: i18n.translate('xpack.security.components.sessionIdleTimeoutWarning.title', { + defaultMessage: 'Warning', + }), + iconType: 'clock', + toastLifeTimeMs, + }; +}; diff --git a/x-pack/plugins/security/public/session/session_lifespan_warning.tsx b/x-pack/plugins/security/public/session/session_lifespan_warning.tsx new file mode 100644 index 00000000000000..7925e92bce4edf --- /dev/null +++ b/x-pack/plugins/security/public/session/session_lifespan_warning.tsx @@ -0,0 +1,48 @@ +/* + * 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 { ToastInput } from 'src/core/public'; +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage, FormattedRelative } from '@kbn/i18n/react'; +import { EuiProgress } from '@elastic/eui'; +import { toMountPoint } from '../../../../../src/plugins/kibana_react/public'; + +interface Props { + timeout: number; +} + +export const SessionLifespanWarning = (props: Props) => { + return ( + <> + +

+ + ), + }} + /> +

+ + ); +}; + +export const createToast = (toastLifeTimeMs: number): ToastInput => { + const timeout = toastLifeTimeMs + Date.now(); + return { + color: 'danger', + text: toMountPoint(), + title: i18n.translate('xpack.security.components.sessionLifespanWarning.title', { + defaultMessage: 'Warning', + }), + iconType: 'alert', + toastLifeTimeMs, + }; +}; diff --git a/x-pack/plugins/security/public/session/session_timeout.mock.ts b/x-pack/plugins/security/public/session/session_timeout.mock.ts index 9917a502790833..df9b8628b180d2 100644 --- a/x-pack/plugins/security/public/session/session_timeout.mock.ts +++ b/x-pack/plugins/security/public/session/session_timeout.mock.ts @@ -8,6 +8,8 @@ import { ISessionTimeout } from './session_timeout'; export function createSessionTimeoutMock() { return { + start: jest.fn(), + stop: jest.fn(), extend: jest.fn(), } as jest.Mocked; } diff --git a/x-pack/plugins/security/public/session/session_timeout.test.tsx b/x-pack/plugins/security/public/session/session_timeout.test.tsx index 80a22c5fb0b2ae..eb947ab95c43b6 100644 --- a/x-pack/plugins/security/public/session/session_timeout.test.tsx +++ b/x-pack/plugins/security/public/session/session_timeout.test.tsx @@ -5,6 +5,7 @@ */ import { coreMock } from 'src/core/public/mocks'; +import BroadcastChannel from 'broadcast-channel'; import { SessionTimeout } from './session_timeout'; import { createSessionExpiredMock } from './session_expired.mock'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; @@ -17,25 +18,46 @@ const expectNoWarningToast = ( expect(notifications.toasts.add).not.toHaveBeenCalled(); }; -const expectWarningToast = ( +const expectIdleTimeoutWarningToast = ( notifications: ReturnType['notifications'], - toastLifeTimeMS: number = 60000 + toastLifeTimeMs: number = 60000 ) => { expect(notifications.toasts.add).toHaveBeenCalledTimes(1); - expect(notifications.toasts.add.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - Object { - "color": "warning", - "text": MountPoint { - "reactNode": , - }, - "title": "Warning", - "toastLifeTimeMs": ${toastLifeTimeMS}, - }, - ] - `); + expect(notifications.toasts.add.mock.calls[0][0]).toMatchInlineSnapshot( + { + text: expect.any(Function), + }, + ` + Object { + "color": "warning", + "iconType": "clock", + "text": Any, + "title": "Warning", + "toastLifeTimeMs": ${toastLifeTimeMs}, + } + ` + ); +}; + +const expectLifespanWarningToast = ( + notifications: ReturnType['notifications'], + toastLifeTimeMs: number = 60000 +) => { + expect(notifications.toasts.add).toHaveBeenCalledTimes(1); + expect(notifications.toasts.add.mock.calls[0][0]).toMatchInlineSnapshot( + { + text: expect.any(Function), + }, + ` + Object { + "color": "danger", + "iconType": "alert", + "text": Any, + "title": "Warning", + "toastLifeTimeMs": ${toastLifeTimeMs}, + } + ` + ); }; const expectWarningToastHidden = ( @@ -46,128 +68,309 @@ const expectWarningToastHidden = ( expect(notifications.toasts.remove).toHaveBeenCalledWith(toast); }; -describe('warning toast', () => { - test(`shows session expiration warning toast`, () => { - const { notifications, http } = coreMock.createSetup(); - const sessionExpired = createSessionExpiredMock(); - const sessionTimeout = new SessionTimeout(2 * 60 * 1000, notifications, sessionExpired, http); +describe('Session Timeout', () => { + const now = new Date().getTime(); + const defaultSessionInfo = { + now, + idleTimeoutExpiration: now + 2 * 60 * 1000, + lifespanExpiration: null, + }; + let notifications: ReturnType['notifications']; + let http: ReturnType['http']; + let sessionExpired: ReturnType; + let sessionTimeout: SessionTimeout; + const toast = Symbol(); - sessionTimeout.extend(); - // we display the warning a minute before we expire the the session, which is 5 seconds before it actually expires - jest.advanceTimersByTime(55 * 1000); - expectWarningToast(notifications); + beforeAll(() => { + BroadcastChannel.enforceOptions({ + type: 'simulate', + }); + Object.defineProperty(window, 'sessionStorage', { + value: { + setItem: jest.fn(() => null), + }, + writable: true, + }); }); - test(`extend delays the warning toast`, () => { - const { notifications, http } = coreMock.createSetup(); - const sessionExpired = createSessionExpiredMock(); - const sessionTimeout = new SessionTimeout(2 * 60 * 1000, notifications, sessionExpired, http); - - sessionTimeout.extend(); - jest.advanceTimersByTime(54 * 1000); - expectNoWarningToast(notifications); + beforeEach(() => { + const setup = coreMock.createSetup(); + notifications = setup.notifications; + http = setup.http; + notifications.toasts.add.mockReturnValue(toast as any); + sessionExpired = createSessionExpiredMock(); + const tenant = ''; + sessionTimeout = new SessionTimeout(notifications, sessionExpired, http, tenant); - sessionTimeout.extend(); - jest.advanceTimersByTime(54 * 1000); - expectNoWarningToast(notifications); + // default mocked response for checking session info + http.fetch.mockResolvedValue(defaultSessionInfo); + }); - jest.advanceTimersByTime(1 * 1000); + afterEach(async () => { + jest.clearAllMocks(); + }); - expectWarningToast(notifications); + afterAll(() => { + BroadcastChannel.enforceOptions(null); + delete (window as any).sessionStorage; }); - test(`extend hides displayed warning toast`, () => { - const { notifications, http } = coreMock.createSetup(); - const toast = Symbol(); - notifications.toasts.add.mockReturnValue(toast as any); - const sessionExpired = createSessionExpiredMock(); - const sessionTimeout = new SessionTimeout(2 * 60 * 1000, notifications, sessionExpired, http); + describe('Lifecycle', () => { + test(`starts and initializes on a non-anonymous path`, async () => { + await sessionTimeout.start(); + // eslint-disable-next-line dot-notation + expect(sessionTimeout['channel']).not.toBeUndefined(); + expect(http.fetch).toHaveBeenCalledTimes(1); + }); - sessionTimeout.extend(); - // we display the warning a minute before we expire the the session, which is 5 seconds before it actually expires - jest.advanceTimersByTime(55 * 1000); - expectWarningToast(notifications); + test(`starts and does not initialize on an anonymous path`, async () => { + http.anonymousPaths.register(window.location.pathname); + await sessionTimeout.start(); + // eslint-disable-next-line dot-notation + expect(sessionTimeout['channel']).toBeUndefined(); + expect(http.fetch).not.toHaveBeenCalled(); + }); - sessionTimeout.extend(); - expectWarningToastHidden(notifications, toast); - }); + test(`stops`, async () => { + await sessionTimeout.start(); + // eslint-disable-next-line dot-notation + const close = jest.fn(sessionTimeout['channel']!.close); + // eslint-disable-next-line dot-notation + sessionTimeout['channel']!.close = close; + // eslint-disable-next-line dot-notation + const cleanup = jest.fn(sessionTimeout['cleanup']); + // eslint-disable-next-line dot-notation + sessionTimeout['cleanup'] = cleanup; - test('clicking "extend" causes a new HTTP request (which implicitly extends the session)', () => { - const { notifications, http } = coreMock.createSetup(); - const sessionExpired = createSessionExpiredMock(); - const sessionTimeout = new SessionTimeout(2 * 60 * 1000, notifications, sessionExpired, http); - - sessionTimeout.extend(); - // we display the warning a minute before we expire the the session, which is 5 seconds before it actually expires - jest.advanceTimersByTime(55 * 1000); - expectWarningToast(notifications); - - expect(http.get).not.toHaveBeenCalled(); - const toastInput = notifications.toasts.add.mock.calls[0][0]; - expect(toastInput).toHaveProperty('text'); - const mountPoint = (toastInput as any).text; - const wrapper = mountWithIntl(mountPoint.__reactMount__); - wrapper.find('EuiButton[data-test-subj="refreshSessionButton"]').simulate('click'); - expect(http.get).toHaveBeenCalled(); + sessionTimeout.stop(); + expect(close).toHaveBeenCalled(); + expect(cleanup).toHaveBeenCalled(); + }); }); - test('when the session timeout is shorter than 65 seconds, display the warning immediately and for a shorter duration', () => { - const { notifications, http } = coreMock.createSetup(); - const sessionExpired = createSessionExpiredMock(); - const sessionTimeout = new SessionTimeout(64 * 1000, notifications, sessionExpired, http); + describe('API calls', () => { + const methodName = 'handleSessionInfoAndResetTimers'; + let method: jest.Mock; - sessionTimeout.extend(); - jest.advanceTimersByTime(0); - expectWarningToast(notifications, 59 * 1000); - }); -}); + beforeEach(() => { + method = jest.fn(sessionTimeout[methodName]); + sessionTimeout[methodName] = method; + }); -describe('session expiration', () => { - test(`expires the session 5 seconds before it really expires`, () => { - const { notifications, http } = coreMock.createSetup(); - const sessionExpired = createSessionExpiredMock(); - const sessionTimeout = new SessionTimeout(2 * 60 * 1000, notifications, sessionExpired, http); + test(`handles success`, async () => { + await sessionTimeout.start(); - sessionTimeout.extend(); - jest.advanceTimersByTime(114 * 1000); - expect(sessionExpired.logout).not.toHaveBeenCalled(); + expect(http.fetch).toHaveBeenCalledTimes(1); + // eslint-disable-next-line dot-notation + expect(sessionTimeout['sessionInfo']).toBe(defaultSessionInfo); + expect(method).toHaveBeenCalledTimes(1); + }); - jest.advanceTimersByTime(1 * 1000); - expect(sessionExpired.logout).toHaveBeenCalled(); + test(`handles error`, async () => { + const mockErrorResponse = new Error('some-error'); + http.fetch.mockRejectedValue(mockErrorResponse); + await sessionTimeout.start(); + + expect(http.fetch).toHaveBeenCalledTimes(1); + // eslint-disable-next-line dot-notation + expect(sessionTimeout['sessionInfo']).toBeUndefined(); + expect(method).not.toHaveBeenCalled(); + }); }); - test(`extend delays the expiration`, () => { - const { notifications, http } = coreMock.createSetup(); - const sessionExpired = createSessionExpiredMock(); - const sessionTimeout = new SessionTimeout(2 * 60 * 1000, notifications, sessionExpired, http); + describe('warning toast', () => { + test(`shows idle timeout warning toast`, async () => { + await sessionTimeout.start(); - sessionTimeout.extend(); - jest.advanceTimersByTime(114 * 1000); + // we display the warning a minute before we expire the the session, which is 5 seconds before it actually expires + jest.advanceTimersByTime(55 * 1000); + expectIdleTimeoutWarningToast(notifications); + }); - sessionTimeout.extend(); - jest.advanceTimersByTime(114 * 1000); - expect(sessionExpired.logout).not.toHaveBeenCalled(); + test(`shows lifespan warning toast`, async () => { + const sessionInfo = { + now, + idleTimeoutExpiration: null, + lifespanExpiration: now + 2 * 60 * 1000, + }; + http.fetch.mockResolvedValue(sessionInfo); + await sessionTimeout.start(); - jest.advanceTimersByTime(1 * 1000); - expect(sessionExpired.logout).toHaveBeenCalled(); - }); + // we display the warning a minute before we expire the the session, which is 5 seconds before it actually expires + jest.advanceTimersByTime(55 * 1000); + expectLifespanWarningToast(notifications); + }); + + test(`extend only results in an HTTP call if a warning is shown`, async () => { + await sessionTimeout.start(); + expect(http.fetch).toHaveBeenCalledTimes(1); + + await sessionTimeout.extend('/foo'); + expect(http.fetch).toHaveBeenCalledTimes(1); + jest.advanceTimersByTime(54 * 1000); + expect(http.fetch).toHaveBeenCalledTimes(2); + expectNoWarningToast(notifications); + + await sessionTimeout.extend('/foo'); + expect(http.fetch).toHaveBeenCalledTimes(2); + jest.advanceTimersByTime(10 * 1000); + expectIdleTimeoutWarningToast(notifications); + + await sessionTimeout.extend('/foo'); + expect(http.fetch).toHaveBeenCalledTimes(3); + }); + + test(`extend does not result in an HTTP call if a lifespan warning is shown`, async () => { + const sessionInfo = { + now, + idleTimeoutExpiration: null, + lifespanExpiration: now + 2 * 60 * 1000, + }; + http.fetch.mockResolvedValue(sessionInfo); + await sessionTimeout.start(); + + // we display the warning a minute before we expire the the session, which is 5 seconds before it actually expires + jest.advanceTimersByTime(55 * 1000); + expectLifespanWarningToast(notifications); - test(`if the session timeout is shorter than 5 seconds, expire session immediately`, () => { - const { notifications, http } = coreMock.createSetup(); - const sessionExpired = createSessionExpiredMock(); - const sessionTimeout = new SessionTimeout(4 * 1000, notifications, sessionExpired, http); + expect(http.fetch).toHaveBeenCalledTimes(1); + await sessionTimeout.extend('/foo'); + expect(http.fetch).toHaveBeenCalledTimes(1); + }); - sessionTimeout.extend(); - jest.advanceTimersByTime(0); - expect(sessionExpired.logout).toHaveBeenCalled(); + test(`extend hides displayed warning toast`, async () => { + await sessionTimeout.start(); + expect(http.fetch).toHaveBeenCalledTimes(1); + + // we display the warning a minute before we expire the the session, which is 5 seconds before it actually expires + const elapsed = 55 * 1000; + jest.advanceTimersByTime(elapsed); + expectIdleTimeoutWarningToast(notifications); + + http.fetch.mockResolvedValue({ + now: now + elapsed, + idleTimeoutExpiration: now + elapsed + 2 * 60 * 1000, + lifespanExpiration: null, + }); + await sessionTimeout.extend('/foo'); + expect(http.fetch).toHaveBeenCalledTimes(3); + expectWarningToastHidden(notifications, toast); + }); + + test(`extend does nothing for session-related routes`, async () => { + await sessionTimeout.start(); + expect(http.fetch).toHaveBeenCalledTimes(1); + + // we display the warning a minute before we expire the the session, which is 5 seconds before it actually expires + const elapsed = 55 * 1000; + jest.advanceTimersByTime(elapsed); + expect(http.fetch).toHaveBeenCalledTimes(2); + expectIdleTimeoutWarningToast(notifications); + + await sessionTimeout.extend('/internal/security/session'); + expect(http.fetch).toHaveBeenCalledTimes(2); + }); + + test(`checks for updated session info before the warning displays`, async () => { + await sessionTimeout.start(); + expect(http.fetch).toHaveBeenCalledTimes(1); + + // we check for updated session info 1 second before the warning is shown + const elapsed = 54 * 1000; + jest.advanceTimersByTime(elapsed); + expect(http.fetch).toHaveBeenCalledTimes(2); + }); + + test('clicking "extend" causes a new HTTP request (which implicitly extends the session)', async () => { + await sessionTimeout.start(); + expect(http.fetch).toHaveBeenCalledTimes(1); + + // we display the warning a minute before we expire the the session, which is 5 seconds before it actually expires + jest.advanceTimersByTime(55 * 1000); + expect(http.fetch).toHaveBeenCalledTimes(2); + expectIdleTimeoutWarningToast(notifications); + + const toastInput = notifications.toasts.add.mock.calls[0][0]; + expect(toastInput).toHaveProperty('text'); + const mountPoint = (toastInput as any).text; + const wrapper = mountWithIntl(mountPoint.__reactMount__); + wrapper.find('EuiButton[data-test-subj="refreshSessionButton"]').simulate('click'); + expect(http.fetch).toHaveBeenCalledTimes(3); + }); + + test('when the session timeout is shorter than 65 seconds, display the warning immediately and for a shorter duration', async () => { + http.fetch.mockResolvedValue({ + now, + idleTimeoutExpiration: now + 64 * 1000, + lifespanExpiration: null, + }); + await sessionTimeout.start(); + expect(http.fetch).toHaveBeenCalled(); + + jest.advanceTimersByTime(0); + expectIdleTimeoutWarningToast(notifications, 59 * 1000); + }); }); - test(`'null' sessionTimeout never logs you out`, () => { - const { notifications, http } = coreMock.createSetup(); - const sessionExpired = createSessionExpiredMock(); - const sessionTimeout = new SessionTimeout(null, notifications, sessionExpired, http); - sessionTimeout.extend(); - jest.advanceTimersByTime(Number.MAX_VALUE); - expect(sessionExpired.logout).not.toHaveBeenCalled(); + describe('session expiration', () => { + test(`expires the session 5 seconds before it really expires`, async () => { + await sessionTimeout.start(); + + jest.advanceTimersByTime(114 * 1000); + expect(sessionExpired.logout).not.toHaveBeenCalled(); + + jest.advanceTimersByTime(1 * 1000); + expect(sessionExpired.logout).toHaveBeenCalled(); + }); + + test(`extend delays the expiration`, async () => { + await sessionTimeout.start(); + expect(http.fetch).toHaveBeenCalledTimes(1); + + const elapsed = 114 * 1000; + jest.advanceTimersByTime(elapsed); + expect(http.fetch).toHaveBeenCalledTimes(2); + expectIdleTimeoutWarningToast(notifications); + + const sessionInfo = { + now: now + elapsed, + idleTimeoutExpiration: now + elapsed + 2 * 60 * 1000, + lifespanExpiration: null, + }; + http.fetch.mockResolvedValue(sessionInfo); + await sessionTimeout.extend('/foo'); + expect(http.fetch).toHaveBeenCalledTimes(3); + // eslint-disable-next-line dot-notation + expect(sessionTimeout['sessionInfo']).toEqual(sessionInfo); + + // at this point, the session is good for another 120 seconds + jest.advanceTimersByTime(114 * 1000); + expect(sessionExpired.logout).not.toHaveBeenCalled(); + + // because "extend" results in an async request and HTTP call, there is a slight delay when timers are updated + // so we need an extra 100ms of padding for this test to ensure that logout has been called + jest.advanceTimersByTime(1 * 1000 + 100); + expect(sessionExpired.logout).toHaveBeenCalled(); + }); + + test(`if the session timeout is shorter than 5 seconds, expire session immediately`, async () => { + http.fetch.mockResolvedValue({ + now, + idleTimeoutExpiration: now + 4 * 1000, + lifespanExpiration: null, + }); + await sessionTimeout.start(); + + jest.advanceTimersByTime(0); + expect(sessionExpired.logout).toHaveBeenCalled(); + }); + + test(`'null' sessionTimeout never logs you out`, async () => { + http.fetch.mockResolvedValue({ now, idleTimeoutExpiration: null, lifespanExpiration: null }); + await sessionTimeout.start(); + + jest.advanceTimersByTime(Number.MAX_VALUE); + expect(sessionExpired.logout).not.toHaveBeenCalled(); + }); }); }); diff --git a/x-pack/plugins/security/public/session/session_timeout.tsx b/x-pack/plugins/security/public/session/session_timeout.tsx index 32302effd6e464..0069e78b5f3722 100644 --- a/x-pack/plugins/security/public/session/session_timeout.tsx +++ b/x-pack/plugins/security/public/session/session_timeout.tsx @@ -4,12 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { NotificationsSetup, Toast, HttpSetup } from 'src/core/public'; -import React from 'react'; -import { i18n } from '@kbn/i18n'; -import { toMountPoint } from '../../../../../src/plugins/kibana_react/public'; -import { SessionTimeoutWarning } from './session_timeout_warning'; +import { NotificationsSetup, Toast, HttpSetup, ToastInput } from 'src/core/public'; +import { BroadcastChannel } from 'broadcast-channel'; +import { createToast as createIdleTimeoutToast } from './session_idle_timeout_warning'; +import { createToast as createLifespanToast } from './session_lifespan_warning'; import { ISessionExpired } from './session_expired'; +import { SessionInfo } from '../types'; /** * Client session timeout is decreased by this number so that Kibana server @@ -23,58 +23,188 @@ const GRACE_PERIOD_MS = 5 * 1000; */ const WARNING_MS = 60 * 1000; +/** + * Current session info is checked this number of milliseconds before the + * warning toast shows. This will prevent the toast from being shown if the + * session has already been extended. + */ +const SESSION_CHECK_MS = 1000; + +/** + * Route to get session info and extend session expiration + */ +const SESSION_ROUTE = '/internal/security/session'; + export interface ISessionTimeout { - extend(): void; + start(): void; + stop(): void; + extend(url: string): void; } export class SessionTimeout { - private warningTimeoutMilliseconds?: number; - private expirationTimeoutMilliseconds?: number; + private channel?: BroadcastChannel; + private sessionInfo?: SessionInfo; + private fetchTimer?: number; + private warningTimer?: number; + private expirationTimer?: number; private warningToast?: Toast; constructor( - private sessionTimeoutMilliseconds: number | null, private notifications: NotificationsSetup, private sessionExpired: ISessionExpired, - private http: HttpSetup + private http: HttpSetup, + private tenant: string ) {} - extend() { - if (this.sessionTimeoutMilliseconds == null) { + start() { + if (this.http.anonymousPaths.isAnonymous(window.location.pathname)) { return; } - if (this.warningTimeoutMilliseconds) { - window.clearTimeout(this.warningTimeoutMilliseconds); + // subscribe to a broadcast channel for session timeout messages + // this allows us to synchronize the UX across tabs and avoid repetitive API calls + const name = `${this.tenant}/session_timeout`; + this.channel = new BroadcastChannel(name, { webWorkerSupport: false }); + this.channel.onmessage = this.handleSessionInfoAndResetTimers; + + // Triggers an initial call to the endpoint to get session info; + // when that returns, it will set the timeout + return this.fetchSessionInfoAndResetTimers(); + } + + stop() { + if (this.channel) { + this.channel.close(); } - if (this.expirationTimeoutMilliseconds) { - window.clearTimeout(this.expirationTimeoutMilliseconds); + this.cleanup(); + } + + /** + * When the user makes an authenticated, non-system API call, this function is used to check + * and see if the session has been extended. + * @param url The URL that was called + */ + extend(url: string) { + // avoid an additional API calls when the user clicks the button on the session idle timeout + if (url.endsWith(SESSION_ROUTE)) { + return; } - if (this.warningToast) { - this.notifications.toasts.remove(this.warningToast); + + const { isLifespanTimeout } = this.getTimeout(); + if (this.warningToast && !isLifespanTimeout) { + // the idle timeout warning is currently showing and the user has clicked elsewhere on the page; + // make a new call to get the latest session info + return this.fetchSessionInfoAndResetTimers(); + } + } + + /** + * Fetch latest session information from the server, and optionally attempt to extend + * the session expiration. + */ + private fetchSessionInfoAndResetTimers = async (extend = false) => { + const method = extend ? 'POST' : 'GET'; + const headers = extend ? {} : { 'kbn-system-api': 'true' }; + try { + const result = await this.http.fetch(SESSION_ROUTE, { method, headers }); + + this.handleSessionInfoAndResetTimers(result); + + // share this updated session info with any other tabs to sync the UX + if (this.channel) { + this.channel.postMessage(result); + } + } catch (err) { + // do nothing; 401 errors will be caught by the http interceptor + } + }; + + /** + * Processes latest session information, and resets timers based on it. These timers are + * used to trigger an HTTP call for updated session information, to show a timeout + * warning, and to log the user out when their session is expired. + */ + private handleSessionInfoAndResetTimers = (sessionInfo: SessionInfo) => { + this.sessionInfo = sessionInfo; + // save the provider name in session storage, we will need it when we log out + const key = `${this.tenant}/session_provider`; + sessionStorage.setItem(key, sessionInfo.provider); + + const { timeout, isLifespanTimeout } = this.getTimeout(); + if (timeout == null) { + return; } - this.warningTimeoutMilliseconds = window.setTimeout( - () => this.showWarning(), - Math.max(this.sessionTimeoutMilliseconds - WARNING_MS - GRACE_PERIOD_MS, 0) + + this.cleanup(); + + // set timers + const timeoutVal = timeout - WARNING_MS - GRACE_PERIOD_MS - SESSION_CHECK_MS; + if (timeoutVal > 0 && !isLifespanTimeout) { + // we should check for the latest session info before the warning displays + this.fetchTimer = window.setTimeout(this.fetchSessionInfoAndResetTimers, timeoutVal); + } + this.warningTimer = window.setTimeout( + this.showWarning, + Math.max(timeout - WARNING_MS - GRACE_PERIOD_MS, 0) ); - this.expirationTimeoutMilliseconds = window.setTimeout( + this.expirationTimer = window.setTimeout( () => this.sessionExpired.logout(), - Math.max(this.sessionTimeoutMilliseconds - GRACE_PERIOD_MS, 0) + Math.max(timeout - GRACE_PERIOD_MS, 0) ); - } + }; - private showWarning = () => { - this.warningToast = this.notifications.toasts.add({ - color: 'warning', - text: toMountPoint(), - title: i18n.translate('xpack.security.components.sessionTimeoutWarning.title', { - defaultMessage: 'Warning', - }), - toastLifeTimeMs: Math.min(this.sessionTimeoutMilliseconds! - GRACE_PERIOD_MS, WARNING_MS), - }); + private cleanup = () => { + if (this.fetchTimer) { + window.clearTimeout(this.fetchTimer); + } + if (this.warningTimer) { + window.clearTimeout(this.warningTimer); + } + if (this.expirationTimer) { + window.clearTimeout(this.expirationTimer); + } + if (this.warningToast) { + this.notifications.toasts.remove(this.warningToast); + this.warningToast = undefined; + } }; - private refreshSession = () => { - this.http.get('/api/security/v1/me'); + /** + * Get the amount of time until the session times out, and whether or not the + * session has reached it maximum lifespan. + */ + private getTimeout = (): { timeout: number | null; isLifespanTimeout: boolean } => { + let timeout = null; + let isLifespanTimeout = false; + if (this.sessionInfo) { + const { now, idleTimeoutExpiration, lifespanExpiration } = this.sessionInfo; + if (idleTimeoutExpiration) { + timeout = idleTimeoutExpiration - now; + } + if ( + lifespanExpiration && + (idleTimeoutExpiration === null || lifespanExpiration <= idleTimeoutExpiration) + ) { + timeout = lifespanExpiration - now; + isLifespanTimeout = true; + } + } + return { timeout, isLifespanTimeout }; + }; + + /** + * Show a warning toast depending on the session state. + */ + private showWarning = () => { + const { timeout, isLifespanTimeout } = this.getTimeout(); + const toastLifeTimeMs = Math.min(timeout! - GRACE_PERIOD_MS, WARNING_MS); + let toast: ToastInput; + if (!isLifespanTimeout) { + const refresh = () => this.fetchSessionInfoAndResetTimers(true); + toast = createIdleTimeoutToast(toastLifeTimeMs, refresh); + } else { + toast = createLifespanToast(toastLifeTimeMs); + } + this.warningToast = this.notifications.toasts.add(toast); }; } diff --git a/x-pack/plugins/security/public/session/session_timeout_http_interceptor.ts b/x-pack/plugins/security/public/session/session_timeout_http_interceptor.ts index 98516cb4a613be..81625e1753b273 100644 --- a/x-pack/plugins/security/public/session/session_timeout_http_interceptor.ts +++ b/x-pack/plugins/security/public/session/session_timeout_http_interceptor.ts @@ -24,7 +24,7 @@ export class SessionTimeoutHttpInterceptor implements HttpInterceptor { return; } - this.sessionTimeout.extend(); + this.sessionTimeout.extend(httpResponse.request.url); } responseError(httpErrorResponse: HttpErrorResponse) { @@ -45,6 +45,6 @@ export class SessionTimeoutHttpInterceptor implements HttpInterceptor { return; } - this.sessionTimeout.extend(); + this.sessionTimeout.extend(httpErrorResponse.request.url); } } diff --git a/x-pack/plugins/security/public/session/session_timeout_warning.tsx b/x-pack/plugins/security/public/session/session_timeout_warning.tsx deleted file mode 100644 index e1b4542031ed1f..00000000000000 --- a/x-pack/plugins/security/public/session/session_timeout_warning.tsx +++ /dev/null @@ -1,39 +0,0 @@ -/* - * 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 React from 'react'; -import { EuiButton } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; - -interface Props { - onRefreshSession: () => void; -} - -export const SessionTimeoutWarning = (props: Props) => { - return ( - <> -

- -

-
- - - -
- - ); -}; diff --git a/x-pack/plugins/security/public/session/unauthorized_response_http_interceptor.test.ts b/x-pack/plugins/security/public/session/unauthorized_response_http_interceptor.test.ts index 6f339a6fc9c958..ff2db01cb6c587 100644 --- a/x-pack/plugins/security/public/session/unauthorized_response_http_interceptor.test.ts +++ b/x-pack/plugins/security/public/session/unauthorized_response_http_interceptor.test.ts @@ -25,6 +25,7 @@ const setupHttp = (basePath: string) => { }); return http; }; +const tenant = ''; afterEach(() => { fetchMock.restore(); @@ -32,7 +33,7 @@ afterEach(() => { it(`logs out 401 responses`, async () => { const http = setupHttp('/foo'); - const sessionExpired = new SessionExpired(http.basePath); + const sessionExpired = new SessionExpired(http.basePath, tenant); const logoutPromise = new Promise(resolve => { jest.spyOn(sessionExpired, 'logout').mockImplementation(() => resolve()); }); @@ -58,7 +59,7 @@ it(`ignores anonymous paths`, async () => { const http = setupHttp('/foo'); const { anonymousPaths } = http; anonymousPaths.register('/bar'); - const sessionExpired = new SessionExpired(http.basePath); + const sessionExpired = new SessionExpired(http.basePath, tenant); const interceptor = new UnauthorizedResponseHttpInterceptor(sessionExpired, anonymousPaths); http.intercept(interceptor); fetchMock.mock('*', 401); @@ -69,7 +70,7 @@ it(`ignores anonymous paths`, async () => { it(`ignores errors which don't have a response, for example network connectivity issues`, async () => { const http = setupHttp('/foo'); - const sessionExpired = new SessionExpired(http.basePath); + const sessionExpired = new SessionExpired(http.basePath, tenant); const interceptor = new UnauthorizedResponseHttpInterceptor(sessionExpired, http.anonymousPaths); http.intercept(interceptor); fetchMock.mock('*', new Promise((resolve, reject) => reject(new Error('Network is down')))); @@ -80,7 +81,7 @@ it(`ignores errors which don't have a response, for example network connectivity it(`ignores requests which omit credentials`, async () => { const http = setupHttp('/foo'); - const sessionExpired = new SessionExpired(http.basePath); + const sessionExpired = new SessionExpired(http.basePath, tenant); const interceptor = new UnauthorizedResponseHttpInterceptor(sessionExpired, http.anonymousPaths); http.intercept(interceptor); fetchMock.mock('*', 401); diff --git a/x-pack/plugins/security/public/types.ts b/x-pack/plugins/security/public/types.ts new file mode 100644 index 00000000000000..e9c4b6e281cf3e --- /dev/null +++ b/x-pack/plugins/security/public/types.ts @@ -0,0 +1,12 @@ +/* + * 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 SessionInfo { + now: number; + idleTimeoutExpiration: number | null; + lifespanExpiration: number | null; + provider: string; +} diff --git a/x-pack/plugins/security/server/authentication/authenticator.test.ts b/x-pack/plugins/security/server/authentication/authenticator.test.ts index 78c6feac0fa297..12b4620d554a2e 100644 --- a/x-pack/plugins/security/server/authentication/authenticator.test.ts +++ b/x-pack/plugins/security/server/authentication/authenticator.test.ts @@ -28,7 +28,11 @@ function getMockOptions(config: Partial = {}) { basePath: httpServiceMock.createSetupContract().basePath, loggers: loggingServiceMock.create(), isSystemAPIRequest: jest.fn(), - config: { sessionTimeout: null, authc: { providers: [], oidc: {}, saml: {} }, ...config }, + config: { + session: { idleTimeout: null, lifespan: null }, + authc: { providers: [], oidc: {}, saml: {} }, + ...config, + }, sessionStorageFactory: sessionStorageMock.createFactory(), }; } @@ -51,7 +55,9 @@ describe('Authenticator', () => { describe('initialization', () => { it('fails if authentication providers are not configured.', () => { - const mockOptions = getMockOptions({ authc: { providers: [], oidc: {}, saml: {} } }); + const mockOptions = getMockOptions({ + authc: { providers: [], oidc: {}, saml: {} }, + }); expect(() => new Authenticator(mockOptions)).toThrowError( 'No authentication provider is configured. Verify `xpack.security.authc.providers` config value.' ); @@ -73,7 +79,9 @@ describe('Authenticator', () => { let mockOptions: ReturnType; let mockSessionStorage: jest.Mocked>; beforeEach(() => { - mockOptions = getMockOptions({ authc: { providers: ['basic'], oidc: {}, saml: {} } }); + mockOptions = getMockOptions({ + authc: { providers: ['basic'], oidc: {}, saml: {} }, + }); mockSessionStorage = sessionStorageMock.create(); mockOptions.sessionStorageFactory.asScoped.mockReturnValue(mockSessionStorage); @@ -151,7 +159,8 @@ describe('Authenticator', () => { expect(mockSessionStorage.set).toHaveBeenCalledTimes(1); expect(mockSessionStorage.set).toHaveBeenCalledWith({ - expires: null, + idleTimeoutExpiration: null, + lifespanExpiration: null, state: { authorization }, provider: 'basic', }); @@ -173,7 +182,12 @@ describe('Authenticator', () => { const request = httpServerMock.createKibanaRequest(); mockBasicAuthenticationProvider.login.mockResolvedValue(AuthenticationResult.succeeded(user)); - mockSessionStorage.get.mockResolvedValue({ expires: null, state, provider: 'token' }); + mockSessionStorage.get.mockResolvedValue({ + idleTimeoutExpiration: null, + lifespanExpiration: null, + state, + provider: 'token', + }); const authenticationResult = await authenticator.login(request, { provider: 'basic', @@ -286,7 +300,9 @@ describe('Authenticator', () => { let mockOptions: ReturnType; let mockSessionStorage: jest.Mocked>; beforeEach(() => { - mockOptions = getMockOptions({ authc: { providers: ['basic'], oidc: {}, saml: {} } }); + mockOptions = getMockOptions({ + authc: { providers: ['basic'], oidc: {}, saml: {} }, + }); mockSessionStorage = sessionStorageMock.create(); mockOptions.sessionStorageFactory.asScoped.mockReturnValue(mockSessionStorage); @@ -344,7 +360,8 @@ describe('Authenticator', () => { expect(mockSessionStorage.set).toHaveBeenCalledTimes(1); expect(mockSessionStorage.set).toHaveBeenCalledWith({ - expires: null, + idleTimeoutExpiration: null, + lifespanExpiration: null, state: { authorization }, provider: 'basic', }); @@ -366,7 +383,8 @@ describe('Authenticator', () => { expect(mockSessionStorage.set).toHaveBeenCalledTimes(1); expect(mockSessionStorage.set).toHaveBeenCalledWith({ - expires: null, + idleTimeoutExpiration: null, + lifespanExpiration: null, state: { authorization }, provider: 'basic', }); @@ -381,7 +399,12 @@ describe('Authenticator', () => { mockBasicAuthenticationProvider.authenticate.mockResolvedValue( AuthenticationResult.succeeded(user) ); - mockSessionStorage.get.mockResolvedValue({ expires: null, state, provider: 'basic' }); + mockSessionStorage.get.mockResolvedValue({ + idleTimeoutExpiration: null, + lifespanExpiration: null, + state, + provider: 'basic', + }); const authenticationResult = await authenticator.authenticate(request); expect(authenticationResult.succeeded()).toBe(true); @@ -400,7 +423,58 @@ describe('Authenticator', () => { mockBasicAuthenticationProvider.authenticate.mockResolvedValue( AuthenticationResult.succeeded(user) ); - mockSessionStorage.get.mockResolvedValue({ expires: null, state, provider: 'basic' }); + mockSessionStorage.get.mockResolvedValue({ + idleTimeoutExpiration: null, + lifespanExpiration: null, + state, + provider: 'basic', + }); + + const authenticationResult = await authenticator.authenticate(request); + expect(authenticationResult.succeeded()).toBe(true); + expect(authenticationResult.user).toEqual(user); + + expect(mockSessionStorage.set).toHaveBeenCalledTimes(1); + expect(mockSessionStorage.set).toHaveBeenCalledWith({ + idleTimeoutExpiration: null, + lifespanExpiration: null, + state, + provider: 'basic', + }); + expect(mockSessionStorage.clear).not.toHaveBeenCalled(); + }); + + it('properly extends session expiration if it is defined.', async () => { + const user = mockAuthenticatedUser(); + const state = { authorization: 'Basic xxx' }; + const request = httpServerMock.createKibanaRequest(); + const currentDate = new Date(Date.UTC(2019, 10, 10)).valueOf(); + + // Create new authenticator with non-null session `idleTimeout`. + mockOptions = getMockOptions({ + session: { + idleTimeout: 3600 * 24, + lifespan: null, + }, + authc: { providers: ['basic'], oidc: {}, saml: {} }, + }); + + mockSessionStorage = sessionStorageMock.create(); + mockSessionStorage.get.mockResolvedValue({ + idleTimeoutExpiration: null, + lifespanExpiration: null, + state, + provider: 'basic', + }); + mockOptions.sessionStorageFactory.asScoped.mockReturnValue(mockSessionStorage); + + authenticator = new Authenticator(mockOptions); + + mockBasicAuthenticationProvider.authenticate.mockResolvedValue( + AuthenticationResult.succeeded(user) + ); + + jest.spyOn(Date, 'now').mockImplementation(() => currentDate); const authenticationResult = await authenticator.authenticate(request); expect(authenticationResult.succeeded()).toBe(true); @@ -408,27 +482,39 @@ describe('Authenticator', () => { expect(mockSessionStorage.set).toHaveBeenCalledTimes(1); expect(mockSessionStorage.set).toHaveBeenCalledWith({ - expires: null, + idleTimeoutExpiration: currentDate + 3600 * 24, + lifespanExpiration: null, state, provider: 'basic', }); expect(mockSessionStorage.clear).not.toHaveBeenCalled(); }); - it('properly extends session timeout if it is defined.', async () => { + it('does not extend session lifespan expiration.', async () => { const user = mockAuthenticatedUser(); const state = { authorization: 'Basic xxx' }; const request = httpServerMock.createKibanaRequest(); const currentDate = new Date(Date.UTC(2019, 10, 10)).valueOf(); + const hr = 1000 * 60 * 60; - // Create new authenticator with non-null `sessionTimeout`. + // Create new authenticator with non-null session `idleTimeout` and `lifespan`. mockOptions = getMockOptions({ - sessionTimeout: 3600 * 24, + session: { + idleTimeout: hr * 2, + lifespan: hr * 8, + }, authc: { providers: ['basic'], oidc: {}, saml: {} }, }); mockSessionStorage = sessionStorageMock.create(); - mockSessionStorage.get.mockResolvedValue({ expires: null, state, provider: 'basic' }); + mockSessionStorage.get.mockResolvedValue({ + // this session was created 6.5 hrs ago (and has 1.5 hrs left in its lifespan) + // it was last extended 1 hour ago, which means it will expire in 1 hour + idleTimeoutExpiration: currentDate + hr * 1, + lifespanExpiration: currentDate + hr * 1.5, + state, + provider: 'basic', + }); mockOptions.sessionStorageFactory.asScoped.mockReturnValue(mockSessionStorage); authenticator = new Authenticator(mockOptions); @@ -445,13 +531,69 @@ describe('Authenticator', () => { expect(mockSessionStorage.set).toHaveBeenCalledTimes(1); expect(mockSessionStorage.set).toHaveBeenCalledWith({ - expires: currentDate + 3600 * 24, + idleTimeoutExpiration: currentDate + hr * 2, + lifespanExpiration: currentDate + hr * 1.5, state, provider: 'basic', }); expect(mockSessionStorage.clear).not.toHaveBeenCalled(); }); + it('only updates the session lifespan expiration if it does not match the current server config.', async () => { + const user = mockAuthenticatedUser(); + const state = { authorization: 'Basic xxx' }; + const request = httpServerMock.createKibanaRequest(); + const hr = 1000 * 60 * 60; + + async function createAndUpdateSession( + lifespan: number | null, + oldExpiration: number | null, + newExpiration: number | null + ) { + mockOptions = getMockOptions({ + session: { + idleTimeout: null, + lifespan, + }, + authc: { providers: ['basic'], oidc: {}, saml: {} }, + }); + + mockSessionStorage = sessionStorageMock.create(); + mockSessionStorage.get.mockResolvedValue({ + idleTimeoutExpiration: 1, + lifespanExpiration: oldExpiration, + state, + provider: 'basic', + }); + mockOptions.sessionStorageFactory.asScoped.mockReturnValue(mockSessionStorage); + + authenticator = new Authenticator(mockOptions); + + mockBasicAuthenticationProvider.authenticate.mockResolvedValue( + AuthenticationResult.succeeded(user) + ); + + const authenticationResult = await authenticator.authenticate(request); + expect(authenticationResult.succeeded()).toBe(true); + expect(authenticationResult.user).toEqual(user); + + expect(mockSessionStorage.set).toHaveBeenCalledTimes(1); + expect(mockSessionStorage.set).toHaveBeenCalledWith({ + idleTimeoutExpiration: 1, + lifespanExpiration: newExpiration, + state, + provider: 'basic', + }); + expect(mockSessionStorage.clear).not.toHaveBeenCalled(); + } + // do not change max expiration + createAndUpdateSession(hr * 8, 1234, 1234); + createAndUpdateSession(null, null, null); + // change max expiration + createAndUpdateSession(null, 1234, null); + createAndUpdateSession(hr * 8, null, hr * 8); + }); + it('does not touch session for system API calls if authentication fails with non-401 reason.', async () => { const state = { authorization: 'Basic xxx' }; const request = httpServerMock.createKibanaRequest(); @@ -460,7 +602,12 @@ describe('Authenticator', () => { mockBasicAuthenticationProvider.authenticate.mockResolvedValue( AuthenticationResult.failed(new Error('some error')) ); - mockSessionStorage.get.mockResolvedValue({ expires: null, state, provider: 'basic' }); + mockSessionStorage.get.mockResolvedValue({ + idleTimeoutExpiration: null, + lifespanExpiration: null, + state, + provider: 'basic', + }); const authenticationResult = await authenticator.authenticate(request); expect(authenticationResult.failed()).toBe(true); @@ -477,7 +624,12 @@ describe('Authenticator', () => { mockBasicAuthenticationProvider.authenticate.mockResolvedValue( AuthenticationResult.failed(new Error('some error')) ); - mockSessionStorage.get.mockResolvedValue({ expires: null, state, provider: 'basic' }); + mockSessionStorage.get.mockResolvedValue({ + idleTimeoutExpiration: null, + lifespanExpiration: null, + state, + provider: 'basic', + }); const authenticationResult = await authenticator.authenticate(request); expect(authenticationResult.failed()).toBe(true); @@ -497,7 +649,8 @@ describe('Authenticator', () => { AuthenticationResult.succeeded(user, { state: newState }) ); mockSessionStorage.get.mockResolvedValue({ - expires: null, + idleTimeoutExpiration: null, + lifespanExpiration: null, state: existingState, provider: 'basic', }); @@ -508,7 +661,8 @@ describe('Authenticator', () => { expect(mockSessionStorage.set).toHaveBeenCalledTimes(1); expect(mockSessionStorage.set).toHaveBeenCalledWith({ - expires: null, + idleTimeoutExpiration: null, + lifespanExpiration: null, state: newState, provider: 'basic', }); @@ -526,7 +680,8 @@ describe('Authenticator', () => { AuthenticationResult.succeeded(user, { state: newState }) ); mockSessionStorage.get.mockResolvedValue({ - expires: null, + idleTimeoutExpiration: null, + lifespanExpiration: null, state: existingState, provider: 'basic', }); @@ -537,7 +692,8 @@ describe('Authenticator', () => { expect(mockSessionStorage.set).toHaveBeenCalledTimes(1); expect(mockSessionStorage.set).toHaveBeenCalledWith({ - expires: null, + idleTimeoutExpiration: null, + lifespanExpiration: null, state: newState, provider: 'basic', }); @@ -552,7 +708,12 @@ describe('Authenticator', () => { mockBasicAuthenticationProvider.authenticate.mockResolvedValue( AuthenticationResult.failed(Boom.unauthorized()) ); - mockSessionStorage.get.mockResolvedValue({ expires: null, state, provider: 'basic' }); + mockSessionStorage.get.mockResolvedValue({ + idleTimeoutExpiration: null, + lifespanExpiration: null, + state, + provider: 'basic', + }); const authenticationResult = await authenticator.authenticate(request); expect(authenticationResult.failed()).toBe(true); @@ -569,7 +730,12 @@ describe('Authenticator', () => { mockBasicAuthenticationProvider.authenticate.mockResolvedValue( AuthenticationResult.failed(Boom.unauthorized()) ); - mockSessionStorage.get.mockResolvedValue({ expires: null, state, provider: 'basic' }); + mockSessionStorage.get.mockResolvedValue({ + idleTimeoutExpiration: null, + lifespanExpiration: null, + state, + provider: 'basic', + }); const authenticationResult = await authenticator.authenticate(request); expect(authenticationResult.failed()).toBe(true); @@ -585,7 +751,12 @@ describe('Authenticator', () => { mockBasicAuthenticationProvider.authenticate.mockResolvedValue( AuthenticationResult.redirectTo('some-url', { state: null }) ); - mockSessionStorage.get.mockResolvedValue({ expires: null, state, provider: 'basic' }); + mockSessionStorage.get.mockResolvedValue({ + idleTimeoutExpiration: null, + lifespanExpiration: null, + state, + provider: 'basic', + }); const authenticationResult = await authenticator.authenticate(request); expect(authenticationResult.redirected()).toBe(true); @@ -602,7 +773,12 @@ describe('Authenticator', () => { mockBasicAuthenticationProvider.authenticate.mockResolvedValue( AuthenticationResult.notHandled() ); - mockSessionStorage.get.mockResolvedValue({ expires: null, state, provider: 'basic' }); + mockSessionStorage.get.mockResolvedValue({ + idleTimeoutExpiration: null, + lifespanExpiration: null, + state, + provider: 'basic', + }); const authenticationResult = await authenticator.authenticate(request); expect(authenticationResult.notHandled()).toBe(true); @@ -619,7 +795,12 @@ describe('Authenticator', () => { mockBasicAuthenticationProvider.authenticate.mockResolvedValue( AuthenticationResult.notHandled() ); - mockSessionStorage.get.mockResolvedValue({ expires: null, state, provider: 'basic' }); + mockSessionStorage.get.mockResolvedValue({ + idleTimeoutExpiration: null, + lifespanExpiration: null, + state, + provider: 'basic', + }); const authenticationResult = await authenticator.authenticate(request); expect(authenticationResult.notHandled()).toBe(true); @@ -636,7 +817,12 @@ describe('Authenticator', () => { mockBasicAuthenticationProvider.authenticate.mockResolvedValue( AuthenticationResult.notHandled() ); - mockSessionStorage.get.mockResolvedValue({ expires: null, state, provider: 'token' }); + mockSessionStorage.get.mockResolvedValue({ + idleTimeoutExpiration: null, + lifespanExpiration: null, + state, + provider: 'token', + }); const authenticationResult = await authenticator.authenticate(request); expect(authenticationResult.notHandled()).toBe(true); @@ -653,7 +839,12 @@ describe('Authenticator', () => { mockBasicAuthenticationProvider.authenticate.mockResolvedValue( AuthenticationResult.notHandled() ); - mockSessionStorage.get.mockResolvedValue({ expires: null, state, provider: 'token' }); + mockSessionStorage.get.mockResolvedValue({ + idleTimeoutExpiration: null, + lifespanExpiration: null, + state, + provider: 'token', + }); const authenticationResult = await authenticator.authenticate(request); expect(authenticationResult.notHandled()).toBe(true); @@ -668,7 +859,9 @@ describe('Authenticator', () => { let mockOptions: ReturnType; let mockSessionStorage: jest.Mocked>; beforeEach(() => { - mockOptions = getMockOptions({ authc: { providers: ['basic'], oidc: {}, saml: {} } }); + mockOptions = getMockOptions({ + authc: { providers: ['basic'], oidc: {}, saml: {} }, + }); mockSessionStorage = sessionStorageMock.create(); mockOptions.sessionStorageFactory.asScoped.mockReturnValue(mockSessionStorage); @@ -697,7 +890,12 @@ describe('Authenticator', () => { mockBasicAuthenticationProvider.logout.mockResolvedValue( DeauthenticationResult.redirectTo('some-url') ); - mockSessionStorage.get.mockResolvedValue({ expires: null, state, provider: 'basic' }); + mockSessionStorage.get.mockResolvedValue({ + idleTimeoutExpiration: null, + lifespanExpiration: null, + state, + provider: 'basic', + }); const deauthenticationResult = await authenticator.logout(request); @@ -707,10 +905,41 @@ describe('Authenticator', () => { expect(deauthenticationResult.redirectURL).toBe('some-url'); }); + it('if session does not exist but provider name is valid, returns whatever authentication provider returns.', async () => { + const request = httpServerMock.createKibanaRequest({ query: { provider: 'basic' } }); + mockSessionStorage.get.mockResolvedValue(null); + + mockBasicAuthenticationProvider.logout.mockResolvedValue( + DeauthenticationResult.redirectTo('some-url') + ); + + const deauthenticationResult = await authenticator.logout(request); + + expect(mockBasicAuthenticationProvider.logout).toHaveBeenCalledTimes(1); + expect(mockSessionStorage.clear).not.toHaveBeenCalled(); + expect(deauthenticationResult.redirected()).toBe(true); + expect(deauthenticationResult.redirectURL).toBe('some-url'); + }); + + it('returns `notHandled` if session does not exist and provider name is invalid', async () => { + const request = httpServerMock.createKibanaRequest({ query: { provider: 'foo' } }); + mockSessionStorage.get.mockResolvedValue(null); + + const deauthenticationResult = await authenticator.logout(request); + + expect(deauthenticationResult.notHandled()).toBe(true); + expect(mockSessionStorage.clear).not.toHaveBeenCalled(); + }); + it('only clears session if it belongs to not configured provider.', async () => { const request = httpServerMock.createKibanaRequest(); const state = { authorization: 'Bearer xxx' }; - mockSessionStorage.get.mockResolvedValue({ expires: null, state, provider: 'token' }); + mockSessionStorage.get.mockResolvedValue({ + idleTimeoutExpiration: null, + lifespanExpiration: null, + state, + provider: 'token', + }); const deauthenticationResult = await authenticator.logout(request); @@ -719,4 +948,51 @@ describe('Authenticator', () => { expect(deauthenticationResult.notHandled()).toBe(true); }); }); + + describe('`getSessionInfo` method', () => { + let authenticator: Authenticator; + let mockOptions: ReturnType; + let mockSessionStorage: jest.Mocked>; + beforeEach(() => { + mockOptions = getMockOptions({ + authc: { providers: ['basic'], oidc: {}, saml: {} }, + }); + mockSessionStorage = sessionStorageMock.create(); + mockOptions.sessionStorageFactory.asScoped.mockReturnValue(mockSessionStorage); + + authenticator = new Authenticator(mockOptions); + }); + + it('returns current session info if session exists.', async () => { + const request = httpServerMock.createKibanaRequest(); + const state = { authorization: 'Basic xxx' }; + const currentDate = new Date(Date.UTC(2019, 10, 10)).valueOf(); + const mockInfo = { + now: currentDate, + idleTimeoutExpiration: currentDate + 60000, + lifespanExpiration: currentDate + 120000, + provider: 'basic', + }; + mockSessionStorage.get.mockResolvedValue({ + idleTimeoutExpiration: mockInfo.idleTimeoutExpiration, + lifespanExpiration: mockInfo.lifespanExpiration, + state, + provider: mockInfo.provider, + }); + jest.spyOn(Date, 'now').mockImplementation(() => currentDate); + + const sessionInfo = await authenticator.getSessionInfo(request); + + expect(sessionInfo).toEqual(mockInfo); + }); + + it('returns `null` if session does not exist.', async () => { + const request = httpServerMock.createKibanaRequest(); + mockSessionStorage.get.mockResolvedValue(null); + + const sessionInfo = await authenticator.getSessionInfo(request); + + expect(sessionInfo).toBe(null); + }); + }); }); diff --git a/x-pack/plugins/security/server/authentication/authenticator.ts b/x-pack/plugins/security/server/authentication/authenticator.ts index 18bdc9624b12b1..17a773c6b6e8ce 100644 --- a/x-pack/plugins/security/server/authentication/authenticator.ts +++ b/x-pack/plugins/security/server/authentication/authenticator.ts @@ -31,6 +31,7 @@ import { import { AuthenticationResult } from './authentication_result'; import { DeauthenticationResult } from './deauthentication_result'; import { Tokens } from './tokens'; +import { SessionInfo } from '../../public/types'; /** * The shape of the session that is actually stored in the cookie. @@ -45,7 +46,13 @@ export interface ProviderSession { * The Unix time in ms when the session should be considered expired. If `null`, session will stay * active until the browser is closed. */ - expires: number | null; + idleTimeoutExpiration: number | null; + + /** + * The Unix time in ms which is the max total lifespan of the session. If `null`, session expire + * time can be extended indefinitely. + */ + lifespanExpiration: number | null; /** * Session value that is fed to the authentication provider. The shape is unknown upfront and @@ -77,7 +84,7 @@ export interface ProviderLoginAttempt { } export interface AuthenticatorOptions { - config: Pick; + config: Pick; basePath: HttpServiceSetup['basePath']; loggers: LoggerFactory; clusterClient: IClusterClient; @@ -153,9 +160,14 @@ export class Authenticator { private readonly providers: Map; /** - * Session duration in ms. If `null` session will stay active until the browser is closed. + * Session timeout in ms. If `null` session will stay active until the browser is closed. */ - private readonly ttl: number | null = null; + private readonly idleTimeout: number | null = null; + + /** + * Session max lifespan in ms. If `null` session may live indefinitely. + */ + private readonly lifespan: number | null = null; /** * Internal authenticator logger. @@ -202,7 +214,9 @@ export class Authenticator { }) ); - this.ttl = this.options.config.sessionTimeout; + // only set these vars if they are defined in options (otherwise coalesce to existing/default) + this.idleTimeout = this.options.config.session.idleTimeout; + this.lifespan = this.options.config.session.lifespan; } /** @@ -257,10 +271,12 @@ export class Authenticator { if (existingSession && shouldClearSession) { sessionStorage.clear(); } else if (!attempt.stateless && authenticationResult.shouldUpdateState()) { + const { idleTimeoutExpiration, lifespanExpiration } = this.calculateExpiry(existingSession); sessionStorage.set({ state: authenticationResult.state, provider: attempt.provider, - expires: this.ttl && Date.now() + this.ttl, + idleTimeoutExpiration, + lifespanExpiration, }); } @@ -315,10 +331,18 @@ export class Authenticator { const sessionStorage = this.options.sessionStorageFactory.asScoped(request); const sessionValue = await this.getSessionValue(sessionStorage); + const providerName = this.getProviderName(request.query); if (sessionValue) { sessionStorage.clear(); return this.providers.get(sessionValue.provider)!.logout(request, sessionValue.state); + } else if (providerName) { + // provider name is passed in a query param and sourced from the browser's local storage; + // hence, we can't assume that this provider exists, so we have to check it + const provider = this.providers.get(providerName); + if (provider) { + return provider.logout(request, null); + } } // Normally when there is no active session in Kibana, `logout` method shouldn't do anything @@ -334,6 +358,29 @@ export class Authenticator { return DeauthenticationResult.notHandled(); } + /** + * Returns session information for the current request. + * @param request Request instance. + */ + async getSessionInfo(request: KibanaRequest): Promise { + assertRequest(request); + + const sessionStorage = this.options.sessionStorageFactory.asScoped(request); + const sessionValue = await this.getSessionValue(sessionStorage); + + if (sessionValue) { + // We can't rely on the client's system clock, so in addition to returning expiration timestamps, we also return + // the current server time -- that way the client can calculate the relative time to expiration. + return { + now: Date.now(), + idleTimeoutExpiration: sessionValue.idleTimeoutExpiration, + lifespanExpiration: sessionValue.lifespanExpiration, + provider: sessionValue.provider, + }; + } + return null; + } + /** * Returns provider iterator where providers are sorted in the order of priority (based on the session ownership). * @param sessionValue Current session value. @@ -410,13 +457,34 @@ export class Authenticator { ) { sessionStorage.clear(); } else if (sessionCanBeUpdated) { + const { idleTimeoutExpiration, lifespanExpiration } = this.calculateExpiry(existingSession); sessionStorage.set({ state: authenticationResult.shouldUpdateState() ? authenticationResult.state : existingSession!.state, provider: providerType, - expires: this.ttl && Date.now() + this.ttl, + idleTimeoutExpiration, + lifespanExpiration, }); } } + + private getProviderName(query: any): string | null { + if (query && query.provider && typeof query.provider === 'string') { + return query.provider; + } + return null; + } + + private calculateExpiry( + existingSession: ProviderSession | null + ): { idleTimeoutExpiration: number | null; lifespanExpiration: number | null } { + let lifespanExpiration = this.lifespan && Date.now() + this.lifespan; + if (existingSession && existingSession.lifespanExpiration && this.lifespan) { + lifespanExpiration = existingSession.lifespanExpiration; + } + const idleTimeoutExpiration = this.idleTimeout && Date.now() + this.idleTimeout; + + return { idleTimeoutExpiration, lifespanExpiration }; + } } diff --git a/x-pack/plugins/security/server/authentication/index.mock.ts b/x-pack/plugins/security/server/authentication/index.mock.ts index dcaf26f53fe010..77f1f9e45aea72 100644 --- a/x-pack/plugins/security/server/authentication/index.mock.ts +++ b/x-pack/plugins/security/server/authentication/index.mock.ts @@ -14,5 +14,6 @@ export const authenticationMock = { invalidateAPIKey: jest.fn(), isAuthenticated: jest.fn(), logout: jest.fn(), + getSessionInfo: jest.fn(), }), }; diff --git a/x-pack/plugins/security/server/authentication/index.ts b/x-pack/plugins/security/server/authentication/index.ts index df16dd375e858a..2e67a0eaaa6d51 100644 --- a/x-pack/plugins/security/server/authentication/index.ts +++ b/x-pack/plugins/security/server/authentication/index.ts @@ -68,15 +68,22 @@ export async function setupAuthentication({ const authenticator = new Authenticator({ clusterClient, basePath: http.basePath, - config: { sessionTimeout: config.sessionTimeout, authc: config.authc }, + config: { session: config.session, authc: config.authc }, isSystemAPIRequest: (request: KibanaRequest) => getLegacyAPI().isSystemAPIRequest(request), loggers, sessionStorageFactory: await http.createCookieSessionStorageFactory({ encryptionKey: config.encryptionKey, isSecure: config.secureCookies, name: config.cookieName, - validate: (sessionValue: ProviderSession) => - !(sessionValue.expires && sessionValue.expires < Date.now()), + validate: (sessionValue: ProviderSession) => { + const { idleTimeoutExpiration, lifespanExpiration } = sessionValue; + if (idleTimeoutExpiration && idleTimeoutExpiration < Date.now()) { + return false; + } else if (lifespanExpiration && lifespanExpiration < Date.now()) { + return false; + } + return true; + }, }), }); @@ -151,6 +158,7 @@ export async function setupAuthentication({ return { login: authenticator.login.bind(authenticator), logout: authenticator.logout.bind(authenticator), + getSessionInfo: authenticator.getSessionInfo.bind(authenticator), getCurrentUser, createAPIKey: (request: KibanaRequest, params: CreateAPIKeyParams) => apiKeys.create(request, params), diff --git a/x-pack/plugins/security/server/authentication/providers/token.test.ts b/x-pack/plugins/security/server/authentication/providers/token.test.ts index 8eb20447c7e2cd..a6850dcdf8321d 100644 --- a/x-pack/plugins/security/server/authentication/providers/token.test.ts +++ b/x-pack/plugins/security/server/authentication/providers/token.test.ts @@ -422,20 +422,16 @@ describe('TokenAuthenticationProvider', () => { }); describe('`logout` method', () => { - it('returns `notHandled` if state is not presented.', async () => { + it('returns `redirected` if state is not presented.', async () => { const request = httpServerMock.createKibanaRequest(); - const tokenPair = { accessToken: 'foo', refreshToken: 'bar' }; let deauthenticateResult = await provider.logout(request); - expect(deauthenticateResult.notHandled()).toBe(true); + expect(deauthenticateResult.redirected()).toBe(true); deauthenticateResult = await provider.logout(request, null); - expect(deauthenticateResult.notHandled()).toBe(true); + expect(deauthenticateResult.redirected()).toBe(true); sinon.assert.notCalled(mockOptions.tokens.invalidate); - - deauthenticateResult = await provider.logout(request, tokenPair); - expect(deauthenticateResult.notHandled()).toBe(false); }); it('fails if `tokens.invalidate` fails', async () => { diff --git a/x-pack/plugins/security/server/authentication/providers/token.ts b/x-pack/plugins/security/server/authentication/providers/token.ts index d1881ad4b5498b..c5f8f07e50b11c 100644 --- a/x-pack/plugins/security/server/authentication/providers/token.ts +++ b/x-pack/plugins/security/server/authentication/providers/token.ts @@ -120,18 +120,16 @@ export class TokenAuthenticationProvider extends BaseAuthenticationProvider { public async logout(request: KibanaRequest, state?: ProviderState | null) { this.logger.debug(`Trying to log user out via ${request.url.path}.`); - if (!state) { + if (state) { + this.logger.debug('Token-based logout has been initiated by the user.'); + try { + await this.options.tokens.invalidate(state); + } catch (err) { + this.logger.debug(`Failed invalidating user's access token: ${err.message}`); + return DeauthenticationResult.failed(err); + } + } else { this.logger.debug('There are no access and refresh tokens to invalidate.'); - return DeauthenticationResult.notHandled(); - } - - this.logger.debug('Token-based logout has been initiated by the user.'); - - try { - await this.options.tokens.invalidate(state); - } catch (err) { - this.logger.debug(`Failed invalidating user's access token: ${err.message}`); - return DeauthenticationResult.failed(err); } const queryString = request.url.search || `?msg=LOGGED_OUT`; diff --git a/x-pack/plugins/security/server/config.test.ts b/x-pack/plugins/security/server/config.test.ts index 569611516c880c..9ddb3e6e96b90b 100644 --- a/x-pack/plugins/security/server/config.test.ts +++ b/x-pack/plugins/security/server/config.test.ts @@ -13,48 +13,57 @@ import { createConfig$, ConfigSchema } from './config'; describe('config schema', () => { it('generates proper defaults', () => { expect(ConfigSchema.validate({})).toMatchInlineSnapshot(` - Object { - "authc": Object { - "providers": Array [ - "basic", - ], - }, - "cookieName": "sid", - "encryptionKey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "loginAssistanceMessage": "", - "secureCookies": false, - "sessionTimeout": null, - } - `); + Object { + "authc": Object { + "providers": Array [ + "basic", + ], + }, + "cookieName": "sid", + "encryptionKey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "loginAssistanceMessage": "", + "secureCookies": false, + "session": Object { + "idleTimeout": null, + "lifespan": null, + }, + } + `); expect(ConfigSchema.validate({}, { dist: false })).toMatchInlineSnapshot(` - Object { - "authc": Object { - "providers": Array [ - "basic", - ], - }, - "cookieName": "sid", - "encryptionKey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "loginAssistanceMessage": "", - "secureCookies": false, - "sessionTimeout": null, - } - `); + Object { + "authc": Object { + "providers": Array [ + "basic", + ], + }, + "cookieName": "sid", + "encryptionKey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "loginAssistanceMessage": "", + "secureCookies": false, + "session": Object { + "idleTimeout": null, + "lifespan": null, + }, + } + `); expect(ConfigSchema.validate({}, { dist: true })).toMatchInlineSnapshot(` - Object { - "authc": Object { - "providers": Array [ - "basic", - ], - }, - "cookieName": "sid", - "loginAssistanceMessage": "", - "secureCookies": false, - "sessionTimeout": null, - } - `); + Object { + "authc": Object { + "providers": Array [ + "basic", + ], + }, + "cookieName": "sid", + "loginAssistanceMessage": "", + "secureCookies": false, + "session": Object { + "idleTimeout": null, + "lifespan": null, + }, + } + `); }); it('should throw error if xpack.security.encryptionKey is less than 32 characters', () => { @@ -253,7 +262,11 @@ describe('createConfig$()', () => { const config = await createConfig$(contextMock, true) .pipe(first()) .toPromise(); - expect(config).toEqual({ encryptionKey: 'ab'.repeat(16), secureCookies: true }); + expect(config).toEqual({ + encryptionKey: 'ab'.repeat(16), + secureCookies: true, + session: { idleTimeout: null, lifespan: null }, + }); expect(loggingServiceMock.collect(contextMock.logger).warn).toMatchInlineSnapshot(` Array [ @@ -273,7 +286,7 @@ describe('createConfig$()', () => { const config = await createConfig$(contextMock, false) .pipe(first()) .toPromise(); - expect(config).toEqual({ encryptionKey: 'a'.repeat(32), secureCookies: false }); + expect(config.secureCookies).toEqual(false); expect(loggingServiceMock.collect(contextMock.logger).warn).toMatchInlineSnapshot(` Array [ @@ -293,7 +306,7 @@ describe('createConfig$()', () => { const config = await createConfig$(contextMock, false) .pipe(first()) .toPromise(); - expect(config).toEqual({ encryptionKey: 'a'.repeat(32), secureCookies: true }); + expect(config.secureCookies).toEqual(true); expect(loggingServiceMock.collect(contextMock.logger).warn).toMatchInlineSnapshot(` Array [ @@ -313,7 +326,7 @@ describe('createConfig$()', () => { const config = await createConfig$(contextMock, true) .pipe(first()) .toPromise(); - expect(config).toEqual({ encryptionKey: 'a'.repeat(32), secureCookies: true }); + expect(config.secureCookies).toEqual(true); expect(loggingServiceMock.collect(contextMock.logger).warn).toEqual([]); }); diff --git a/x-pack/plugins/security/server/config.ts b/x-pack/plugins/security/server/config.ts index a257a253443935..c7d990f81369ec 100644 --- a/x-pack/plugins/security/server/config.ts +++ b/x-pack/plugins/security/server/config.ts @@ -34,7 +34,11 @@ export const ConfigSchema = schema.object( schema.maybe(schema.string({ minLength: 32 })), schema.string({ minLength: 32, defaultValue: 'a'.repeat(32) }) ), - sessionTimeout: schema.oneOf([schema.number(), schema.literal(null)], { defaultValue: null }), + sessionTimeout: schema.maybe(schema.oneOf([schema.number(), schema.literal(null)])), // DEPRECATED + session: schema.object({ + idleTimeout: schema.oneOf([schema.number(), schema.literal(null)], { defaultValue: null }), + lifespan: schema.oneOf([schema.number(), schema.literal(null)], { defaultValue: null }), + }), secureCookies: schema.boolean({ defaultValue: false }), authc: schema.object({ providers: schema.arrayOf(schema.string(), { defaultValue: ['basic'], minSize: 1 }), @@ -83,11 +87,23 @@ export function createConfig$(context: PluginInitializerContext, isTLSEnabled: b secureCookies = true; } - return { + // "sessionTimeout" is deprecated and replaced with "session.idleTimeout" + // however, NP does not yet have a mechanism to automatically rename deprecated keys + // for the time being, we'll do it manually: + const sess = config.session; + const session = { + idleTimeout: (sess && sess.idleTimeout) || config.sessionTimeout || null, + lifespan: (sess && sess.lifespan) || null, + }; + + const val = { ...config, encryptionKey, secureCookies, + session, }; + delete val.sessionTimeout; // DEPRECATED + return val; }) ); } diff --git a/x-pack/plugins/security/server/plugin.test.ts b/x-pack/plugins/security/server/plugin.test.ts index 2ff0e915fc1b02..26788c3ef9230a 100644 --- a/x-pack/plugins/security/server/plugin.test.ts +++ b/x-pack/plugins/security/server/plugin.test.ts @@ -20,7 +20,10 @@ describe('Security Plugin', () => { plugin = new Plugin( coreMock.createPluginInitializerContext({ cookieName: 'sid', - sessionTimeout: 1500, + session: { + idleTimeout: 1500, + lifespan: null, + }, authc: { providers: ['saml', 'token'], saml: { realm: 'saml1', maxRedirectURLSize: new ByteSizeValue(2048) }, @@ -54,7 +57,10 @@ describe('Security Plugin', () => { "cookieName": "sid", "loginAssistanceMessage": undefined, "secureCookies": true, - "sessionTimeout": 1500, + "session": Object { + "idleTimeout": 1500, + "lifespan": null, + }, }, "license": Object { "getFeatures": [Function], @@ -66,6 +72,7 @@ describe('Security Plugin', () => { "authc": Object { "createAPIKey": [Function], "getCurrentUser": [Function], + "getSessionInfo": [Function], "invalidateAPIKey": [Function], "isAuthenticated": [Function], "login": [Function], diff --git a/x-pack/plugins/security/server/plugin.ts b/x-pack/plugins/security/server/plugin.ts index c8761050524a59..e9566035173497 100644 --- a/x-pack/plugins/security/server/plugin.ts +++ b/x-pack/plugins/security/server/plugin.ts @@ -74,7 +74,10 @@ export interface PluginSetupContract { registerPrivilegesWithCluster: () => void; license: SecurityLicense; config: RecursiveReadonly<{ - sessionTimeout: number | null; + session: { + idleTimeout: number | null; + lifespan: number | null; + }; secureCookies: boolean; authc: { providers: string[] }; }>; @@ -206,7 +209,10 @@ export class Plugin { // exception may be `sessionTimeout` as other parts of the app may want to know it. config: { loginAssistanceMessage: config.loginAssistanceMessage, - sessionTimeout: config.sessionTimeout, + session: { + idleTimeout: config.session.idleTimeout, + lifespan: config.session.lifespan, + }, secureCookies: config.secureCookies, cookieName: config.cookieName, authc: { providers: config.authc.providers }, diff --git a/x-pack/plugins/security/server/routes/authentication/index.ts b/x-pack/plugins/security/server/routes/authentication/index.ts index 0e3f03255dcb90..086647dcb34597 100644 --- a/x-pack/plugins/security/server/routes/authentication/index.ts +++ b/x-pack/plugins/security/server/routes/authentication/index.ts @@ -4,10 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ +import { defineSessionRoutes } from './session'; import { defineSAMLRoutes } from './saml'; import { RouteDefinitionParams } from '..'; export function defineAuthenticationRoutes(params: RouteDefinitionParams) { + defineSessionRoutes(params); if (params.config.authc.providers.includes('saml')) { defineSAMLRoutes(params); } diff --git a/x-pack/plugins/security/server/routes/authentication/session.ts b/x-pack/plugins/security/server/routes/authentication/session.ts new file mode 100644 index 00000000000000..cdebc19d7cf8db --- /dev/null +++ b/x-pack/plugins/security/server/routes/authentication/session.ts @@ -0,0 +1,46 @@ +/* + * 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 { RouteDefinitionParams } from '..'; + +/** + * Defines routes required for all authentication realms. + */ +export function defineSessionRoutes({ router, logger, authc, basePath }: RouteDefinitionParams) { + router.get( + { + path: '/internal/security/session', + validate: false, + }, + async (_context, request, response) => { + try { + const sessionInfo = await authc.getSessionInfo(request); + // This is an authenticated request, so sessionInfo will always be non-null. + return response.ok({ body: sessionInfo! }); + } catch (err) { + logger.error(`Error retrieving user session: ${err.message}`); + return response.internalError(); + } + } + ); + + router.post( + { + path: '/internal/security/session', + validate: false, + }, + async (_context, _request, response) => { + // We can't easily return updated session info in a single HTTP call, because session data is obtained from + // the HTTP request, not the response. So the easiest way to facilitate this is to redirect the client to GET + // the session endpoint after the client's session has been extended. + return response.redirected({ + headers: { + location: `${basePath.serverBasePath}/internal/security/session`, + }, + }); + } + ); +} diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index f83d0c9ea3c9a3..f5fc453557122e 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -9929,9 +9929,8 @@ "xpack.security.account.passwordsDoNotMatch": "パスワードが一致していません。", "xpack.security.account.usernameGroupDescription": "この情報は変更できません。", "xpack.security.account.usernameGroupTitle": "ユーザー名とメールアドレス", - "xpack.security.components.sessionTimeoutWarning.message": "操作がないため間もなくログアウトします。再開するには [OK] をクリックしてくださ。", - "xpack.security.components.sessionTimeoutWarning.okButtonText": "OK", - "xpack.security.components.sessionTimeoutWarning.title": "警告", + "xpack.security.components.sessionIdleTimeoutWarning.okButtonText": "OK", + "xpack.security.components.sessionIdleTimeoutWarning.title": "警告", "xpack.security.loggedOut.login": "ログイン", "xpack.security.loggedOut.title": "ログアウト完了", "xpack.security.login.basicLoginForm.invalidUsernameOrPasswordErrorMessage": "無効なユーザー名またはパスワード再試行してください。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index a830eaacd29e35..288fc92be3cbd8 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -10018,9 +10018,8 @@ "xpack.security.account.passwordsDoNotMatch": "密码不匹配。", "xpack.security.account.usernameGroupDescription": "不能更改此信息。", "xpack.security.account.usernameGroupTitle": "用户名和电子邮件", - "xpack.security.components.sessionTimeoutWarning.message": "由于处于不活动状态,您即将退出。单击“确定”可以恢复。", - "xpack.security.components.sessionTimeoutWarning.okButtonText": "确定", - "xpack.security.components.sessionTimeoutWarning.title": "警告", + "xpack.security.components.sessionIdleTimeoutWarning.okButtonText": "确定", + "xpack.security.components.sessionIdleTimeoutWarning.title": "警告", "xpack.security.loggedOut.login": "登录", "xpack.security.loggedOut.title": "已成功退出", "xpack.security.login.basicLoginForm.invalidUsernameOrPasswordErrorMessage": "用户名或密码无效。请重试。", diff --git a/x-pack/test/api_integration/apis/security/index.js b/x-pack/test/api_integration/apis/security/index.js index 4d034622427fce..052d984774e691 100644 --- a/x-pack/test/api_integration/apis/security/index.js +++ b/x-pack/test/api_integration/apis/security/index.js @@ -14,5 +14,6 @@ export default function ({ loadTestFile }) { loadTestFile(require.resolve('./index_fields')); loadTestFile(require.resolve('./roles')); loadTestFile(require.resolve('./privileges')); + loadTestFile(require.resolve('./session')); }); } diff --git a/x-pack/test/api_integration/apis/security/session.ts b/x-pack/test/api_integration/apis/security/session.ts new file mode 100644 index 00000000000000..7c7883f58cb30e --- /dev/null +++ b/x-pack/test/api_integration/apis/security/session.ts @@ -0,0 +1,87 @@ +/* + * 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 { Cookie, cookie } from 'request'; +import expect from '@kbn/expect/expect.js'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const config = getService('config'); + + const kibanaServerConfig = config.get('servers.kibana'); + const validUsername = kibanaServerConfig.username; + const validPassword = kibanaServerConfig.password; + + describe('Session', () => { + let sessionCookie: Cookie; + + const saveCookie = async (response: any) => { + // save the response cookie, and pass back the result + sessionCookie = cookie(response.headers['set-cookie'][0])!; + return response; + }; + const getSessionInfo = async () => + supertest + .get('/internal/security/session') + .set('kbn-xsrf', 'xxx') + .set('kbn-system-api', 'true') + .set('Cookie', sessionCookie.cookieString()) + .send() + .expect(200); + const extendSession = async () => + supertest + .post('/internal/security/session') + .set('kbn-xsrf', 'xxx') + .set('Cookie', sessionCookie.cookieString()) + .send() + .expect(302) + .then(saveCookie); + + beforeEach(async () => { + await supertest + .post('/api/security/v1/login') + .set('kbn-xsrf', 'xxx') + .send({ username: validUsername, password: validPassword }) + .expect(204) + .then(saveCookie); + }); + + describe('GET /internal/security/session', () => { + it('should return current session information', async () => { + const { body } = await getSessionInfo(); + expect(body.now).to.be.a('number'); + expect(body.idleTimeoutExpiration).to.be.a('number'); + expect(body.lifespanExpiration).to.be(null); + expect(body.provider).to.be('basic'); + }); + + it('should not extend the session', async () => { + const { body } = await getSessionInfo(); + const { body: body2 } = await getSessionInfo(); + expect(body2.now).to.be.greaterThan(body.now); + expect(body2.idleTimeoutExpiration).to.equal(body.idleTimeoutExpiration); + }); + }); + + describe('POST /internal/security/session', () => { + it('should redirect to GET', async () => { + const response = await extendSession(); + expect(response.headers.location).to.be('/internal/security/session'); + }); + + it('should extend the session', async () => { + // browsers will follow the redirect and return the new session info, but this testing framework does not + // we simulate that behavior in this test by sending another GET request + const { body } = await getSessionInfo(); + await extendSession(); + const { body: body2 } = await getSessionInfo(); + expect(body2.now).to.be.greaterThan(body.now); + expect(body2.idleTimeoutExpiration).to.be.greaterThan(body.idleTimeoutExpiration); + }); + }); + }); +} diff --git a/x-pack/test/api_integration/config.js b/x-pack/test/api_integration/config.js index 64a9cafca406a6..9c67dfe61b957a 100644 --- a/x-pack/test/api_integration/config.js +++ b/x-pack/test/api_integration/config.js @@ -21,6 +21,7 @@ export async function getApiIntegrationConfig({ readConfigFile }) { ...xPackFunctionalTestsConfig.get('kbnTestServer'), serverArgs: [ ...xPackFunctionalTestsConfig.get('kbnTestServer.serverArgs'), + '--xpack.security.session.idleTimeout=3600000', // 1 hour '--optimize.enabled=false', ], }, diff --git a/yarn.lock b/yarn.lock index e30abf76145a37..7e965979fd46ff 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1004,7 +1004,7 @@ dependencies: regenerator-runtime "^0.13.2" -"@babel/runtime@^7.4.4", "@babel/runtime@^7.6.3": +"@babel/runtime@^7.4.4", "@babel/runtime@^7.6.2", "@babel/runtime@^7.6.3", "@babel/runtime@^7.7.2": version "7.7.2" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.7.2.tgz#111a78002a5c25fc8e3361bedc9529c696b85a6a" integrity sha512-JONRbXbTXc9WQE2mAZd1p0Z3DZ/6vaQIkgYMSTP3KjRCyd7rCZCcfhCyX+YjwcKxcZ82UrxbRD358bpExNgrjw== @@ -6448,6 +6448,11 @@ better-assert@~1.0.0: dependencies: callsite "1.0.0" +big-integer@^1.6.16: + version "1.6.48" + resolved "https://registry.yarnpkg.com/big-integer/-/big-integer-1.6.48.tgz#8fd88bd1632cba4a1c8c3e3d7159f08bb95b4b9e" + integrity sha512-j51egjPa7/i+RdiRuJbPdJ2FIUYYPhvYLjzoYbcMMm62ooO6F94fETG4MTs46zPAF9Brs04OajboA/qTGuz78w== + big-time@2.x.x: version "2.0.1" resolved "https://registry.yarnpkg.com/big-time/-/big-time-2.0.1.tgz#68c7df8dc30f97e953f25a67a76ac9713c16c9de" @@ -6779,6 +6784,19 @@ brfs@^2.0.2: static-module "^3.0.2" through2 "^2.0.0" +broadcast-channel@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/broadcast-channel/-/broadcast-channel-3.0.3.tgz#e6668693af410f7dda007fd6f80e21992d51f3cc" + integrity sha512-ogRIiGDL0bdeOzPO13YQKX12IvRBDOxej2CJaEwuEOF011C9JBABz+8MJ/WZ34eGbXGrfVBeeeaMTWjBzxVKkw== + dependencies: + "@babel/runtime" "^7.7.2" + detect-node "^2.0.4" + js-sha3 "0.8.0" + microseconds "0.1.0" + nano-time "1.0.0" + rimraf "3.0.0" + unload "2.2.0" + brorand@^1.0.1: version "1.1.0" resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" @@ -16938,6 +16956,11 @@ js-levenshtein@^1.1.3: resolved "https://registry.yarnpkg.com/js-levenshtein/-/js-levenshtein-1.1.3.tgz#3ef627df48ec8cf24bacf05c0f184ff30ef413c5" integrity sha512-/812MXr9RBtMObviZ8gQBhHO8MOrGj8HlEE+4ccMTElNA/6I3u39u+bhny55Lk921yn44nSZFy9naNLElL5wgQ== +js-sha3@0.8.0: + version "0.8.0" + resolved "https://registry.yarnpkg.com/js-sha3/-/js-sha3-0.8.0.tgz#b9b7a5da73afad7dedd0f8c463954cbde6818840" + integrity sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q== + js-stringify@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/js-stringify/-/js-stringify-1.0.2.tgz#1736fddfd9724f28a3682adc6230ae7e4e9679db" @@ -18997,6 +19020,11 @@ micromatch@^4.0.0, micromatch@^4.0.2: braces "^3.0.1" picomatch "^2.0.5" +microseconds@0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/microseconds/-/microseconds-0.1.0.tgz#47dc7bcf62171b8030e2152fd82f12a6894a7119" + integrity sha1-R9x7z2IXG4Aw4hUv2C8SpolKcRk= + miller-rabin@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/miller-rabin/-/miller-rabin-4.0.1.tgz#f080351c865b0dc562a8462966daa53543c78a4d" @@ -19577,6 +19605,13 @@ nan@^2.10.0, nan@^2.9.2: resolved "https://registry.yarnpkg.com/nan/-/nan-2.10.0.tgz#96d0cd610ebd58d4b4de9cc0c6828cda99c7548f" integrity sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA== +nano-time@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/nano-time/-/nano-time-1.0.0.tgz#b0554f69ad89e22d0907f7a12b0993a5d96137ef" + integrity sha1-sFVPaa2J4i0JB/ehKwmTpdlhN+8= + dependencies: + big-integer "^1.6.16" + nanomatch@^1.2.5: version "1.2.7" resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.7.tgz#53cd4aa109ff68b7f869591fdc9d10daeeea3e79" @@ -24453,6 +24488,13 @@ rimraf@2.6.3, rimraf@^2.6.3, rimraf@~2.6.2: dependencies: glob "^7.1.3" +rimraf@3.0.0, rimraf@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.0.tgz#614176d4b3010b75e5c390eb0ee96f6dc0cebb9b" + integrity sha512-NDGVxTsjqfunkds7CqsOiEnxln4Bo7Nddl3XhS4pXg5OzwkLqJ971ZVAAnB+DDLnF76N+VnDEiBHaVV8I06SUg== + dependencies: + glob "^7.1.3" + rimraf@^2.7.1: version "2.7.1" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" @@ -24460,13 +24502,6 @@ rimraf@^2.7.1: dependencies: glob "^7.1.3" -rimraf@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.0.tgz#614176d4b3010b75e5c390eb0ee96f6dc0cebb9b" - integrity sha512-NDGVxTsjqfunkds7CqsOiEnxln4Bo7Nddl3XhS4pXg5OzwkLqJ971ZVAAnB+DDLnF76N+VnDEiBHaVV8I06SUg== - dependencies: - glob "^7.1.3" - rimraf@~2.0.2: version "2.0.3" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.0.3.tgz#f50a2965e7144e9afd998982f15df706730f56a9" @@ -28185,6 +28220,14 @@ unlazy-loader@^0.1.3: dependencies: requires-regex "^0.3.3" +unload@2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/unload/-/unload-2.2.0.tgz#ccc88fdcad345faa06a92039ec0f80b488880ef7" + integrity sha512-B60uB5TNBLtN6/LsgAf3udH9saB5p7gqJwcFfbOEZ8BcBHnGwCf6G/TGiEqkRAxX7zAFIUtzdrXQSdL3Q/wqNA== + dependencies: + "@babel/runtime" "^7.6.2" + detect-node "^2.0.4" + unpipe@1.0.0, unpipe@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" From 1fb779b7d03a4bc970f1d987080b524d23fdec52 Mon Sep 17 00:00:00 2001 From: John Dorlus Date: Tue, 26 Nov 2019 11:34:03 -0500 Subject: [PATCH 05/12] Skipped these tests because their apps are not enabled on cloud. (#51677) --- x-pack/test/functional/apps/cross_cluster_replication/index.ts | 2 +- x-pack/test/functional/apps/remote_clusters/index.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/test/functional/apps/cross_cluster_replication/index.ts b/x-pack/test/functional/apps/cross_cluster_replication/index.ts index 21fc1982edc422..efcfaaba6037ce 100644 --- a/x-pack/test/functional/apps/cross_cluster_replication/index.ts +++ b/x-pack/test/functional/apps/cross_cluster_replication/index.ts @@ -8,7 +8,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default ({ loadTestFile }: FtrProviderContext) => { describe('Cross Cluster Replication app', function() { - this.tags('ciGroup4'); + this.tags(['ciGroup4', 'skipCloud']); loadTestFile(require.resolve('./home_page')); }); }; diff --git a/x-pack/test/functional/apps/remote_clusters/index.ts b/x-pack/test/functional/apps/remote_clusters/index.ts index dc47bd9de38156..9a4bc5b6a5cbd7 100644 --- a/x-pack/test/functional/apps/remote_clusters/index.ts +++ b/x-pack/test/functional/apps/remote_clusters/index.ts @@ -8,7 +8,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default ({ loadTestFile }: FtrProviderContext) => { describe('Remote Clusters app', function() { - this.tags('ciGroup4'); + this.tags(['ciGroup4', 'skipCloud']); loadTestFile(require.resolve('./home_page')); }); }; From 074f24ee32c5ef59c49eb89856e0502eb15f3d75 Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Tue, 26 Nov 2019 18:03:16 +0100 Subject: [PATCH 06/12] [APM] Fix watcher integration (#51721) Closes #51720. --- .../ServiceIntegrations/WatcherFlyout.tsx | 1 + .../__test__/createErrorGroupWatch.test.ts | 20 +++++++++++++------ .../createErrorGroupWatch.ts | 10 ++++++++-- .../apm/public/services/rest/callApi.ts | 7 +++---- .../services/rest/{watcher.js => watcher.ts} | 15 +++++++++++--- 5 files changed, 38 insertions(+), 15 deletions(-) rename x-pack/legacy/plugins/apm/public/services/rest/{watcher.js => watcher.ts} (60%) diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/WatcherFlyout.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/WatcherFlyout.tsx index d52c869b958722..18964531958f7a 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/WatcherFlyout.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/WatcherFlyout.tsx @@ -191,6 +191,7 @@ export class WatcherFlyout extends Component< ) as string; return createErrorGroupWatch({ + http: core.http, emails, schedule, serviceName, diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/__test__/createErrorGroupWatch.test.ts b/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/__test__/createErrorGroupWatch.test.ts index c7860b81a7b1ec..f05d343ad7ba5b 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/__test__/createErrorGroupWatch.test.ts +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/__test__/createErrorGroupWatch.test.ts @@ -7,22 +7,30 @@ import { isArray, isObject, isString } from 'lodash'; import mustache from 'mustache'; import uuid from 'uuid'; -// @ts-ignore import * as rest from '../../../../../services/rest/watcher'; import { createErrorGroupWatch } from '../createErrorGroupWatch'; import { esResponse } from './esResponse'; +import { HttpServiceBase } from 'kibana/public'; // disable html escaping since this is also disabled in watcher\s mustache implementation mustache.escape = value => value; +jest.mock('../../../../../services/rest/callApi', () => ({ + callApi: () => Promise.resolve(null) +})); + describe('createErrorGroupWatch', () => { let createWatchResponse: string; let tmpl: any; + const createWatchSpy = jest + .spyOn(rest, 'createWatch') + .mockResolvedValue(undefined); + beforeEach(async () => { jest.spyOn(uuid, 'v4').mockReturnValue(new Buffer('mocked-uuid')); - jest.spyOn(rest, 'createWatch').mockReturnValue(undefined); createWatchResponse = await createErrorGroupWatch({ + http: {} as HttpServiceBase, emails: ['my@email.dk', 'mySecond@email.dk'], schedule: { daily: { @@ -36,19 +44,19 @@ describe('createErrorGroupWatch', () => { apmIndexPatternTitle: 'myIndexPattern' }); - const watchBody = rest.createWatch.mock.calls[0][1]; + const watchBody = createWatchSpy.mock.calls[0][0].watch; const templateCtx = { payload: esResponse, metadata: watchBody.metadata }; - tmpl = renderMustache(rest.createWatch.mock.calls[0][1], templateCtx); + tmpl = renderMustache(createWatchSpy.mock.calls[0][0].watch, templateCtx); }); afterEach(() => jest.restoreAllMocks()); it('should call createWatch with correct args', () => { - expect(rest.createWatch.mock.calls[0][0]).toBe('apm-mocked-uuid'); + expect(createWatchSpy.mock.calls[0][0].id).toBe('apm-mocked-uuid'); }); it('should format slack message correctly', () => { @@ -78,7 +86,7 @@ describe('createErrorGroupWatch', () => { }); it('should return watch id', async () => { - const id = rest.createWatch.mock.calls[0][0]; + const id = createWatchSpy.mock.calls[0][0].id; expect(createWatchResponse).toEqual(id); }); }); diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/createErrorGroupWatch.ts b/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/createErrorGroupWatch.ts index e7d06403b8f8e1..1d21e35f122d92 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/createErrorGroupWatch.ts +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/createErrorGroupWatch.ts @@ -8,6 +8,7 @@ import { i18n } from '@kbn/i18n'; import { isEmpty } from 'lodash'; import url from 'url'; import uuid from 'uuid'; +import { HttpServiceBase } from 'kibana/public'; import { ERROR_CULPRIT, ERROR_EXC_HANDLED, @@ -17,7 +18,6 @@ import { PROCESSOR_EVENT, SERVICE_NAME } from '../../../../../common/elasticsearch_fieldnames'; -// @ts-ignore import { createWatch } from '../../../../services/rest/watcher'; function getSlackPathUrl(slackUrl?: string) { @@ -35,6 +35,7 @@ export interface Schedule { } interface Arguments { + http: HttpServiceBase; emails: string[]; schedule: Schedule; serviceName: string; @@ -54,6 +55,7 @@ interface Actions { } export async function createErrorGroupWatch({ + http, emails = [], schedule, serviceName, @@ -250,6 +252,10 @@ export async function createErrorGroupWatch({ }; } - await createWatch(id, body); + await createWatch({ + http, + id, + watch: body + }); return id; } diff --git a/x-pack/legacy/plugins/apm/public/services/rest/callApi.ts b/x-pack/legacy/plugins/apm/public/services/rest/callApi.ts index e1b61d06e35590..887200bdfc22a7 100644 --- a/x-pack/legacy/plugins/apm/public/services/rest/callApi.ts +++ b/x-pack/legacy/plugins/apm/public/services/rest/callApi.ts @@ -9,10 +9,11 @@ import LRU from 'lru-cache'; import hash from 'object-hash'; import { HttpServiceBase, HttpFetchOptions } from 'kibana/public'; -export type FetchOptions = HttpFetchOptions & { +export type FetchOptions = Omit & { pathname: string; forceCache?: boolean; method?: string; + body?: any; }; function fetchOptionsWithDebug(fetchOptions: FetchOptions) { @@ -26,9 +27,7 @@ function fetchOptionsWithDebug(fetchOptions: FetchOptions) { const body = isGet ? {} : { - body: JSON.stringify( - fetchOptions.body || ({} as HttpFetchOptions['body']) - ) + body: JSON.stringify(fetchOptions.body || {}) }; return { diff --git a/x-pack/legacy/plugins/apm/public/services/rest/watcher.js b/x-pack/legacy/plugins/apm/public/services/rest/watcher.ts similarity index 60% rename from x-pack/legacy/plugins/apm/public/services/rest/watcher.js rename to x-pack/legacy/plugins/apm/public/services/rest/watcher.ts index 9d68a1665912c8..dfa64b5368ee96 100644 --- a/x-pack/legacy/plugins/apm/public/services/rest/watcher.js +++ b/x-pack/legacy/plugins/apm/public/services/rest/watcher.ts @@ -4,12 +4,21 @@ * you may not use this file except in compliance with the Elastic License. */ +import { HttpServiceBase } from 'kibana/public'; import { callApi } from './callApi'; -export async function createWatch(id, watch) { - return callApi({ +export async function createWatch({ + id, + watch, + http +}: { + http: HttpServiceBase; + id: string; + watch: any; +}) { + return callApi(http, { method: 'PUT', pathname: `/api/watcher/watch/${id}`, - body: JSON.stringify({ type: 'json', id, watch }) + body: { type: 'json', id, watch } }); } From c8f0a751a775b2dbe1f8f26629ed00326a4a3479 Mon Sep 17 00:00:00 2001 From: Chris Roberson Date: Tue, 26 Nov 2019 12:20:37 -0500 Subject: [PATCH 07/12] [Timepicker] Ensure we filter out undefined values (#51458) * Fix error with undefined from or to * PR feedback * Remove unnecessary test --- src/plugins/data/public/query/timefilter/time_history.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/data/public/query/timefilter/time_history.ts b/src/plugins/data/public/query/timefilter/time_history.ts index 4dabbb557e9db2..fe73fd85b164d9 100644 --- a/src/plugins/data/public/query/timefilter/time_history.ts +++ b/src/plugins/data/public/query/timefilter/time_history.ts @@ -37,7 +37,7 @@ export class TimeHistory { } add(time: TimeRange) { - if (!time) { + if (!time || !time.from || !time.to) { return; } From 84489619bbc1f7d0e0d6104e0248116460074227 Mon Sep 17 00:00:00 2001 From: Tim Roes Date: Tue, 26 Nov 2019 18:21:02 +0100 Subject: [PATCH 08/12] Upgrade typescript-eslint to 2.9.0 (#51737) * Upgrade typescript-eslint to 2.9.0 * Remove redundant APM eslint disable --- package.json | 4 +- packages/eslint-config-kibana/package.json | 4 +- .../avg_duration_by_browser/transformer.ts | 2 - yarn.lock | 40 +++++++++---------- 4 files changed, 24 insertions(+), 26 deletions(-) diff --git a/package.json b/package.json index a4f7b869aef6f6..ce430891052689 100644 --- a/package.json +++ b/package.json @@ -349,8 +349,8 @@ "@types/uuid": "^3.4.4", "@types/vinyl-fs": "^2.4.11", "@types/zen-observable": "^0.8.0", - "@typescript-eslint/eslint-plugin": "^2.8.0", - "@typescript-eslint/parser": "^2.8.0", + "@typescript-eslint/eslint-plugin": "^2.9.0", + "@typescript-eslint/parser": "^2.9.0", "angular-mocks": "^1.7.8", "archiver": "^3.1.1", "axe-core": "^3.3.2", diff --git a/packages/eslint-config-kibana/package.json b/packages/eslint-config-kibana/package.json index 71517bc10404d6..ee65a1cf79148d 100644 --- a/packages/eslint-config-kibana/package.json +++ b/packages/eslint-config-kibana/package.json @@ -15,8 +15,8 @@ }, "homepage": "https://github.com/elastic/eslint-config-kibana#readme", "peerDependencies": { - "@typescript-eslint/eslint-plugin": "^2.8.0", - "@typescript-eslint/parser": "^2.8.0", + "@typescript-eslint/eslint-plugin": "^2.9.0", + "@typescript-eslint/parser": "^2.9.0", "babel-eslint": "^10.0.3", "eslint": "^6.5.1", "eslint-plugin-babel": "^5.3.0", diff --git a/x-pack/legacy/plugins/apm/server/lib/transactions/avg_duration_by_browser/transformer.ts b/x-pack/legacy/plugins/apm/server/lib/transactions/avg_duration_by_browser/transformer.ts index 805f8f192bdb19..5d140155f75e48 100644 --- a/x-pack/legacy/plugins/apm/server/lib/transactions/avg_duration_by_browser/transformer.ts +++ b/x-pack/legacy/plugins/apm/server/lib/transactions/avg_duration_by_browser/transformer.ts @@ -14,8 +14,6 @@ export function transformer({ response: ESResponse; }): AvgDurationByBrowserAPIResponse { const allUserAgentKeys = new Set( - // TODO(TS-3.7-ESLINT) - // eslint-disable-next-line @typescript-eslint/camelcase (response.aggregations?.user_agent_keys?.buckets ?? []).map(({ key }) => key.toString() ) diff --git a/yarn.lock b/yarn.lock index 7e965979fd46ff..1cf41a3ecd57c9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4154,24 +4154,24 @@ resolved "https://registry.yarnpkg.com/@types/zen-observable/-/zen-observable-0.8.0.tgz#8b63ab7f1aa5321248aad5ac890a485656dcea4d" integrity sha512-te5lMAWii1uEJ4FwLjzdlbw3+n0FZNOvFXHxQDKeT0dilh7HOzdMzV2TrJVUzq8ep7J4Na8OUYPRLSQkJHAlrg== -"@typescript-eslint/eslint-plugin@^2.8.0": - version "2.8.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.8.0.tgz#eca584d46094ebebc3cb3e9fb625bfbc904a534d" - integrity sha512-ohqul5s6XEB0AzPWZCuJF5Fd6qC0b4+l5BGEnrlpmvXxvyymb8yw8Bs4YMF8usNAeuCJK87eFIHy8g8GFvOtGA== +"@typescript-eslint/eslint-plugin@^2.9.0": + version "2.9.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.9.0.tgz#fa810282c0e45f6c2310b9c0dfcd25bff97ab7e9" + integrity sha512-98rfOt3NYn5Gr9wekTB8TexxN6oM8ZRvYuphPs1Atfsy419SDLYCaE30aJkRiiTCwGEY98vOhFsEVm7Zs4toQQ== dependencies: - "@typescript-eslint/experimental-utils" "2.8.0" + "@typescript-eslint/experimental-utils" "2.9.0" eslint-utils "^1.4.3" functional-red-black-tree "^1.0.1" regexpp "^3.0.0" tsutils "^3.17.1" -"@typescript-eslint/experimental-utils@2.8.0": - version "2.8.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-2.8.0.tgz#208b4164d175587e9b03ce6fea97d55f19c30ca9" - integrity sha512-jZ05E4SxCbbXseQGXOKf3ESKcsGxT8Ucpkp1jiVp55MGhOvZB2twmWKf894PAuVQTCgbPbJz9ZbRDqtUWzP8xA== +"@typescript-eslint/experimental-utils@2.9.0": + version "2.9.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-2.9.0.tgz#bbe99a8d9510240c055fc4b17230dd0192ba3c7f" + integrity sha512-0lOLFdpdJsCMqMSZT7l7W2ta0+GX8A3iefG3FovJjrX+QR8y6htFlFdU7aOVPL6pDvt6XcsOb8fxk5sq+girTw== dependencies: "@types/json-schema" "^7.0.3" - "@typescript-eslint/typescript-estree" "2.8.0" + "@typescript-eslint/typescript-estree" "2.9.0" eslint-scope "^5.0.0" "@typescript-eslint/experimental-utils@^1.13.0": @@ -4183,14 +4183,14 @@ "@typescript-eslint/typescript-estree" "1.13.0" eslint-scope "^4.0.0" -"@typescript-eslint/parser@^2.8.0": - version "2.8.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-2.8.0.tgz#e10f7c40c8cf2fb19920c879311e6c46ad17bacb" - integrity sha512-NseXWzhkucq+JM2HgqAAoKEzGQMb5LuTRjFPLQzGIdLthXMNUfuiskbl7QSykvWW6mvzCtYbw1fYWGa2EIaekw== +"@typescript-eslint/parser@^2.9.0": + version "2.9.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-2.9.0.tgz#2e9cf438de119b143f642a3a406e1e27eb70b7cd" + integrity sha512-fJ+dNs3CCvEsJK2/Vg5c2ZjuQ860ySOAsodDPwBaVlrGvRN+iCNC8kUfLFL8cT49W4GSiLPa/bHiMjYXA7EhKQ== dependencies: "@types/eslint-visitor-keys" "^1.0.0" - "@typescript-eslint/experimental-utils" "2.8.0" - "@typescript-eslint/typescript-estree" "2.8.0" + "@typescript-eslint/experimental-utils" "2.9.0" + "@typescript-eslint/typescript-estree" "2.9.0" eslint-visitor-keys "^1.1.0" "@typescript-eslint/typescript-estree@1.13.0": @@ -4201,10 +4201,10 @@ lodash.unescape "4.0.1" semver "5.5.0" -"@typescript-eslint/typescript-estree@2.8.0": - version "2.8.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-2.8.0.tgz#fcc3fe6532840085d29b75432c8a59895876aeca" - integrity sha512-ksvjBDTdbAQ04cR5JyFSDX113k66FxH1tAXmi+dj6hufsl/G0eMc/f1GgLjEVPkYClDbRKv+rnBFuE5EusomUw== +"@typescript-eslint/typescript-estree@2.9.0": + version "2.9.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-2.9.0.tgz#09138daf8f47d0e494ba7db9e77394e928803017" + integrity sha512-v6btSPXEWCP594eZbM+JCXuFoXWXyF/z8kaSBSdCb83DF+Y7+xItW29SsKtSULgLemqJBT+LpT+0ZqdfH7QVmA== dependencies: debug "^4.1.1" eslint-visitor-keys "^1.1.0" From 3b6e51b2d8078180a0e6e7cfdadead4d6ae07ead Mon Sep 17 00:00:00 2001 From: patrykkopycinski Date: Tue, 26 Nov 2019 19:56:52 +0100 Subject: [PATCH 09/12] [SIEM] Fix styled-components bump in siem app (#51559) --- .../components/edit_data_provider/index.tsx | 176 +++++++++--------- .../siem/public/components/loading/index.tsx | 46 ++--- .../siem/public/components/page/index.tsx | 16 +- .../column_headers/events_select/index.tsx | 4 +- .../timeline/data_providers/empty.tsx | 2 +- .../timeline/data_providers/providers.tsx | 2 +- .../components/timeline/properties/index.tsx | 39 ---- .../search_or_filter/search_or_filter.tsx | 88 ++++----- .../public/components/wrapper_page/index.tsx | 2 + 9 files changed, 177 insertions(+), 198 deletions(-) diff --git a/x-pack/legacy/plugins/siem/public/components/edit_data_provider/index.tsx b/x-pack/legacy/plugins/siem/public/components/edit_data_provider/index.tsx index 214ac926e88682..18b271a3abc297 100644 --- a/x-pack/legacy/plugins/siem/public/components/edit_data_provider/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/edit_data_provider/index.tsx @@ -49,8 +49,7 @@ HeaderContainer.displayName = 'HeaderContainer'; // SIDE EFFECT: the following `createGlobalStyle` overrides the default styling // of euiComboBoxOptionsList because it's implemented as a popover, so it's // not selectable as a child of the styled component -// eslint-disable-next-line no-unused-expressions -createGlobalStyle` +const StatefulEditDataProviderGlobalStyle = createGlobalStyle` .euiComboBoxOptionsList { z-index: 9999; } @@ -158,104 +157,107 @@ export const StatefulEditDataProvider = React.memo( }, []); return ( - - - - - - - 0 ? updatedField[0].label : null}> + <> + + + + + + + 0 ? updatedField[0].label : null}> + + + + + + + - - - + + + + + + + + + {updatedOperator.length > 0 && + updatedOperator[0].label !== i18n.EXISTS && + updatedOperator[0].label !== i18n.DOES_NOT_EXIST ? ( - - + - - - - - - + ) : null} - {updatedOperator.length > 0 && - updatedOperator[0].label !== i18n.EXISTS && - updatedOperator[0].label !== i18n.DOES_NOT_EXIST ? ( - - - + - ) : null} - - - - - - - - { - onDataProviderEdited({ - andProviderId, - excluded: getExcludedFromSelection(updatedOperator), - field: updatedField.length > 0 ? updatedField[0].label : '', - id: timelineId, - operator: getQueryOperatorFromSelection(updatedOperator), - providerId, - value: updatedValue, - }); - }} - size="s" - > - {i18n.SAVE} - - - - - - + + + + { + onDataProviderEdited({ + andProviderId, + excluded: getExcludedFromSelection(updatedOperator), + field: updatedField.length > 0 ? updatedField[0].label : '', + id: timelineId, + operator: getQueryOperatorFromSelection(updatedOperator), + providerId, + value: updatedValue, + }); + }} + size="s" + > + {i18n.SAVE} + + + + + + + + ); } ); diff --git a/x-pack/legacy/plugins/siem/public/components/loading/index.tsx b/x-pack/legacy/plugins/siem/public/components/loading/index.tsx index eb85edce78a8ff..42867c09b971b2 100644 --- a/x-pack/legacy/plugins/siem/public/components/loading/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/loading/index.tsx @@ -10,8 +10,7 @@ import { pure } from 'recompose'; import styled, { createGlobalStyle } from 'styled-components'; // SIDE EFFECT: the following `createGlobalStyle` overrides default styling in angular code that was not theme-friendly -// eslint-disable-next-line no-unused-expressions -createGlobalStyle` +const LoadingPanelGlobalStyle = createGlobalStyle` .euiPanel-loading-hide-border { border: none; } @@ -41,27 +40,30 @@ export const LoadingPanel = pure( position = 'relative', zIndex = 'inherit', }) => ( - - - - - - - + <> + + + + + + + - - {text} - - - - - + + {text} + + + + + + + ) ); diff --git a/x-pack/legacy/plugins/siem/public/components/page/index.tsx b/x-pack/legacy/plugins/siem/public/components/page/index.tsx index bc701006c3a9c0..d56012de88929f 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/index.tsx @@ -15,9 +15,12 @@ import { } from '@elastic/eui'; import styled, { createGlobalStyle } from 'styled-components'; -// SIDE EFFECT: the following `createGlobalStyle` overrides default styling in angular code that was not theme-friendly -// eslint-disable-next-line no-unused-expressions -createGlobalStyle` +/* + SIDE EFFECT: the following `createGlobalStyle` overrides default styling in angular code that was not theme-friendly + and `EuiPopover`, `EuiToolTip` global styles +*/ + +export const AppGlobalStyle = createGlobalStyle` div.app-wrapper { background-color: rgba(0,0,0,0); } @@ -25,6 +28,13 @@ createGlobalStyle` div.application { background-color: rgba(0,0,0,0); } + + .euiPopover__panel.euiPopover__panel-isOpen { + z-index: 9900 !important; + } + .euiToolTip { + z-index: 9950 !important; + } `; export const DescriptionListStyled = styled(EuiDescriptionList)` diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/events_select/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/events_select/index.tsx index 747ef8f3ffe47b..4f414af74a9143 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/events_select/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/events_select/index.tsx @@ -17,8 +17,7 @@ export const EVENTS_SELECT_WIDTH = 60; // px // SIDE EFFECT: the following `createGlobalStyle` overrides // the style of the select items -// eslint-disable-next-line -createGlobalStyle` +const EventsSelectGlobalStyle = createGlobalStyle` .eventsSelectItem { width: 100% !important; @@ -73,6 +72,7 @@ export const EventsSelect = pure(({ checkState, timelineId }) => { /> + ); }); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/empty.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/empty.tsx index 29d2df51724576..3ef7240ee03758 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/empty.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/empty.tsx @@ -50,7 +50,7 @@ const HighlightedBackground = styled.span` HighlightedBackground.displayName = 'HighlightedBackground'; const EmptyContainer = styled.div<{ showSmallMsg: boolean }>` - width: ${props => (props.showSmallMsg ? '60px' : 'auto')} + width: ${props => (props.showSmallMsg ? '60px' : 'auto')}; align-items: center; display: flex; flex-direction: row; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/providers.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/providers.tsx index 112962367cd362..5a8654509fa881 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/providers.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/providers.tsx @@ -46,7 +46,7 @@ interface Props { const ROW_OF_DATA_PROVIDERS_HEIGHT = 43; // px const PanelProviders = styled.div` - position: relative + position: relative; display: flex; flex-direction: row; min-height: 100px; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/properties/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/properties/index.tsx index ccc222673d7bc5..7b69e006f48ad3 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/properties/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/properties/index.tsx @@ -4,9 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiAvatar, EuiFlexItem, EuiIcon } from '@elastic/eui'; import React, { useState, useCallback } from 'react'; -import styled, { createGlobalStyle } from 'styled-components'; import { Note } from '../../../lib/note'; import { InputsModelId } from '../../../store/inputs/constants'; @@ -22,43 +20,6 @@ type UpdateTitle = ({ id, title }: { id: string; title: string }) => void; type UpdateDescription = ({ id, description }: { id: string; description: string }) => void; type ToggleLock = ({ linkToId }: { linkToId: InputsModelId }) => void; -// SIDE EFFECT: the following `createGlobalStyle` overrides `EuiPopover` -// and `EuiToolTip` global styles: -// eslint-disable-next-line no-unused-expressions -createGlobalStyle` - .euiPopover__panel.euiPopover__panel-isOpen { - z-index: 9900 !important; - } - .euiToolTip { - z-index: 9950 !important; - } -`; - -const Avatar = styled(EuiAvatar)` - margin-left: 5px; -`; - -Avatar.displayName = 'Avatar'; - -const DescriptionPopoverMenuContainer = styled.div` - margin-top: 15px; -`; - -DescriptionPopoverMenuContainer.displayName = 'DescriptionPopoverMenuContainer'; - -const SettingsIcon = styled(EuiIcon)` - margin-left: 4px; - cursor: pointer; -`; - -SettingsIcon.displayName = 'SettingsIcon'; - -const HiddenFlexItem = styled(EuiFlexItem)` - display: none; -`; - -HiddenFlexItem.displayName = 'HiddenFlexItem'; - interface Props { associateNote: AssociateNote; createTimeline: CreateTimeline; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/search_or_filter.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/search_or_filter.tsx index 2d953ce3cfc951..eaa476bf3e2b27 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/search_or_filter.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/search_or_filter.tsx @@ -26,8 +26,7 @@ const searchOrFilterPopoverClassName = 'searchOrFilterPopover'; const searchOrFilterPopoverWidth = '352px'; // SIDE EFFECT: the following creates a global class selector -// eslint-disable-next-line no-unused-expressions -createGlobalStyle` +const SearchOrFilterGlobalStyle = createGlobalStyle` .${timelineSelectModeItemsClassName} { width: 350px !important; } @@ -110,48 +109,51 @@ export const SearchOrFilter = pure( updateKqlMode, updateReduxTime, }) => ( - - - - - updateKqlMode({ id: timelineId, kqlMode: mode })} - options={options} - popoverClassName={searchOrFilterPopoverClassName} - valueOfSelected={kqlMode} + <> + + + + + updateKqlMode({ id: timelineId, kqlMode: mode })} + options={options} + popoverClassName={searchOrFilterPopoverClassName} + valueOfSelected={kqlMode} + /> + + + + - - - - - - - + + + + + ) ); diff --git a/x-pack/legacy/plugins/siem/public/components/wrapper_page/index.tsx b/x-pack/legacy/plugins/siem/public/components/wrapper_page/index.tsx index 5998aa527206e7..309693427459e4 100644 --- a/x-pack/legacy/plugins/siem/public/components/wrapper_page/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/wrapper_page/index.tsx @@ -9,6 +9,7 @@ import React from 'react'; import styled, { css } from 'styled-components'; import { gutterTimeline } from '../../lib/helpers'; +import { AppGlobalStyle } from '../page/index'; const Wrapper = styled.div` ${({ theme }) => css` @@ -54,6 +55,7 @@ export const WrapperPage = React.memo( return ( {children} + ); } From f5296293c25504f280ce18c70f90f9e5e53e95c3 Mon Sep 17 00:00:00 2001 From: Corey Robertson Date: Tue, 26 Nov 2019 14:22:02 -0500 Subject: [PATCH 10/12] [Canvas] Move workpad api routes to New Platform (#51116) * Move workpad api routes to New Platform * Cleanup * Clean up/Pr Feedback * Adding missing dependency to tests * Fix typecheck * Loosen workpad schema restrictions --- .../canvas/__tests__/fixtures/workpads.ts | 13 + .../canvas/public/lib/workpad_service.js | 1 + .../plugins/canvas/server/routes/index.ts | 2 - .../canvas/server/routes/workpad.test.js | 462 ------------------ .../plugins/canvas/server/routes/workpad.ts | 254 ---------- x-pack/plugins/canvas/kibana.json | 10 + x-pack/plugins/canvas/server/index.ts | 11 + x-pack/plugins/canvas/server/plugin.ts | 25 + .../server/routes/catch_error_handler.ts | 30 ++ x-pack/plugins/canvas/server/routes/index.ts | 17 + .../server/routes/workpad/create.test.ts | 102 ++++ .../canvas/server/routes/workpad/create.ts | 57 +++ .../server/routes/workpad/delete.test.ts | 78 +++ .../canvas/server/routes/workpad/delete.ts | 32 ++ .../canvas/server/routes/workpad/find.test.ts | 113 +++++ .../canvas/server/routes/workpad/find.ts | 60 +++ .../canvas/server/routes/workpad/get.test.ts | 140 ++++++ .../canvas/server/routes/workpad/get.ts | 65 +++ .../canvas/server/routes/workpad/index.ts | 21 + .../server/routes/workpad/ok_response.ts | 9 + .../server/routes/workpad/update.test.ts | 223 +++++++++ .../canvas/server/routes/workpad/update.ts | 129 +++++ .../server/routes/workpad/workpad_schema.ts | 65 +++ 23 files changed, 1201 insertions(+), 718 deletions(-) delete mode 100644 x-pack/legacy/plugins/canvas/server/routes/workpad.test.js delete mode 100644 x-pack/legacy/plugins/canvas/server/routes/workpad.ts create mode 100644 x-pack/plugins/canvas/kibana.json create mode 100644 x-pack/plugins/canvas/server/index.ts create mode 100644 x-pack/plugins/canvas/server/plugin.ts create mode 100644 x-pack/plugins/canvas/server/routes/catch_error_handler.ts create mode 100644 x-pack/plugins/canvas/server/routes/index.ts create mode 100644 x-pack/plugins/canvas/server/routes/workpad/create.test.ts create mode 100644 x-pack/plugins/canvas/server/routes/workpad/create.ts create mode 100644 x-pack/plugins/canvas/server/routes/workpad/delete.test.ts create mode 100644 x-pack/plugins/canvas/server/routes/workpad/delete.ts create mode 100644 x-pack/plugins/canvas/server/routes/workpad/find.test.ts create mode 100644 x-pack/plugins/canvas/server/routes/workpad/find.ts create mode 100644 x-pack/plugins/canvas/server/routes/workpad/get.test.ts create mode 100644 x-pack/plugins/canvas/server/routes/workpad/get.ts create mode 100644 x-pack/plugins/canvas/server/routes/workpad/index.ts create mode 100644 x-pack/plugins/canvas/server/routes/workpad/ok_response.ts create mode 100644 x-pack/plugins/canvas/server/routes/workpad/update.test.ts create mode 100644 x-pack/plugins/canvas/server/routes/workpad/update.ts create mode 100644 x-pack/plugins/canvas/server/routes/workpad/workpad_schema.ts diff --git a/x-pack/legacy/plugins/canvas/__tests__/fixtures/workpads.ts b/x-pack/legacy/plugins/canvas/__tests__/fixtures/workpads.ts index d7ebbd87c97e6c..271fc7a9790577 100644 --- a/x-pack/legacy/plugins/canvas/__tests__/fixtures/workpads.ts +++ b/x-pack/legacy/plugins/canvas/__tests__/fixtures/workpads.ts @@ -192,3 +192,16 @@ export const elements: CanvasElement[] = [ { ...BaseElement, expression: 'filters | demodata | pointseries | pie | render' }, { ...BaseElement, expression: 'image | render' }, ]; + +export const workpadWithGroupAsElement: CanvasWorkpad = { + ...BaseWorkpad, + pages: [ + { + ...BasePage, + elements: [ + { ...BaseElement, expression: 'image | render' }, + { ...BaseElement, id: 'group-1234' }, + ], + }, + ], +}; diff --git a/x-pack/legacy/plugins/canvas/public/lib/workpad_service.js b/x-pack/legacy/plugins/canvas/public/lib/workpad_service.js index 33067f1837f41d..f1ed069c15d4d7 100644 --- a/x-pack/legacy/plugins/canvas/public/lib/workpad_service.js +++ b/x-pack/legacy/plugins/canvas/public/lib/workpad_service.js @@ -29,6 +29,7 @@ export function get(workpadId) { }); } +// TODO: I think this function is never used. Look into and remove the corresponding route as well export function update(id, workpad) { return fetch.put(`${apiPath}/${id}`, workpad); } diff --git a/x-pack/legacy/plugins/canvas/server/routes/index.ts b/x-pack/legacy/plugins/canvas/server/routes/index.ts index a0502c5e891a22..515d5b5e895edf 100644 --- a/x-pack/legacy/plugins/canvas/server/routes/index.ts +++ b/x-pack/legacy/plugins/canvas/server/routes/index.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { workpad } from './workpad'; import { esFields } from './es_fields'; import { customElements } from './custom_elements'; import { shareableWorkpads } from './shareables'; @@ -13,6 +12,5 @@ import { CoreSetup } from '../shim'; export function routes(setup: CoreSetup): void { customElements(setup.http.route, setup.elasticsearch); esFields(setup.http.route, setup.elasticsearch); - workpad(setup.http.route, setup.elasticsearch); shareableWorkpads(setup.http.route); } diff --git a/x-pack/legacy/plugins/canvas/server/routes/workpad.test.js b/x-pack/legacy/plugins/canvas/server/routes/workpad.test.js deleted file mode 100644 index 09a5c3b89c31eb..00000000000000 --- a/x-pack/legacy/plugins/canvas/server/routes/workpad.test.js +++ /dev/null @@ -1,462 +0,0 @@ -/* - * 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 Hapi from 'hapi'; -import { - CANVAS_TYPE, - API_ROUTE_WORKPAD, - API_ROUTE_WORKPAD_ASSETS, - API_ROUTE_WORKPAD_STRUCTURES, -} from '../../common/lib/constants'; -import { workpad } from './workpad'; - -const routePrefix = API_ROUTE_WORKPAD; -const routePrefixAssets = API_ROUTE_WORKPAD_ASSETS; -const routePrefixStructures = API_ROUTE_WORKPAD_STRUCTURES; - -jest.mock('uuid/v4', () => jest.fn().mockReturnValue('123abc')); - -describe(`${CANVAS_TYPE} API`, () => { - const savedObjectsClient = { - get: jest.fn(), - create: jest.fn(), - delete: jest.fn(), - find: jest.fn(), - }; - - afterEach(() => { - savedObjectsClient.get.mockReset(); - savedObjectsClient.create.mockReset(); - savedObjectsClient.delete.mockReset(); - savedObjectsClient.find.mockReset(); - }); - - // Mock toISOString function of all Date types - global.Date = class Date extends global.Date { - toISOString() { - return '2019-02-12T21:01:22.479Z'; - } - }; - - // Setup mock server - const mockServer = new Hapi.Server({ debug: false, port: 0 }); - const mockEs = { - getCluster: () => ({ - errors: { - // formatResponse will fail without objects here - '400': Error, - '401': Error, - '403': Error, - '404': Error, - }, - }), - }; - - mockServer.ext('onRequest', (req, h) => { - req.getSavedObjectsClient = () => savedObjectsClient; - return h.continue; - }); - workpad(mockServer.route.bind(mockServer), mockEs); - - describe(`GET ${routePrefix}/{id}`, () => { - test('returns successful response', async () => { - const request = { - method: 'GET', - url: `${routePrefix}/123`, - }; - - savedObjectsClient.get.mockResolvedValueOnce({ id: '123', attributes: { foo: true } }); - - const { payload, statusCode } = await mockServer.inject(request); - const response = JSON.parse(payload); - - expect(statusCode).toBe(200); - expect(response).toMatchInlineSnapshot(` -Object { - "foo": true, - "id": "123", -} -`); - expect(savedObjectsClient.get.mock.calls).toMatchInlineSnapshot(` -Array [ - Array [ - "canvas-workpad", - "123", - ], -] -`); - }); - }); - - describe(`POST ${routePrefix}`, () => { - test('returns successful response without id in payload', async () => { - const request = { - method: 'POST', - url: routePrefix, - payload: { - foo: true, - }, - }; - - savedObjectsClient.create.mockResolvedValueOnce({}); - - const { payload, statusCode } = await mockServer.inject(request); - const response = JSON.parse(payload); - - expect(statusCode).toBe(200); - expect(response).toMatchInlineSnapshot(` -Object { - "ok": true, -} -`); - expect(savedObjectsClient.create.mock.calls).toMatchInlineSnapshot(` -Array [ - Array [ - "canvas-workpad", - Object { - "@created": "2019-02-12T21:01:22.479Z", - "@timestamp": "2019-02-12T21:01:22.479Z", - "foo": true, - }, - Object { - "id": "workpad-123abc", - }, - ], -] -`); - }); - - test('returns succesful response with id in payload', async () => { - const request = { - method: 'POST', - url: routePrefix, - payload: { - id: '123', - foo: true, - }, - }; - - savedObjectsClient.create.mockResolvedValueOnce({}); - - const { payload, statusCode } = await mockServer.inject(request); - const response = JSON.parse(payload); - - expect(statusCode).toBe(200); - expect(response).toMatchInlineSnapshot(` -Object { - "ok": true, -} -`); - expect(savedObjectsClient.create.mock.calls).toMatchInlineSnapshot(` -Array [ - Array [ - "canvas-workpad", - Object { - "@created": "2019-02-12T21:01:22.479Z", - "@timestamp": "2019-02-12T21:01:22.479Z", - "foo": true, - }, - Object { - "id": "123", - }, - ], -] -`); - }); - }); - - describe(`PUT ${routePrefix}/{id}`, () => { - test('formats successful response', async () => { - const request = { - method: 'PUT', - url: `${routePrefix}/123`, - payload: { - id: '234', - foo: true, - }, - }; - - savedObjectsClient.get.mockResolvedValueOnce({ - attributes: { - '@created': new Date().toISOString(), - }, - }); - savedObjectsClient.create.mockResolvedValueOnce({}); - - const { payload, statusCode } = await mockServer.inject(request); - const response = JSON.parse(payload); - - expect(statusCode).toBe(200); - expect(response).toMatchInlineSnapshot(` -Object { - "ok": true, -} -`); - expect(savedObjectsClient.get.mock.calls).toMatchInlineSnapshot(` -Array [ - Array [ - "canvas-workpad", - "123", - ], -] -`); - expect(savedObjectsClient.create.mock.calls).toMatchInlineSnapshot(` -Array [ - Array [ - "canvas-workpad", - Object { - "@created": "2019-02-12T21:01:22.479Z", - "@timestamp": "2019-02-12T21:01:22.479Z", - "foo": true, - }, - Object { - "id": "123", - "overwrite": true, - }, - ], -] -`); - }); - }); - - describe(`DELETE ${routePrefix}/{id}`, () => { - test('formats successful response', async () => { - const request = { - method: 'DELETE', - url: `${routePrefix}/123`, - }; - - savedObjectsClient.delete.mockResolvedValueOnce({}); - - const { payload, statusCode } = await mockServer.inject(request); - const response = JSON.parse(payload); - - expect(statusCode).toBe(200); - expect(response).toMatchInlineSnapshot(` -Object { - "ok": true, -} -`); - expect(savedObjectsClient.delete.mock.calls).toMatchInlineSnapshot(` -Array [ - Array [ - "canvas-workpad", - "123", - ], -] -`); - }); - }); - - it(`GET ${routePrefix}/find`, async () => { - const request = { - method: 'GET', - url: `${routePrefix}/find?name=abc&page=2&perPage=10`, - }; - - savedObjectsClient.find.mockResolvedValueOnce({ - saved_objects: [ - { - id: '1', - attributes: { - foo: true, - }, - }, - ], - }); - - const { payload, statusCode } = await mockServer.inject(request); - const response = JSON.parse(payload); - - expect(statusCode).toBe(200); - expect(response).toMatchInlineSnapshot(` -Object { - "workpads": Array [ - Object { - "foo": true, - "id": "1", - }, - ], -} -`); - expect(savedObjectsClient.find.mock.calls).toMatchInlineSnapshot(` -Array [ - Array [ - Object { - "fields": Array [ - "id", - "name", - "@created", - "@timestamp", - ], - "page": "2", - "perPage": "10", - "search": "abc* | abc", - "searchFields": Array [ - "name", - ], - "sortField": "@timestamp", - "sortOrder": "desc", - "type": "canvas-workpad", - }, - ], -] -`); - }); - - describe(`PUT ${routePrefixAssets}/{id}`, () => { - test('only updates assets', async () => { - const request = { - method: 'PUT', - url: `${routePrefixAssets}/123`, - payload: { - 'asset-123': { - id: 'asset-123', - '@created': '2019-02-14T00:00:00.000Z', - type: 'dataurl', - value: 'mockbase64data', - }, - 'asset-456': { - id: 'asset-456', - '@created': '2019-02-15T00:00:00.000Z', - type: 'dataurl', - value: 'mockbase64data', - }, - }, - }; - - // provide some existing workpad data to check that it's preserved - savedObjectsClient.get.mockResolvedValueOnce({ - attributes: { - '@created': new Date().toISOString(), - name: 'fake workpad', - }, - }); - savedObjectsClient.create.mockResolvedValueOnce({}); - - const { payload, statusCode } = await mockServer.inject(request); - const response = JSON.parse(payload); - - expect(statusCode).toBe(200); - expect(response).toMatchInlineSnapshot(` -Object { - "ok": true, -} -`); - expect(savedObjectsClient.get.mock.calls).toMatchInlineSnapshot(` -Array [ - Array [ - "canvas-workpad", - "123", - ], -] -`); - expect(savedObjectsClient.create.mock.calls).toMatchInlineSnapshot(` -Array [ - Array [ - "canvas-workpad", - Object { - "@created": "2019-02-12T21:01:22.479Z", - "@timestamp": "2019-02-12T21:01:22.479Z", - "assets": Object { - "asset-123": Object { - "@created": "2019-02-14T00:00:00.000Z", - "id": "asset-123", - "type": "dataurl", - "value": "mockbase64data", - }, - "asset-456": Object { - "@created": "2019-02-15T00:00:00.000Z", - "id": "asset-456", - "type": "dataurl", - "value": "mockbase64data", - }, - }, - "name": "fake workpad", - }, - Object { - "id": "123", - "overwrite": true, - }, - ], -] -`); - }); - }); - - describe(`PUT ${routePrefixStructures}/{id}`, () => { - test('only updates workpad', async () => { - const request = { - method: 'PUT', - url: `${routePrefixStructures}/123`, - payload: { - name: 'renamed workpad', - css: '.canvasPage { color: LavenderBlush; }', - }, - }; - - // provide some existing asset data and a name to replace - savedObjectsClient.get.mockResolvedValueOnce({ - attributes: { - '@created': new Date().toISOString(), - name: 'fake workpad', - assets: { - 'asset-123': { - id: 'asset-123', - '@created': '2019-02-14T00:00:00.000Z', - type: 'dataurl', - value: 'mockbase64data', - }, - }, - }, - }); - savedObjectsClient.create.mockResolvedValueOnce({}); - - const { payload, statusCode } = await mockServer.inject(request); - const response = JSON.parse(payload); - - expect(statusCode).toBe(200); - expect(response).toMatchInlineSnapshot(` -Object { - "ok": true, -} -`); - expect(savedObjectsClient.get.mock.calls).toMatchInlineSnapshot(` -Array [ - Array [ - "canvas-workpad", - "123", - ], -] -`); - expect(savedObjectsClient.create.mock.calls).toMatchInlineSnapshot(` -Array [ - Array [ - "canvas-workpad", - Object { - "@created": "2019-02-12T21:01:22.479Z", - "@timestamp": "2019-02-12T21:01:22.479Z", - "assets": Object { - "asset-123": Object { - "@created": "2019-02-14T00:00:00.000Z", - "id": "asset-123", - "type": "dataurl", - "value": "mockbase64data", - }, - }, - "css": ".canvasPage { color: LavenderBlush; }", - "name": "renamed workpad", - }, - Object { - "id": "123", - "overwrite": true, - }, - ], -] -`); - }); - }); -}); diff --git a/x-pack/legacy/plugins/canvas/server/routes/workpad.ts b/x-pack/legacy/plugins/canvas/server/routes/workpad.ts deleted file mode 100644 index 380fe97ca9ef10..00000000000000 --- a/x-pack/legacy/plugins/canvas/server/routes/workpad.ts +++ /dev/null @@ -1,254 +0,0 @@ -/* - * 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 boom from 'boom'; -import { omit } from 'lodash'; -import { SavedObjectsClientContract, SavedObjectAttributes } from 'src/core/server'; -import { - CANVAS_TYPE, - API_ROUTE_WORKPAD, - API_ROUTE_WORKPAD_ASSETS, - API_ROUTE_WORKPAD_STRUCTURES, -} from '../../common/lib/constants'; -import { getId } from '../../public/lib/get_id'; -import { CoreSetup } from '../shim'; -// @ts-ignore Untyped Local -import { formatResponse as formatRes } from '../lib/format_response'; -import { CanvasWorkpad } from '../../types'; - -type WorkpadAttributes = Pick> & { - '@timestamp': string; - '@created': string; -}; - -interface WorkpadRequestFacade { - getSavedObjectsClient: () => SavedObjectsClientContract; -} - -type WorkpadRequest = WorkpadRequestFacade & { - params: { - id: string; - }; - payload: CanvasWorkpad; -}; - -type FindWorkpadRequest = WorkpadRequestFacade & { - query: { - name: string; - page: number; - perPage: number; - }; -}; - -type AssetsRequest = WorkpadRequestFacade & { - params: { - id: string; - }; - payload: CanvasWorkpad['assets']; -}; - -export function workpad( - route: CoreSetup['http']['route'], - elasticsearch: CoreSetup['elasticsearch'] -) { - // @ts-ignore EsErrors is not on the Cluster type - const { errors: esErrors } = elasticsearch.getCluster('data'); - const routePrefix = API_ROUTE_WORKPAD; - const routePrefixAssets = API_ROUTE_WORKPAD_ASSETS; - const routePrefixStructures = API_ROUTE_WORKPAD_STRUCTURES; - const formatResponse = formatRes(esErrors); - - function createWorkpad(req: WorkpadRequest) { - const savedObjectsClient = req.getSavedObjectsClient(); - - if (!req.payload) { - return Promise.reject(boom.badRequest('A workpad payload is required')); - } - - const now = new Date().toISOString(); - const { id, ...payload } = req.payload; - return savedObjectsClient.create( - CANVAS_TYPE, - { - ...payload, - '@timestamp': now, - '@created': now, - }, - { id: id || getId('workpad') } - ); - } - - function updateWorkpad( - req: WorkpadRequest | AssetsRequest, - newPayload?: CanvasWorkpad | { assets: CanvasWorkpad['assets'] } - ) { - const savedObjectsClient = req.getSavedObjectsClient(); - const { id } = req.params; - const payload = newPayload ? newPayload : req.payload; - - const now = new Date().toISOString(); - - return savedObjectsClient.get(CANVAS_TYPE, id).then(workpadObject => { - // TODO: Using create with force over-write because of version conflict issues with update - return savedObjectsClient.create( - CANVAS_TYPE, - { - ...(workpadObject.attributes as SavedObjectAttributes), - ...omit(payload, 'id'), // never write the id property - '@timestamp': now, // always update the modified time - '@created': workpadObject.attributes['@created'], // ensure created is not modified - }, - { overwrite: true, id } - ); - }); - } - - function deleteWorkpad(req: WorkpadRequest) { - const savedObjectsClient = req.getSavedObjectsClient(); - const { id } = req.params; - - return savedObjectsClient.delete(CANVAS_TYPE, id); - } - - function findWorkpad(req: FindWorkpadRequest) { - const savedObjectsClient = req.getSavedObjectsClient(); - const { name, page, perPage } = req.query; - - return savedObjectsClient.find({ - type: CANVAS_TYPE, - sortField: '@timestamp', - sortOrder: 'desc', - search: name ? `${name}* | ${name}` : '*', - searchFields: ['name'], - fields: ['id', 'name', '@created', '@timestamp'], - page, - perPage, - }); - } - - // get workpad - route({ - method: 'GET', - path: `${routePrefix}/{id}`, - handler(req: WorkpadRequest) { - const savedObjectsClient = req.getSavedObjectsClient(); - const { id } = req.params; - - return savedObjectsClient - .get(CANVAS_TYPE, id) - .then(obj => { - if ( - // not sure if we need to be this defensive - obj.type === 'canvas-workpad' && - obj.attributes && - obj.attributes.pages && - obj.attributes.pages.length - ) { - obj.attributes.pages.forEach(page => { - const elements = (page.elements || []).filter( - ({ id: pageId }) => !pageId.startsWith('group') - ); - const groups = (page.groups || []).concat( - (page.elements || []).filter(({ id: pageId }) => pageId.startsWith('group')) - ); - page.elements = elements; - page.groups = groups; - }); - } - return obj; - }) - .then(obj => ({ id: obj.id, ...obj.attributes })) - .then(formatResponse) - .catch(formatResponse); - }, - }); - - // create workpad - route({ - method: 'POST', - path: routePrefix, - // @ts-ignore config option missing on route method type - config: { payload: { allow: 'application/json', maxBytes: 26214400 } }, // 25MB payload limit - handler(request: WorkpadRequest) { - return createWorkpad(request) - .then(() => ({ ok: true })) - .catch(formatResponse); - }, - }); - - // update workpad - route({ - method: 'PUT', - path: `${routePrefix}/{id}`, - // @ts-ignore config option missing on route method type - config: { payload: { allow: 'application/json', maxBytes: 26214400 } }, // 25MB payload limit - handler(request: WorkpadRequest) { - return updateWorkpad(request) - .then(() => ({ ok: true })) - .catch(formatResponse); - }, - }); - - // update workpad assets - route({ - method: 'PUT', - path: `${routePrefixAssets}/{id}`, - // @ts-ignore config option missing on route method type - config: { payload: { allow: 'application/json', maxBytes: 26214400 } }, // 25MB payload limit - handler(request: AssetsRequest) { - const payload = { assets: request.payload }; - return updateWorkpad(request, payload) - .then(() => ({ ok: true })) - .catch(formatResponse); - }, - }); - - // update workpad structures - route({ - method: 'PUT', - path: `${routePrefixStructures}/{id}`, - // @ts-ignore config option missing on route method type - config: { payload: { allow: 'application/json', maxBytes: 26214400 } }, // 25MB payload limit - handler(request: WorkpadRequest) { - return updateWorkpad(request) - .then(() => ({ ok: true })) - .catch(formatResponse); - }, - }); - - // delete workpad - route({ - method: 'DELETE', - path: `${routePrefix}/{id}`, - handler(request: WorkpadRequest) { - return deleteWorkpad(request) - .then(() => ({ ok: true })) - .catch(formatResponse); - }, - }); - - // find workpads - route({ - method: 'GET', - path: `${routePrefix}/find`, - handler(request: FindWorkpadRequest) { - return findWorkpad(request) - .then(formatResponse) - .then(resp => { - return { - total: resp.total, - workpads: resp.saved_objects.map(hit => ({ id: hit.id, ...hit.attributes })), - }; - }) - .catch(() => { - return { - total: 0, - workpads: [], - }; - }); - }, - }); -} diff --git a/x-pack/plugins/canvas/kibana.json b/x-pack/plugins/canvas/kibana.json new file mode 100644 index 00000000000000..87214f0287054c --- /dev/null +++ b/x-pack/plugins/canvas/kibana.json @@ -0,0 +1,10 @@ +{ + "id": "canvas", + "version": "8.0.0", + "kibanaVersion": "kibana", + "configPath": ["xpack", "canvas"], + "server": true, + "ui": false, + "requiredPlugins": [] + } + \ No newline at end of file diff --git a/x-pack/plugins/canvas/server/index.ts b/x-pack/plugins/canvas/server/index.ts new file mode 100644 index 00000000000000..e881f7db69c785 --- /dev/null +++ b/x-pack/plugins/canvas/server/index.ts @@ -0,0 +1,11 @@ +/* + * 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 { PluginInitializerContext } from 'src/core/server'; +import { CanvasPlugin } from './plugin'; + +export const plugin = (initializerContext: PluginInitializerContext) => + new CanvasPlugin(initializerContext); diff --git a/x-pack/plugins/canvas/server/plugin.ts b/x-pack/plugins/canvas/server/plugin.ts new file mode 100644 index 00000000000000..76b86c2ac39b43 --- /dev/null +++ b/x-pack/plugins/canvas/server/plugin.ts @@ -0,0 +1,25 @@ +/* + * 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 { CoreSetup, PluginInitializerContext, Plugin, Logger } from 'src/core/server'; +import { initRoutes } from './routes'; + +export class CanvasPlugin implements Plugin { + private readonly logger: Logger; + constructor(initializerContext: PluginInitializerContext) { + this.logger = initializerContext.logger.get(); + } + + public setup(coreSetup: CoreSetup): void { + const canvasRouter = coreSetup.http.createRouter(); + + initRoutes({ router: canvasRouter, logger: this.logger }); + } + + public start() {} + + public stop() {} +} diff --git a/x-pack/plugins/canvas/server/routes/catch_error_handler.ts b/x-pack/plugins/canvas/server/routes/catch_error_handler.ts new file mode 100644 index 00000000000000..fb7f4d6ee26006 --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/catch_error_handler.ts @@ -0,0 +1,30 @@ +/* + * 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 { ObjectType } from '@kbn/config-schema'; +import { RequestHandler } from 'src/core/server'; + +export const catchErrorHandler: < + P extends ObjectType, + Q extends ObjectType, + B extends ObjectType +>( + fn: RequestHandler +) => RequestHandler = fn => { + return async (context, request, response) => { + try { + return await fn(context, request, response); + } catch (error) { + if (error.isBoom) { + return response.customError({ + body: error.output.payload, + statusCode: error.output.statusCode, + }); + } + return response.internalError({ body: error }); + } + }; +}; diff --git a/x-pack/plugins/canvas/server/routes/index.ts b/x-pack/plugins/canvas/server/routes/index.ts new file mode 100644 index 00000000000000..46873a6b325423 --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/index.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 { IRouter, Logger } from 'src/core/server'; +import { initWorkpadRoutes } from './workpad'; + +export interface RouteInitializerDeps { + router: IRouter; + logger: Logger; +} + +export function initRoutes(deps: RouteInitializerDeps) { + initWorkpadRoutes(deps); +} diff --git a/x-pack/plugins/canvas/server/routes/workpad/create.test.ts b/x-pack/plugins/canvas/server/routes/workpad/create.test.ts new file mode 100644 index 00000000000000..dbad1a97dc4588 --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/workpad/create.test.ts @@ -0,0 +1,102 @@ +/* + * 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 sinon from 'sinon'; +import { + savedObjectsClientMock, + httpServiceMock, + httpServerMock, + loggingServiceMock, +} from 'src/core/server/mocks'; +import { CANVAS_TYPE } from '../../../../../legacy/plugins/canvas/common/lib/constants'; +import { initializeCreateWorkpadRoute } from './create'; +import { + IRouter, + kibanaResponseFactory, + RequestHandlerContext, + RequestHandler, +} from 'src/core/server'; + +const mockRouteContext = ({ + core: { + savedObjects: { + client: savedObjectsClientMock.create(), + }, + }, +} as unknown) as RequestHandlerContext; + +const mockedUUID = '123abc'; +const now = new Date(); +const nowIso = now.toISOString(); + +jest.mock('uuid/v4', () => jest.fn().mockReturnValue('123abc')); + +describe('POST workpad', () => { + let routeHandler: RequestHandler; + let clock: sinon.SinonFakeTimers; + + beforeEach(() => { + clock = sinon.useFakeTimers(now); + + const httpService = httpServiceMock.createSetupContract(); + + const router = httpService.createRouter('') as jest.Mocked; + initializeCreateWorkpadRoute({ + router, + logger: loggingServiceMock.create().get(), + }); + + routeHandler = router.post.mock.calls[0][1]; + }); + + afterEach(() => { + clock.restore(); + }); + + it(`returns 200 when the workpad is created`, async () => { + const mockWorkpad = { + pages: [], + }; + + const request = httpServerMock.createKibanaRequest({ + method: 'post', + path: 'api/canvas/workpad', + body: mockWorkpad, + }); + + const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory); + + expect(response.status).toBe(200); + expect(response.payload).toEqual({ ok: true }); + expect(mockRouteContext.core.savedObjects.client.create).toBeCalledWith( + CANVAS_TYPE, + { + ...mockWorkpad, + '@timestamp': nowIso, + '@created': nowIso, + }, + { + id: `workpad-${mockedUUID}`, + } + ); + }); + + it(`returns bad request if create is unsuccessful`, async () => { + const request = httpServerMock.createKibanaRequest({ + method: 'post', + path: 'api/canvas/workpad', + body: {}, + }); + + (mockRouteContext.core.savedObjects.client.create as jest.Mock).mockImplementation(() => { + throw mockRouteContext.core.savedObjects.client.errors.createBadRequestError('bad request'); + }); + + const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory); + + expect(response.status).toBe(400); + }); +}); diff --git a/x-pack/plugins/canvas/server/routes/workpad/create.ts b/x-pack/plugins/canvas/server/routes/workpad/create.ts new file mode 100644 index 00000000000000..be904356720b68 --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/workpad/create.ts @@ -0,0 +1,57 @@ +/* + * 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 { RouteInitializerDeps } from '../'; +import { + CANVAS_TYPE, + API_ROUTE_WORKPAD, +} from '../../../../../legacy/plugins/canvas/common/lib/constants'; +import { CanvasWorkpad } from '../../../../../legacy/plugins/canvas/types'; +import { getId } from '../../../../../legacy/plugins/canvas/public/lib/get_id'; +import { WorkpadSchema } from './workpad_schema'; +import { okResponse } from './ok_response'; +import { catchErrorHandler } from '../catch_error_handler'; + +export type WorkpadAttributes = Pick> & { + '@timestamp': string; + '@created': string; +}; + +export function initializeCreateWorkpadRoute(deps: RouteInitializerDeps) { + const { router } = deps; + router.post( + { + path: `${API_ROUTE_WORKPAD}`, + validate: { + body: WorkpadSchema, + }, + }, + catchErrorHandler(async (context, request, response) => { + if (!request.body) { + return response.badRequest({ body: 'A workpad payload is required' }); + } + + const workpad = request.body as CanvasWorkpad; + + const now = new Date().toISOString(); + const { id, ...payload } = workpad; + + await context.core.savedObjects.client.create( + CANVAS_TYPE, + { + ...payload, + '@timestamp': now, + '@created': now, + }, + { id: id || getId('workpad') } + ); + + return response.ok({ + body: okResponse, + }); + }) + ); +} diff --git a/x-pack/plugins/canvas/server/routes/workpad/delete.test.ts b/x-pack/plugins/canvas/server/routes/workpad/delete.test.ts new file mode 100644 index 00000000000000..e693840826b7ab --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/workpad/delete.test.ts @@ -0,0 +1,78 @@ +/* + * 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 { CANVAS_TYPE } from '../../../../../legacy/plugins/canvas/common/lib/constants'; +import { initializeDeleteWorkpadRoute } from './delete'; +import { + IRouter, + kibanaResponseFactory, + RequestHandlerContext, + RequestHandler, +} from 'src/core/server'; +import { + savedObjectsClientMock, + httpServiceMock, + httpServerMock, + loggingServiceMock, +} from 'src/core/server/mocks'; + +const mockRouteContext = ({ + core: { + savedObjects: { + client: savedObjectsClientMock.create(), + }, + }, +} as unknown) as RequestHandlerContext; + +describe('DELETE workpad', () => { + let routeHandler: RequestHandler; + + beforeEach(() => { + const httpService = httpServiceMock.createSetupContract(); + const router = httpService.createRouter('') as jest.Mocked; + initializeDeleteWorkpadRoute({ + router, + logger: loggingServiceMock.create().get(), + }); + + routeHandler = router.delete.mock.calls[0][1]; + }); + + it(`returns 200 ok when the workpad is deleted`, async () => { + const id = 'some-id'; + const request = httpServerMock.createKibanaRequest({ + method: 'delete', + path: `api/canvas/workpad/${id}`, + params: { + id, + }, + }); + + const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory); + + expect(response.status).toBe(200); + expect(response.payload).toEqual({ ok: true }); + expect(mockRouteContext.core.savedObjects.client.delete).toBeCalledWith(CANVAS_TYPE, id); + }); + + it(`returns bad request if delete is unsuccessful`, async () => { + const request = httpServerMock.createKibanaRequest({ + method: 'delete', + path: `api/canvas/workpad/some-id`, + params: { + id: 'some-id', + }, + }); + + (mockRouteContext.core.savedObjects.client.delete as jest.Mock).mockImplementationOnce(() => { + throw mockRouteContext.core.savedObjects.client.errors.createBadRequestError('bad request'); + }); + + const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory); + + expect(response.status).toBe(400); + }); +}); diff --git a/x-pack/plugins/canvas/server/routes/workpad/delete.ts b/x-pack/plugins/canvas/server/routes/workpad/delete.ts new file mode 100644 index 00000000000000..7adf11e7a887be --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/workpad/delete.ts @@ -0,0 +1,32 @@ +/* + * 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'; +import { RouteInitializerDeps } from '../'; +import { + CANVAS_TYPE, + API_ROUTE_WORKPAD, +} from '../../../../../legacy/plugins/canvas/common/lib/constants'; +import { okResponse } from './ok_response'; +import { catchErrorHandler } from '../catch_error_handler'; + +export function initializeDeleteWorkpadRoute(deps: RouteInitializerDeps) { + const { router } = deps; + router.delete( + { + path: `${API_ROUTE_WORKPAD}/{id}`, + validate: { + params: schema.object({ + id: schema.string(), + }), + }, + }, + catchErrorHandler(async (context, request, response) => { + context.core.savedObjects.client.delete(CANVAS_TYPE, request.params.id); + return response.ok({ body: okResponse }); + }) + ); +} diff --git a/x-pack/plugins/canvas/server/routes/workpad/find.test.ts b/x-pack/plugins/canvas/server/routes/workpad/find.test.ts new file mode 100644 index 00000000000000..08de9b20e98185 --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/workpad/find.test.ts @@ -0,0 +1,113 @@ +/* + * 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 { initializeFindWorkpadsRoute } from './find'; +import { + IRouter, + kibanaResponseFactory, + RequestHandlerContext, + RequestHandler, +} from 'src/core/server'; +import { + savedObjectsClientMock, + httpServiceMock, + httpServerMock, + loggingServiceMock, +} from 'src/core/server/mocks'; + +const mockRouteContext = ({ + core: { + savedObjects: { + client: savedObjectsClientMock.create(), + }, + }, +} as unknown) as RequestHandlerContext; + +describe('Find workpad', () => { + let routeHandler: RequestHandler; + + beforeEach(() => { + const httpService = httpServiceMock.createSetupContract(); + const router = httpService.createRouter('') as jest.Mocked; + initializeFindWorkpadsRoute({ + router, + logger: loggingServiceMock.create().get(), + }); + + routeHandler = router.get.mock.calls[0][1]; + }); + + it(`returns 200 with the found workpads`, async () => { + const name = 'something'; + const perPage = 10000; + const mockResults = { + total: 2, + saved_objects: [ + { id: 1, attributes: { key: 'value' } }, + { id: 2, attributes: { key: 'other-value' } }, + ], + }; + + const findMock = mockRouteContext.core.savedObjects.client.find as jest.Mock; + + findMock.mockResolvedValueOnce(mockResults); + + const request = httpServerMock.createKibanaRequest({ + method: 'get', + path: `api/canvas/workpad/find`, + query: { + name, + perPage, + }, + }); + + const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory); + expect(response.status).toBe(200); + expect(findMock.mock.calls[0][0].search).toBe(`${name}* | ${name}`); + expect(findMock.mock.calls[0][0].perPage).toBe(perPage); + + expect(response.payload).toMatchInlineSnapshot(` + Object { + "total": 2, + "workpads": Array [ + Object { + "id": 1, + "key": "value", + }, + Object { + "id": 2, + "key": "other-value", + }, + ], + } + `); + }); + + it(`returns 200 with empty results on error`, async () => { + (mockRouteContext.core.savedObjects.client.find as jest.Mock).mockImplementationOnce(() => { + throw new Error('generic error'); + }); + + const request = httpServerMock.createKibanaRequest({ + method: 'get', + path: `api/canvas/workpad/find`, + query: { + name: 'something', + perPage: 1000, + }, + }); + + const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory); + + expect(response.status).toBe(200); + expect(response.payload).toMatchInlineSnapshot(` + Object { + "total": 0, + "workpads": Array [], + } + `); + }); +}); diff --git a/x-pack/plugins/canvas/server/routes/workpad/find.ts b/x-pack/plugins/canvas/server/routes/workpad/find.ts new file mode 100644 index 00000000000000..a528a756116093 --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/workpad/find.ts @@ -0,0 +1,60 @@ +/* + * 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'; +import { SavedObjectAttributes } from 'src/core/server'; +import { RouteInitializerDeps } from '../'; +import { + CANVAS_TYPE, + API_ROUTE_WORKPAD, +} from '../../../../../legacy/plugins/canvas/common/lib/constants'; + +export function initializeFindWorkpadsRoute(deps: RouteInitializerDeps) { + const { router } = deps; + router.get( + { + path: `${API_ROUTE_WORKPAD}/find`, + validate: { + query: schema.object({ + name: schema.string(), + page: schema.maybe(schema.number()), + perPage: schema.number(), + }), + }, + }, + async (context, request, response) => { + const savedObjectsClient = context.core.savedObjects.client; + const { name, page, perPage } = request.query; + + try { + const workpads = await savedObjectsClient.find({ + type: CANVAS_TYPE, + sortField: '@timestamp', + sortOrder: 'desc', + search: name ? `${name}* | ${name}` : '*', + searchFields: ['name'], + fields: ['id', 'name', '@created', '@timestamp'], + page, + perPage, + }); + + return response.ok({ + body: { + total: workpads.total, + workpads: workpads.saved_objects.map(hit => ({ id: hit.id, ...hit.attributes })), + }, + }); + } catch (error) { + return response.ok({ + body: { + total: 0, + workpads: [], + }, + }); + } + } + ); +} diff --git a/x-pack/plugins/canvas/server/routes/workpad/get.test.ts b/x-pack/plugins/canvas/server/routes/workpad/get.test.ts new file mode 100644 index 00000000000000..a31293f572c75f --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/workpad/get.test.ts @@ -0,0 +1,140 @@ +/* + * 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 { CANVAS_TYPE } from '../../../../../legacy/plugins/canvas/common/lib/constants'; +import { initializeGetWorkpadRoute } from './get'; +import { + IRouter, + kibanaResponseFactory, + RequestHandlerContext, + RequestHandler, +} from 'src/core/server'; +import { + savedObjectsClientMock, + httpServiceMock, + httpServerMock, + loggingServiceMock, +} from 'src/core/server/mocks'; +import { workpadWithGroupAsElement } from '../../../../../legacy/plugins/canvas/__tests__/fixtures/workpads'; +import { CanvasWorkpad } from '../../../../../legacy/plugins/canvas/types'; + +const mockRouteContext = ({ + core: { + savedObjects: { + client: savedObjectsClientMock.create(), + }, + }, +} as unknown) as RequestHandlerContext; + +describe('GET workpad', () => { + let routeHandler: RequestHandler; + + beforeEach(() => { + const httpService = httpServiceMock.createSetupContract(); + const router = httpService.createRouter('') as jest.Mocked; + initializeGetWorkpadRoute({ + router, + logger: loggingServiceMock.create().get(), + }); + + routeHandler = router.get.mock.calls[0][1]; + }); + + it(`returns 200 when the workpad is found`, async () => { + const request = httpServerMock.createKibanaRequest({ + method: 'get', + path: 'api/canvas/workpad/123', + params: { + id: '123', + }, + }); + + const savedObjectsClient = savedObjectsClientMock.create(); + savedObjectsClient.get.mockResolvedValueOnce({ + id: '123', + type: CANVAS_TYPE, + attributes: { foo: true }, + references: [], + }); + + mockRouteContext.core.savedObjects.client = savedObjectsClient; + + const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory); + + expect(response.status).toBe(200); + expect(response.payload).toMatchInlineSnapshot(` + Object { + "foo": true, + "id": "123", + } + `); + + expect(savedObjectsClient.get.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "canvas-workpad", + "123", + ], + ] + `); + }); + + it('corrects elements that should be groups', async () => { + const request = httpServerMock.createKibanaRequest({ + method: 'get', + path: 'api/canvas/workpad/123', + params: { + id: '123', + }, + }); + + const savedObjectsClient = savedObjectsClientMock.create(); + savedObjectsClient.get.mockResolvedValueOnce({ + id: '123', + type: CANVAS_TYPE, + attributes: workpadWithGroupAsElement as any, + references: [], + }); + + mockRouteContext.core.savedObjects.client = savedObjectsClient; + + const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory); + const workpad = response.payload as CanvasWorkpad; + + expect(response.status).toBe(200); + expect(workpad).not.toBeUndefined(); + + expect(workpad.pages[0].elements.length).toBe(1); + expect(workpad.pages[0].groups.length).toBe(1); + }); + + it('returns 404 if the workpad is not found', async () => { + const id = '123'; + const request = httpServerMock.createKibanaRequest({ + method: 'get', + path: 'api/canvas/workpad/123', + params: { + id, + }, + }); + + const savedObjectsClient = savedObjectsClientMock.create(); + savedObjectsClient.get.mockImplementation(() => { + throw savedObjectsClient.errors.createGenericNotFoundError(CANVAS_TYPE, id); + }); + mockRouteContext.core.savedObjects.client = savedObjectsClient; + + const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory); + + expect(response.payload).toMatchInlineSnapshot(` + Object { + "error": "Not Found", + "message": "Saved object [canvas-workpad/123] not found", + "statusCode": 404, + } + `); + }); +}); diff --git a/x-pack/plugins/canvas/server/routes/workpad/get.ts b/x-pack/plugins/canvas/server/routes/workpad/get.ts new file mode 100644 index 00000000000000..7a51006aa9f024 --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/workpad/get.ts @@ -0,0 +1,65 @@ +/* + * 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'; +import { RouteInitializerDeps } from '../'; +import { + CANVAS_TYPE, + API_ROUTE_WORKPAD, +} from '../../../../../legacy/plugins/canvas/common/lib/constants'; +import { CanvasWorkpad } from '../../../../../legacy/plugins/canvas/types'; +import { catchErrorHandler } from '../catch_error_handler'; + +export type WorkpadAttributes = Pick> & { + '@timestamp': string; + '@created': string; +}; + +export function initializeGetWorkpadRoute(deps: RouteInitializerDeps) { + const { router } = deps; + router.get( + { + path: `${API_ROUTE_WORKPAD}/{id}`, + validate: { + params: schema.object({ + id: schema.string(), + }), + }, + }, + catchErrorHandler(async (context, request, response) => { + const workpad = await context.core.savedObjects.client.get( + CANVAS_TYPE, + request.params.id + ); + + if ( + // not sure if we need to be this defensive + workpad.type === 'canvas-workpad' && + workpad.attributes && + workpad.attributes.pages && + workpad.attributes.pages.length + ) { + workpad.attributes.pages.forEach(page => { + const elements = (page.elements || []).filter( + ({ id: pageId }) => !pageId.startsWith('group') + ); + const groups = (page.groups || []).concat( + (page.elements || []).filter(({ id: pageId }) => pageId.startsWith('group')) + ); + page.elements = elements; + page.groups = groups; + }); + } + + return response.ok({ + body: { + id: workpad.id, + ...workpad.attributes, + }, + }); + }) + ); +} diff --git a/x-pack/plugins/canvas/server/routes/workpad/index.ts b/x-pack/plugins/canvas/server/routes/workpad/index.ts new file mode 100644 index 00000000000000..8a61b30be54149 --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/workpad/index.ts @@ -0,0 +1,21 @@ +/* + * 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 { RouteInitializerDeps } from '../'; +import { initializeFindWorkpadsRoute } from './find'; +import { initializeGetWorkpadRoute } from './get'; +import { initializeCreateWorkpadRoute } from './create'; +import { initializeUpdateWorkpadRoute, initializeUpdateWorkpadAssetsRoute } from './update'; +import { initializeDeleteWorkpadRoute } from './delete'; + +export function initWorkpadRoutes(deps: RouteInitializerDeps) { + initializeFindWorkpadsRoute(deps); + initializeGetWorkpadRoute(deps); + initializeCreateWorkpadRoute(deps); + initializeUpdateWorkpadRoute(deps); + initializeUpdateWorkpadAssetsRoute(deps); + initializeDeleteWorkpadRoute(deps); +} diff --git a/x-pack/plugins/canvas/server/routes/workpad/ok_response.ts b/x-pack/plugins/canvas/server/routes/workpad/ok_response.ts new file mode 100644 index 00000000000000..43d545a5183fed --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/workpad/ok_response.ts @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +export const okResponse = { + ok: true, +}; diff --git a/x-pack/plugins/canvas/server/routes/workpad/update.test.ts b/x-pack/plugins/canvas/server/routes/workpad/update.test.ts new file mode 100644 index 00000000000000..492a6c98d71ee3 --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/workpad/update.test.ts @@ -0,0 +1,223 @@ +/* + * 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 sinon from 'sinon'; +import { CANVAS_TYPE } from '../../../../../legacy/plugins/canvas/common/lib/constants'; +import { initializeUpdateWorkpadRoute, initializeUpdateWorkpadAssetsRoute } from './update'; +import { + IRouter, + kibanaResponseFactory, + RequestHandlerContext, + RequestHandler, +} from 'src/core/server'; +import { + savedObjectsClientMock, + httpServiceMock, + httpServerMock, + loggingServiceMock, +} from 'src/core/server/mocks'; +import { workpads } from '../../../../../legacy/plugins/canvas/__tests__/fixtures/workpads'; +import { okResponse } from './ok_response'; + +const mockRouteContext = ({ + core: { + savedObjects: { + client: savedObjectsClientMock.create(), + }, + }, +} as unknown) as RequestHandlerContext; + +const workpad = workpads[0]; +const now = new Date(); +const nowIso = now.toISOString(); + +jest.mock('uuid/v4', () => jest.fn().mockReturnValue('123abc')); + +describe('PUT workpad', () => { + let routeHandler: RequestHandler; + let clock: sinon.SinonFakeTimers; + + beforeEach(() => { + clock = sinon.useFakeTimers(now); + + const httpService = httpServiceMock.createSetupContract(); + const router = httpService.createRouter('') as jest.Mocked; + initializeUpdateWorkpadRoute({ + router, + logger: loggingServiceMock.create().get(), + }); + + routeHandler = router.put.mock.calls[0][1]; + }); + + afterEach(() => { + jest.resetAllMocks(); + clock.restore(); + }); + + it(`returns 200 ok when the workpad is updated`, async () => { + const updatedWorkpad = { name: 'new name' }; + const { id, ...workpadAttributes } = workpad; + + const request = httpServerMock.createKibanaRequest({ + method: 'put', + path: `api/canvas/workpad/${id}`, + params: { + id, + }, + body: updatedWorkpad, + }); + + const savedObjectsClient = savedObjectsClientMock.create(); + savedObjectsClient.get.mockResolvedValueOnce({ + id, + type: CANVAS_TYPE, + attributes: workpadAttributes as any, + references: [], + }); + + mockRouteContext.core.savedObjects.client = savedObjectsClient; + + const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory); + + expect(response.status).toBe(200); + expect(response.payload).toEqual(okResponse); + expect(mockRouteContext.core.savedObjects.client.create).toBeCalledWith( + CANVAS_TYPE, + { + ...workpadAttributes, + ...updatedWorkpad, + '@timestamp': nowIso, + '@created': workpad['@created'], + }, + { + overwrite: true, + id, + } + ); + }); + + it(`returns not found if existing workpad is not found`, async () => { + const request = httpServerMock.createKibanaRequest({ + method: 'put', + path: 'api/canvas/workpad/some-id', + params: { + id: 'not-found', + }, + body: {}, + }); + + (mockRouteContext.core.savedObjects.client.get as jest.Mock).mockImplementationOnce(() => { + throw mockRouteContext.core.savedObjects.client.errors.createGenericNotFoundError( + 'not found' + ); + }); + + const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory); + + expect(response.status).toBe(404); + }); + + it(`returns bad request if the write fails`, async () => { + const request = httpServerMock.createKibanaRequest({ + method: 'put', + path: 'api/canvas/workpad/some-id', + params: { + id: 'some-id', + }, + body: {}, + }); + + const savedObjectsClient = savedObjectsClientMock.create(); + savedObjectsClient.get.mockResolvedValueOnce({ + id: 'some-id', + type: CANVAS_TYPE, + attributes: {}, + references: [], + }); + + mockRouteContext.core.savedObjects.client = savedObjectsClient; + + (mockRouteContext.core.savedObjects.client.create as jest.Mock).mockImplementationOnce(() => { + throw mockRouteContext.core.savedObjects.client.errors.createBadRequestError('bad request'); + }); + + const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory); + + expect(response.status).toBe(400); + }); +}); + +describe('update assets', () => { + let routeHandler: RequestHandler; + let clock: sinon.SinonFakeTimers; + + beforeEach(() => { + clock = sinon.useFakeTimers(now); + const httpService = httpServiceMock.createSetupContract(); + const router = httpService.createRouter('') as jest.Mocked; + initializeUpdateWorkpadAssetsRoute({ + router, + logger: loggingServiceMock.create().get(), + }); + + routeHandler = router.put.mock.calls[0][1]; + }); + + afterEach(() => { + clock.restore(); + }); + + it('updates assets', async () => { + const { id, ...attributes } = workpad; + const assets = { + 'asset-1': { + '@created': new Date().toISOString(), + id: 'asset-1', + type: 'asset', + value: 'some-url-encoded-asset', + }, + 'asset-2': { + '@created': new Date().toISOString(), + id: 'asset-2', + type: 'asset', + value: 'some-other asset', + }, + }; + + const request = httpServerMock.createKibanaRequest({ + method: 'put', + path: 'api/canvas/workpad-assets/some-id', + params: { + id, + }, + body: assets, + }); + + (mockRouteContext.core.savedObjects.client.get as jest.Mock).mockResolvedValueOnce({ + id, + type: CANVAS_TYPE, + attributes: attributes as any, + references: [], + }); + + const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory); + expect(response.status).toBe(200); + + expect(mockRouteContext.core.savedObjects.client.create).toBeCalledWith( + CANVAS_TYPE, + { + ...attributes, + '@timestamp': nowIso, + assets, + }, + { + id, + overwrite: true, + } + ); + }); +}); diff --git a/x-pack/plugins/canvas/server/routes/workpad/update.ts b/x-pack/plugins/canvas/server/routes/workpad/update.ts new file mode 100644 index 00000000000000..460aa174038ae8 --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/workpad/update.ts @@ -0,0 +1,129 @@ +/* + * 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, TypeOf } from '@kbn/config-schema'; +import { omit } from 'lodash'; +import { KibanaResponseFactory } from 'src/core/server'; +import { SavedObjectsClientContract } from 'src/core/server'; +import { RouteInitializerDeps } from '../'; +import { + CANVAS_TYPE, + API_ROUTE_WORKPAD, + API_ROUTE_WORKPAD_STRUCTURES, + API_ROUTE_WORKPAD_ASSETS, +} from '../../../../../legacy/plugins/canvas/common/lib/constants'; +import { CanvasWorkpad } from '../../../../../legacy/plugins/canvas/types'; +import { WorkpadSchema, WorkpadAssetSchema } from './workpad_schema'; +import { okResponse } from './ok_response'; +import { catchErrorHandler } from '../catch_error_handler'; + +export type WorkpadAttributes = Pick> & { + '@timestamp': string; + '@created': string; +}; + +const AssetsRecordSchema = schema.recordOf(schema.string(), WorkpadAssetSchema); + +const AssetPayloadSchema = schema.object({ + assets: AssetsRecordSchema, +}); + +const workpadUpdateHandler = async ( + payload: TypeOf | TypeOf, + id: string, + savedObjectsClient: SavedObjectsClientContract, + response: KibanaResponseFactory +) => { + const now = new Date().toISOString(); + + const workpadObject = await savedObjectsClient.get(CANVAS_TYPE, id); + await savedObjectsClient.create( + CANVAS_TYPE, + { + ...workpadObject.attributes, + ...omit(payload, 'id'), // never write the id property + '@timestamp': now, // always update the modified time + '@created': workpadObject.attributes['@created'], // ensure created is not modified + }, + { overwrite: true, id } + ); + + return response.ok({ + body: okResponse, + }); +}; + +export function initializeUpdateWorkpadRoute(deps: RouteInitializerDeps) { + const { router } = deps; + // TODO: This route is likely deprecated and everything is using the workpad_structures + // path instead. Investigate further. + router.put( + { + path: `${API_ROUTE_WORKPAD}/{id}`, + validate: { + params: schema.object({ + id: schema.string(), + }), + body: WorkpadSchema, + }, + }, + catchErrorHandler(async (context, request, response) => { + return workpadUpdateHandler( + request.body, + request.params.id, + context.core.savedObjects.client, + response + ); + }) + ); + + router.put( + { + path: `${API_ROUTE_WORKPAD_STRUCTURES}/{id}`, + validate: { + params: schema.object({ + id: schema.string(), + }), + body: WorkpadSchema, + }, + }, + catchErrorHandler(async (context, request, response) => { + return workpadUpdateHandler( + request.body, + request.params.id, + context.core.savedObjects.client, + response + ); + }) + ); +} + +export function initializeUpdateWorkpadAssetsRoute(deps: RouteInitializerDeps) { + const { router } = deps; + + router.put( + { + path: `${API_ROUTE_WORKPAD_ASSETS}/{id}`, + validate: { + params: schema.object({ + id: schema.string(), + }), + // ToDo: Currently the validation must be a schema.object + // Because we don't know what keys the assets will have, we have to allow + // unknowns and then validate in the handler + body: schema.object({}, { allowUnknowns: true }), + }, + }, + async (context, request, response) => { + return workpadUpdateHandler( + { assets: AssetsRecordSchema.validate(request.body) }, + request.params.id, + context.core.savedObjects.client, + response + ); + } + ); +} diff --git a/x-pack/plugins/canvas/server/routes/workpad/workpad_schema.ts b/x-pack/plugins/canvas/server/routes/workpad/workpad_schema.ts new file mode 100644 index 00000000000000..0bcb161575901b --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/workpad/workpad_schema.ts @@ -0,0 +1,65 @@ +/* + * 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'; + +export const PositionSchema = schema.object({ + angle: schema.number(), + height: schema.number(), + left: schema.number(), + parent: schema.nullable(schema.string()), + top: schema.number(), + width: schema.number(), +}); + +export const WorkpadElementSchema = schema.object({ + expression: schema.string(), + filter: schema.maybe(schema.nullable(schema.string())), + id: schema.string(), + position: PositionSchema, +}); + +export const WorkpadPageSchema = schema.object({ + elements: schema.arrayOf(WorkpadElementSchema), + groups: schema.arrayOf( + schema.object({ + id: schema.string(), + position: PositionSchema, + }) + ), + id: schema.string(), + style: schema.recordOf(schema.string(), schema.string()), + transition: schema.maybe( + schema.oneOf([ + schema.object({}), + schema.object({ + name: schema.string(), + }), + ]) + ), +}); + +export const WorkpadAssetSchema = schema.object({ + '@created': schema.string(), + id: schema.string(), + type: schema.string(), + value: schema.string(), +}); + +export const WorkpadSchema = schema.object({ + '@created': schema.maybe(schema.string()), + '@timestamp': schema.maybe(schema.string()), + assets: schema.maybe(schema.recordOf(schema.string(), WorkpadAssetSchema)), + colors: schema.arrayOf(schema.string()), + css: schema.string(), + height: schema.number(), + id: schema.string(), + isWriteable: schema.maybe(schema.boolean()), + name: schema.string(), + page: schema.number(), + pages: schema.arrayOf(WorkpadPageSchema), + width: schema.number(), +}); From c41531122183749159a06641acef22ad5a230019 Mon Sep 17 00:00:00 2001 From: Alexey Antonov Date: Tue, 26 Nov 2019 23:28:04 +0300 Subject: [PATCH 11/12] Move @kbn/es-query into data plugin (#51014) --- .eslintignore | 2 +- .i18nrc.json | 1 - ...a-plugin-public.savedobjectsclient.find.md | 2 +- ...kibana-plugin-public.savedobjectsclient.md | 2 +- package.json | 1 - packages/kbn-es-query/README.md | 92 ---- packages/kbn-es-query/babel.config.js | 35 -- packages/kbn-es-query/index.d.ts | 20 - packages/kbn-es-query/package.json | 28 -- packages/kbn-es-query/scripts/build.js | 20 - .../src/__fixtures__/filter_skeleton.json | 5 - .../__fixtures__/index_pattern_response.json | 303 ------------- packages/kbn-es-query/src/index.d.ts | 20 - packages/kbn-es-query/src/index.js | 20 - .../src/kuery/ast/__tests__/ast.js | 415 ----------------- packages/kbn-es-query/src/kuery/ast/ast.d.ts | 50 --- .../kbn-es-query/src/kuery/ast/index.d.ts | 20 - .../functions/__tests__/geo_bounding_box.js | 120 ----- .../kuery/functions/__tests__/geo_polygon.js | 131 ------ .../src/kuery/functions/__tests__/is.js | 310 ------------- .../utils/get_full_field_name_node.js | 88 ---- .../kuery/node_types/__tests__/function.js | 80 ---- .../kuery/node_types/__tests__/named_arg.js | 62 --- .../kuery/node_types/__tests__/wildcard.js | 107 ----- .../__tests__/get_time_zone_from_settings.js | 36 -- packages/kbn-es-query/src/utils/filters.js | 133 ------ .../src/utils/get_time_zone_from_settings.js | 28 -- packages/kbn-es-query/src/utils/index.js | 20 - packages/kbn-es-query/tasks/build_cli.js | 103 ----- packages/kbn-es-query/tsconfig.browser.json | 11 - packages/kbn-es-query/tsconfig.json | 11 - src/core/public/public.api.md | 2 +- .../service/lib/filter_utils.test.ts | 22 +- .../saved_objects/service/lib/filter_utils.ts | 76 ++-- .../service/lib/repository.test.js | 3 +- .../saved_objects/service/lib/repository.ts | 8 +- .../service/lib/search_dsl/query_params.ts | 6 +- .../service/lib/search_dsl/search_dsl.ts | 4 +- .../components/query_bar_top_row.tsx | 7 +- .../public/vis/timelion_request_handler.ts | 2 +- .../public/components/vis_editor.js | 4 +- .../public/vega_request_handler.ts | 2 +- .../es_query/es_query/build_es_query.test.ts | 2 +- .../es_query/es_query/build_es_query.ts | 2 +- .../es_query/filter_matches_index.test.ts | 3 +- .../es_query/es_query/filter_matches_index.ts | 2 +- .../common/es_query/es_query/from_filters.ts | 2 +- .../es_query/es_query/from_kuery.test.ts | 4 +- .../common/es_query/es_query/from_kuery.ts | 30 +- .../es_query/es_query/migrate_filter.test.ts | 6 +- .../es_query/es_query/migrate_filter.ts | 2 +- src/plugins/data/common/es_query/index.ts | 3 +- .../es_query/kuery/ast/_generated_}/kuery.js | 0 .../common/es_query/kuery/ast/ast.test.ts | 421 ++++++++++++++++++ .../data/common/es_query/kuery/ast/ast.ts | 89 ++-- .../data/common/es_query/kuery/ast/index.ts | 0 .../data/common/es_query}/kuery/ast/kuery.peg | 0 .../common/es_query}/kuery/functions/and.js | 0 .../es_query/kuery/functions/and.test.ts | 54 ++- .../es_query}/kuery/functions/exists.js | 0 .../es_query/kuery/functions/exists.test.ts | 73 +-- .../kuery/functions/geo_bounding_box.js | 0 .../kuery/functions/geo_bounding_box.test.ts | 133 ++++++ .../es_query}/kuery/functions/geo_polygon.js | 0 .../kuery/functions/geo_polygon.test.ts | 143 ++++++ .../common/es_query}/kuery/functions/index.js | 0 .../common/es_query}/kuery/functions/is.js | 20 +- .../es_query/kuery/functions/is.test.ts | 305 +++++++++++++ .../es_query}/kuery/functions/nested.js | 0 .../es_query/kuery/functions/nested.test.ts | 56 ++- .../common/es_query}/kuery/functions/not.js | 0 .../es_query/kuery/functions/not.test.ts | 52 ++- .../common/es_query}/kuery/functions/or.js | 0 .../es_query/kuery/functions/or.test.ts | 63 +-- .../common/es_query}/kuery/functions/range.js | 2 +- .../es_query/kuery/functions/range.test.ts | 206 +++++---- .../kuery/functions/utils/get_fields.js | 0 .../kuery/functions/utils/get_fields.test.ts | 73 ++- .../utils/get_full_field_name_node.js | 0 .../utils/get_full_field_name_node.test.ts | 87 ++++ .../data/common/es_query/kuery/index.ts | 6 +- .../es_query/kuery/kuery_syntax_error.test.ts | 71 +-- .../es_query/kuery/kuery_syntax_error.ts | 57 ++- .../es_query}/kuery/node_types/function.js | 0 .../kuery/node_types/function.test.ts | 75 ++++ .../es_query}/kuery/node_types/index.d.ts | 44 +- .../es_query}/kuery/node_types/index.js | 0 .../es_query}/kuery/node_types/literal.js | 0 .../es_query/kuery/node_types/literal.test.ts | 35 +- .../es_query}/kuery/node_types/named_arg.js | 0 .../kuery/node_types/named_arg.test.ts | 57 +++ .../es_query}/kuery/node_types/wildcard.js | 0 .../kuery/node_types/wildcard.test.ts | 110 +++++ .../data/common/es_query/kuery/types.ts | 23 +- .../common/field_formats/converters/custom.ts | 4 +- .../data/common/field_formats/field_format.ts | 2 +- tasks/config/peg.js | 4 +- .../components/shared/KueryBar/index.tsx | 12 +- .../convert_ui_filters/get_ui_filters_es.ts | 14 +- .../public/lib/adapters/elasticsearch/rest.ts | 9 +- .../graph/public/components/search_bar.tsx | 9 +- .../components/metrics_explorer/kuery_bar.tsx | 4 +- .../store/local/log_filter/selectors.ts | 6 +- .../store/local/waffle_filter/selectors.ts | 5 +- .../plugins/infra/public/utils/kuery.ts | 6 +- .../public/autocomplete_providers/index.js | 4 +- .../components/kql_filter_bar/utils.js | 8 +- .../plugins/siem/public/lib/keury/index.ts | 12 +- .../components/step_define_rule/schema.tsx | 4 +- .../transform/public/app/lib/kibana/common.ts | 2 +- .../components/functional/kuery_bar/index.tsx | 17 +- .../plugins/uptime/public/pages/overview.tsx | 7 +- x-pack/package.json | 1 - .../translations/translations/ja-JP.json | 12 +- .../translations/translations/zh-CN.json | 12 +- 115 files changed, 2025 insertions(+), 2852 deletions(-) delete mode 100644 packages/kbn-es-query/README.md delete mode 100644 packages/kbn-es-query/babel.config.js delete mode 100644 packages/kbn-es-query/index.d.ts delete mode 100644 packages/kbn-es-query/package.json delete mode 100644 packages/kbn-es-query/scripts/build.js delete mode 100644 packages/kbn-es-query/src/__fixtures__/filter_skeleton.json delete mode 100644 packages/kbn-es-query/src/__fixtures__/index_pattern_response.json delete mode 100644 packages/kbn-es-query/src/index.d.ts delete mode 100644 packages/kbn-es-query/src/index.js delete mode 100644 packages/kbn-es-query/src/kuery/ast/__tests__/ast.js delete mode 100644 packages/kbn-es-query/src/kuery/ast/ast.d.ts delete mode 100644 packages/kbn-es-query/src/kuery/ast/index.d.ts delete mode 100644 packages/kbn-es-query/src/kuery/functions/__tests__/geo_bounding_box.js delete mode 100644 packages/kbn-es-query/src/kuery/functions/__tests__/geo_polygon.js delete mode 100644 packages/kbn-es-query/src/kuery/functions/__tests__/is.js delete mode 100644 packages/kbn-es-query/src/kuery/functions/__tests__/utils/get_full_field_name_node.js delete mode 100644 packages/kbn-es-query/src/kuery/node_types/__tests__/function.js delete mode 100644 packages/kbn-es-query/src/kuery/node_types/__tests__/named_arg.js delete mode 100644 packages/kbn-es-query/src/kuery/node_types/__tests__/wildcard.js delete mode 100644 packages/kbn-es-query/src/utils/__tests__/get_time_zone_from_settings.js delete mode 100644 packages/kbn-es-query/src/utils/filters.js delete mode 100644 packages/kbn-es-query/src/utils/get_time_zone_from_settings.js delete mode 100644 packages/kbn-es-query/src/utils/index.js delete mode 100644 packages/kbn-es-query/tasks/build_cli.js delete mode 100644 packages/kbn-es-query/tsconfig.browser.json delete mode 100644 packages/kbn-es-query/tsconfig.json rename {packages/kbn-es-query/src/kuery/ast => src/plugins/data/common/es_query/kuery/ast/_generated_}/kuery.js (100%) create mode 100644 src/plugins/data/common/es_query/kuery/ast/ast.test.ts rename packages/kbn-es-query/src/kuery/ast/ast.js => src/plugins/data/common/es_query/kuery/ast/ast.ts (53%) rename packages/kbn-es-query/src/kuery/ast/index.js => src/plugins/data/common/es_query/kuery/ast/index.ts (100%) rename {packages/kbn-es-query/src => src/plugins/data/common/es_query}/kuery/ast/kuery.peg (100%) rename {packages/kbn-es-query/src => src/plugins/data/common/es_query}/kuery/functions/and.js (100%) rename packages/kbn-es-query/src/kuery/functions/__tests__/and.js => src/plugins/data/common/es_query/kuery/functions/and.test.ts (50%) rename {packages/kbn-es-query/src => src/plugins/data/common/es_query}/kuery/functions/exists.js (100%) rename packages/kbn-es-query/src/kuery/functions/__tests__/exists.js => src/plugins/data/common/es_query/kuery/functions/exists.test.ts (51%) rename {packages/kbn-es-query/src => src/plugins/data/common/es_query}/kuery/functions/geo_bounding_box.js (100%) create mode 100644 src/plugins/data/common/es_query/kuery/functions/geo_bounding_box.test.ts rename {packages/kbn-es-query/src => src/plugins/data/common/es_query}/kuery/functions/geo_polygon.js (100%) create mode 100644 src/plugins/data/common/es_query/kuery/functions/geo_polygon.test.ts rename {packages/kbn-es-query/src => src/plugins/data/common/es_query}/kuery/functions/index.js (100%) rename {packages/kbn-es-query/src => src/plugins/data/common/es_query}/kuery/functions/is.js (95%) create mode 100644 src/plugins/data/common/es_query/kuery/functions/is.test.ts rename {packages/kbn-es-query/src => src/plugins/data/common/es_query}/kuery/functions/nested.js (100%) rename packages/kbn-es-query/src/kuery/functions/__tests__/nested.js => src/plugins/data/common/es_query/kuery/functions/nested.test.ts (54%) rename {packages/kbn-es-query/src => src/plugins/data/common/es_query}/kuery/functions/not.js (100%) rename packages/kbn-es-query/src/kuery/functions/__tests__/not.js => src/plugins/data/common/es_query/kuery/functions/not.test.ts (50%) rename {packages/kbn-es-query/src => src/plugins/data/common/es_query}/kuery/functions/or.js (100%) rename packages/kbn-es-query/src/kuery/functions/__tests__/or.js => src/plugins/data/common/es_query/kuery/functions/or.test.ts (52%) rename {packages/kbn-es-query/src => src/plugins/data/common/es_query}/kuery/functions/range.js (98%) rename packages/kbn-es-query/src/kuery/functions/__tests__/range.js => src/plugins/data/common/es_query/kuery/functions/range.test.ts (54%) rename {packages/kbn-es-query/src => src/plugins/data/common/es_query}/kuery/functions/utils/get_fields.js (100%) rename packages/kbn-es-query/src/kuery/functions/__tests__/utils/get_fields.js => src/plugins/data/common/es_query/kuery/functions/utils/get_fields.test.ts (52%) rename {packages/kbn-es-query/src => src/plugins/data/common/es_query}/kuery/functions/utils/get_full_field_name_node.js (100%) create mode 100644 src/plugins/data/common/es_query/kuery/functions/utils/get_full_field_name_node.test.ts rename packages/kbn-es-query/src/kuery/index.js => src/plugins/data/common/es_query/kuery/index.ts (91%) rename packages/kbn-es-query/src/kuery/errors/index.test.js => src/plugins/data/common/es_query/kuery/kuery_syntax_error.test.ts (66%) rename packages/kbn-es-query/src/kuery/errors/index.js => src/plugins/data/common/es_query/kuery/kuery_syntax_error.ts (55%) rename {packages/kbn-es-query/src => src/plugins/data/common/es_query}/kuery/node_types/function.js (100%) create mode 100644 src/plugins/data/common/es_query/kuery/node_types/function.test.ts rename {packages/kbn-es-query/src => src/plugins/data/common/es_query}/kuery/node_types/index.d.ts (72%) rename {packages/kbn-es-query/src => src/plugins/data/common/es_query}/kuery/node_types/index.js (100%) rename {packages/kbn-es-query/src => src/plugins/data/common/es_query}/kuery/node_types/literal.js (100%) rename packages/kbn-es-query/src/kuery/node_types/__tests__/literal.js => src/plugins/data/common/es_query/kuery/node_types/literal.test.ts (54%) rename {packages/kbn-es-query/src => src/plugins/data/common/es_query}/kuery/node_types/named_arg.js (100%) create mode 100644 src/plugins/data/common/es_query/kuery/node_types/named_arg.test.ts rename {packages/kbn-es-query/src => src/plugins/data/common/es_query}/kuery/node_types/wildcard.js (100%) create mode 100644 src/plugins/data/common/es_query/kuery/node_types/wildcard.test.ts rename packages/kbn-es-query/src/kuery/index.d.ts => src/plugins/data/common/es_query/kuery/types.ts (73%) diff --git a/.eslintignore b/.eslintignore index cf13fc28467d94..90155ca9cb681e 100644 --- a/.eslintignore +++ b/.eslintignore @@ -8,6 +8,7 @@ bower_components /plugins /built_assets /html_docs +/src/plugins/data/common/es_query/kuery/ast/_generated_/** /src/fixtures/vislib/mock_data /src/legacy/ui/public/angular-bootstrap /src/legacy/ui/public/flot-charts @@ -19,7 +20,6 @@ bower_components /src/core/lib/kbn_internal_native_observable /packages/*/target /packages/eslint-config-kibana -/packages/kbn-es-query/src/kuery/ast/kuery.js /packages/kbn-pm/dist /packages/kbn-plugin-generator/sao_template/template /packages/kbn-ui-framework/dist diff --git a/.i18nrc.json b/.i18nrc.json index 2cdf7d2b039c6c..e5ba6762da154d 100644 --- a/.i18nrc.json +++ b/.i18nrc.json @@ -16,7 +16,6 @@ "interpreter": "src/legacy/core_plugins/interpreter", "kbn": "src/legacy/core_plugins/kibana", "kbnDocViews": "src/legacy/core_plugins/kbn_doc_views", - "kbnESQuery": "packages/kbn-es-query", "kbnVislibVisTypes": "src/legacy/core_plugins/kbn_vislib_vis_types", "kibana_react": "src/legacy/core_plugins/kibana_react", "kibana-react": "src/plugins/kibana_react", diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsclient.find.md b/docs/development/core/public/kibana-plugin-public.savedobjectsclient.find.md index 866755e78648af..cecceb04240e60 100644 --- a/docs/development/core/public/kibana-plugin-public.savedobjectsclient.find.md +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsclient.find.md @@ -9,5 +9,5 @@ Search for objects Signature: ```typescript -find: (options: Pick) => Promise>; +find: (options: Pick) => Promise>; ``` diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsclient.md b/docs/development/core/public/kibana-plugin-public.savedobjectsclient.md index 50451b813a61c0..c4ceb47f66e1bb 100644 --- a/docs/development/core/public/kibana-plugin-public.savedobjectsclient.md +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsclient.md @@ -20,7 +20,7 @@ export declare class SavedObjectsClient | [bulkGet](./kibana-plugin-public.savedobjectsclient.bulkget.md) | | (objects?: {
id: string;
type: string;
}[]) => Promise<SavedObjectsBatchResponse<SavedObjectAttributes>> | Returns an array of objects by id | | [create](./kibana-plugin-public.savedobjectsclient.create.md) | | <T extends SavedObjectAttributes>(type: string, attributes: T, options?: SavedObjectsCreateOptions) => Promise<SimpleSavedObject<T>> | Persists an object | | [delete](./kibana-plugin-public.savedobjectsclient.delete.md) | | (type: string, id: string) => Promise<{}> | Deletes an object | -| [find](./kibana-plugin-public.savedobjectsclient.find.md) | | <T extends SavedObjectAttributes>(options: Pick<SavedObjectFindOptionsServer, "search" | "filter" | "type" | "searchFields" | "defaultSearchOperator" | "hasReference" | "sortField" | "page" | "perPage" | "fields">) => Promise<SavedObjectsFindResponsePublic<T>> | Search for objects | +| [find](./kibana-plugin-public.savedobjectsclient.find.md) | | <T extends SavedObjectAttributes>(options: Pick<SavedObjectFindOptionsServer, "search" | "filter" | "type" | "fields" | "searchFields" | "defaultSearchOperator" | "hasReference" | "sortField" | "page" | "perPage">) => Promise<SavedObjectsFindResponsePublic<T>> | Search for objects | | [get](./kibana-plugin-public.savedobjectsclient.get.md) | | <T extends SavedObjectAttributes>(type: string, id: string) => Promise<SimpleSavedObject<T>> | Fetches a single object | ## Methods diff --git a/package.json b/package.json index ce430891052689..2c8d4ad4307b16 100644 --- a/package.json +++ b/package.json @@ -121,7 +121,6 @@ "@kbn/babel-code-parser": "1.0.0", "@kbn/babel-preset": "1.0.0", "@kbn/config-schema": "1.0.0", - "@kbn/es-query": "1.0.0", "@kbn/i18n": "1.0.0", "@kbn/interpreter": "1.0.0", "@kbn/pm": "1.0.0", diff --git a/packages/kbn-es-query/README.md b/packages/kbn-es-query/README.md deleted file mode 100644 index fc403447877d81..00000000000000 --- a/packages/kbn-es-query/README.md +++ /dev/null @@ -1,92 +0,0 @@ -# kbn-es-query - -This module is responsible for generating Elasticsearch queries for Kibana. See explanations below for each of the subdirectories. - -## es_query - -This folder contains the code that combines Lucene/KQL queries and filters into an Elasticsearch query. - -```javascript -buildEsQuery(indexPattern, queries, filters, config) -``` - -Generates the Elasticsearch query DSL from combining the queries and filters provided. - -```javascript -buildQueryFromFilters(filters, indexPattern) -``` - -Generates the Elasticsearch query DSL from the given filters. - -```javascript -luceneStringToDsl(query) -``` - -Generates the Elasticsearch query DSL from the given Lucene query. - -```javascript -migrateFilter(filter, indexPattern) -``` - -Migrates a filter from a previous version of Elasticsearch to the current version. - -```javascript -decorateQuery(query, queryStringOptions) -``` - -Decorates an Elasticsearch query_string query with the given options. - -## filters - -This folder contains the code related to Kibana Filter objects, including their definitions, and helper functions to create them. Filters in Kibana always contain a `meta` property which describes which `index` the filter corresponds to, as well as additional data about the specific filter. - -The object that is created by each of the following functions corresponds to a Filter object in the `lib` directory (e.g. `PhraseFilter`, `RangeFilter`, etc.) - -```javascript -buildExistsFilter(field, indexPattern) -``` - -Creates a filter (`ExistsFilter`) where the given field exists. - -```javascript -buildPhraseFilter(field, value, indexPattern) -``` - -Creates an filter (`PhraseFilter`) where the given field matches the given value. - -```javascript -buildPhrasesFilter(field, params, indexPattern) -``` - -Creates a filter (`PhrasesFilter`) where the given field matches one or more of the given values. `params` should be an array of values. - -```javascript -buildQueryFilter(query, index) -``` - -Creates a filter (`CustomFilter`) corresponding to a raw Elasticsearch query DSL object. - -```javascript -buildRangeFilter(field, params, indexPattern) -``` - -Creates a filter (`RangeFilter`) where the value for the given field is in the given range. `params` should contain `lt`, `lte`, `gt`, and/or `gte`. - -## kuery - -This folder contains the code corresponding to generating Elasticsearch queries using the Kibana query language. - -In general, you will only need to worry about the following functions from the `ast` folder: - -```javascript -fromExpression(expression) -``` - -Generates an abstract syntax tree corresponding to the raw Kibana query `expression`. - -```javascript -toElasticsearchQuery(node, indexPattern) -``` - -Takes an abstract syntax tree (generated from the previous method) and generates the Elasticsearch query DSL using the given `indexPattern`. Note that if no `indexPattern` is provided, then an Elasticsearch query DSL will still be generated, ignoring things like the index pattern scripted fields, field types, etc. - diff --git a/packages/kbn-es-query/babel.config.js b/packages/kbn-es-query/babel.config.js deleted file mode 100644 index 68783433fc711c..00000000000000 --- a/packages/kbn-es-query/babel.config.js +++ /dev/null @@ -1,35 +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. - */ - -// We can't use common Kibana presets here because of babel versions incompatibility -module.exports = { - env: { - public: { - presets: [ - '@kbn/babel-preset/webpack_preset' - ], - }, - server: { - presets: [ - '@kbn/babel-preset/node_preset' - ], - }, - }, - ignore: ['**/__tests__/**/*', '**/*.test.ts', '**/*.test.tsx'], -}; diff --git a/packages/kbn-es-query/index.d.ts b/packages/kbn-es-query/index.d.ts deleted file mode 100644 index 9bbd0a193dfed1..00000000000000 --- a/packages/kbn-es-query/index.d.ts +++ /dev/null @@ -1,20 +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 './src'; diff --git a/packages/kbn-es-query/package.json b/packages/kbn-es-query/package.json deleted file mode 100644 index 2cd2a8f53d2eed..00000000000000 --- a/packages/kbn-es-query/package.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "name": "@kbn/es-query", - "main": "target/server/index.js", - "browser": "target/public/index.js", - "version": "1.0.0", - "license": "Apache-2.0", - "private": true, - "scripts": { - "build": "node scripts/build", - "kbn:bootstrap": "node scripts/build --source-maps", - "kbn:watch": "node scripts/build --source-maps --watch" - }, - "dependencies": { - "lodash": "npm:@elastic/lodash@3.10.1-kibana3", - "moment-timezone": "^0.5.27", - "@kbn/i18n": "1.0.0" - }, - "devDependencies": { - "@babel/cli": "^7.5.5", - "@babel/core": "^7.5.5", - "@kbn/babel-preset": "1.0.0", - "@kbn/dev-utils": "1.0.0", - "@kbn/expect": "1.0.0", - "del": "^5.1.0", - "getopts": "^2.2.4", - "supports-color": "^7.0.0" - } -} diff --git a/packages/kbn-es-query/scripts/build.js b/packages/kbn-es-query/scripts/build.js deleted file mode 100644 index 6d53a8469b0e04..00000000000000 --- a/packages/kbn-es-query/scripts/build.js +++ /dev/null @@ -1,20 +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. - */ - -require('../tasks/build_cli'); diff --git a/packages/kbn-es-query/src/__fixtures__/filter_skeleton.json b/packages/kbn-es-query/src/__fixtures__/filter_skeleton.json deleted file mode 100644 index 1799d04a0fbd89..00000000000000 --- a/packages/kbn-es-query/src/__fixtures__/filter_skeleton.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "meta": { - "index": "logstash-*" - } -} diff --git a/packages/kbn-es-query/src/__fixtures__/index_pattern_response.json b/packages/kbn-es-query/src/__fixtures__/index_pattern_response.json deleted file mode 100644 index 588e6ada69cfe4..00000000000000 --- a/packages/kbn-es-query/src/__fixtures__/index_pattern_response.json +++ /dev/null @@ -1,303 +0,0 @@ -{ - "id": "logstash-*", - "title": "logstash-*", - "fields": [ - { - "name": "bytes", - "type": "number", - "esTypes": ["long"], - "count": 10, - "scripted": false, - "searchable": true, - "aggregatable": true, - "readFromDocValues": true - }, - { - "name": "ssl", - "type": "boolean", - "esTypes": ["boolean"], - "count": 20, - "scripted": false, - "searchable": true, - "aggregatable": true, - "readFromDocValues": true - }, - { - "name": "@timestamp", - "type": "date", - "esTypes": ["date"], - "count": 30, - "scripted": false, - "searchable": true, - "aggregatable": true, - "readFromDocValues": true - }, - { - "name": "time", - "type": "date", - "esTypes": ["date"], - "count": 30, - "scripted": false, - "searchable": true, - "aggregatable": true, - "readFromDocValues": true - }, - { - "name": "@tags", - "type": "string", - "esTypes": ["keyword"], - "count": 0, - "scripted": false, - "searchable": true, - "aggregatable": true, - "readFromDocValues": true - }, - { - "name": "utc_time", - "type": "date", - "esTypes": ["date"], - "count": 0, - "scripted": false, - "searchable": true, - "aggregatable": true, - "readFromDocValues": true - }, - { - "name": "phpmemory", - "type": "number", - "esTypes": ["integer"], - "count": 0, - "scripted": false, - "searchable": true, - "aggregatable": true, - "readFromDocValues": true - }, - { - "name": "ip", - "type": "ip", - "esTypes": ["ip"], - "count": 0, - "scripted": false, - "searchable": true, - "aggregatable": true, - "readFromDocValues": true - }, - { - "name": "request_body", - "type": "attachment", - "esTypes": ["attachment"], - "count": 0, - "scripted": false, - "searchable": true, - "aggregatable": true, - "readFromDocValues": true - }, - { - "name": "point", - "type": "geo_point", - "esTypes": ["geo_point"], - "count": 0, - "scripted": false, - "searchable": true, - "aggregatable": true, - "readFromDocValues": true - }, - { - "name": "area", - "type": "geo_shape", - "esTypes": ["geo_shape"], - "count": 0, - "scripted": false, - "searchable": true, - "aggregatable": true, - "readFromDocValues": true - }, - { - "name": "hashed", - "type": "murmur3", - "esTypes": ["murmur3"], - "count": 0, - "scripted": false, - "searchable": true, - "aggregatable": false, - "readFromDocValues": false - }, - { - "name": "geo.coordinates", - "type": "geo_point", - "esTypes": ["geo_point"], - "count": 0, - "scripted": false, - "searchable": true, - "aggregatable": true, - "readFromDocValues": true - }, - { - "name": "extension", - "type": "string", - "esTypes": ["keyword"], - "count": 0, - "scripted": false, - "searchable": true, - "aggregatable": true, - "readFromDocValues": true - }, - { - "name": "machine.os", - "type": "string", - "esTypes": ["text"], - "count": 0, - "scripted": false, - "searchable": true, - "aggregatable": true, - "readFromDocValues": false - }, - { - "name": "machine.os.raw", - "type": "string", - "esTypes": ["keyword"], - "count": 0, - "scripted": false, - "searchable": true, - "aggregatable": true, - "readFromDocValues": true, - "subType": { "multi": { "parent": "machine.os" } } - }, - { - "name": "geo.src", - "type": "string", - "esTypes": ["keyword"], - "count": 0, - "scripted": false, - "searchable": true, - "aggregatable": true, - "readFromDocValues": true - }, - { - "name": "_id", - "type": "string", - "esTypes": ["_id"], - "count": 0, - "scripted": false, - "searchable": true, - "aggregatable": true, - "readFromDocValues": false - }, - { - "name": "_type", - "type": "string", - "esTypes": ["_type"], - "count": 0, - "scripted": false, - "searchable": true, - "aggregatable": true, - "readFromDocValues": false - }, - { - "name": "_source", - "type": "_source", - "esTypes": ["_source"], - "count": 0, - "scripted": false, - "searchable": true, - "aggregatable": true, - "readFromDocValues": false - }, - { - "name": "non-filterable", - "type": "string", - "esTypes": ["text"], - "count": 0, - "scripted": false, - "searchable": false, - "aggregatable": true, - "readFromDocValues": false - }, - { - "name": "non-sortable", - "type": "string", - "esTypes": ["text"], - "count": 0, - "scripted": false, - "searchable": false, - "aggregatable": false, - "readFromDocValues": false - }, - { - "name": "custom_user_field", - "type": "conflict", - "esTypes": ["long", "text"], - "count": 0, - "scripted": false, - "searchable": true, - "aggregatable": true, - "readFromDocValues": true - }, - { - "name": "script string", - "type": "string", - "count": 0, - "scripted": true, - "script": "'i am a string'", - "lang": "expression", - "searchable": true, - "aggregatable": true, - "readFromDocValues": false - }, - { - "name": "script number", - "type": "number", - "count": 0, - "scripted": true, - "script": "1234", - "lang": "expression", - "searchable": true, - "aggregatable": true, - "readFromDocValues": false - }, - { - "name": "script date", - "type": "date", - "count": 0, - "scripted": true, - "script": "1234", - "lang": "painless", - "searchable": true, - "aggregatable": true, - "readFromDocValues": false - }, - { - "name": "script murmur3", - "type": "murmur3", - "count": 0, - "scripted": true, - "script": "1234", - "lang": "expression", - "searchable": true, - "aggregatable": true, - "readFromDocValues": false - }, - { - "name": "nestedField.child", - "type": "string", - "esTypes": ["text"], - "count": 0, - "scripted": false, - "searchable": true, - "aggregatable": false, - "readFromDocValues": false, - "subType": { "nested": { "path": "nestedField" } } - }, - { - "name": "nestedField.nestedChild.doublyNestedChild", - "type": "string", - "esTypes": ["text"], - "count": 0, - "scripted": false, - "searchable": true, - "aggregatable": false, - "readFromDocValues": false, - "subType": { "nested": { "path": "nestedField.nestedChild" } } - } - ] -} diff --git a/packages/kbn-es-query/src/index.d.ts b/packages/kbn-es-query/src/index.d.ts deleted file mode 100644 index 79e6903b186448..00000000000000 --- a/packages/kbn-es-query/src/index.d.ts +++ /dev/null @@ -1,20 +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 './kuery'; diff --git a/packages/kbn-es-query/src/index.js b/packages/kbn-es-query/src/index.js deleted file mode 100644 index 79e6903b186448..00000000000000 --- a/packages/kbn-es-query/src/index.js +++ /dev/null @@ -1,20 +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 './kuery'; diff --git a/packages/kbn-es-query/src/kuery/ast/__tests__/ast.js b/packages/kbn-es-query/src/kuery/ast/__tests__/ast.js deleted file mode 100644 index 3cbe1203bc5330..00000000000000 --- a/packages/kbn-es-query/src/kuery/ast/__tests__/ast.js +++ /dev/null @@ -1,415 +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 * as ast from '../ast'; -import expect from '@kbn/expect'; -import { nodeTypes } from '../../node_types/index'; -import indexPatternResponse from '../../../__fixtures__/index_pattern_response.json'; - -let indexPattern; - -describe('kuery AST API', function () { - - - beforeEach(() => { - indexPattern = indexPatternResponse; - }); - - describe('fromKueryExpression', function () { - - it('should return a match all "is" function for whitespace', function () { - const expected = nodeTypes.function.buildNode('is', '*', '*'); - const actual = ast.fromKueryExpression(' '); - expect(actual).to.eql(expected); - }); - - it('should return an "is" function with a null field for single literals', function () { - const expected = nodeTypes.function.buildNode('is', null, 'foo'); - const actual = ast.fromKueryExpression('foo'); - expect(actual).to.eql(expected); - }); - - it('should ignore extraneous whitespace at the beginning and end of the query', function () { - const expected = nodeTypes.function.buildNode('is', null, 'foo'); - const actual = ast.fromKueryExpression(' foo '); - expect(actual).to.eql(expected); - }); - - it('should not split on whitespace', function () { - const expected = nodeTypes.function.buildNode('is', null, 'foo bar'); - const actual = ast.fromKueryExpression('foo bar'); - expect(actual).to.eql(expected); - }); - - it('should support "and" as a binary operator', function () { - const expected = nodeTypes.function.buildNode('and', [ - nodeTypes.function.buildNode('is', null, 'foo'), - nodeTypes.function.buildNode('is', null, 'bar'), - ]); - const actual = ast.fromKueryExpression('foo and bar'); - expect(actual).to.eql(expected); - }); - - it('should support "or" as a binary operator', function () { - const expected = nodeTypes.function.buildNode('or', [ - nodeTypes.function.buildNode('is', null, 'foo'), - nodeTypes.function.buildNode('is', null, 'bar'), - ]); - const actual = ast.fromKueryExpression('foo or bar'); - expect(actual).to.eql(expected); - }); - - it('should support negation of queries with a "not" prefix', function () { - const expected = nodeTypes.function.buildNode('not', - nodeTypes.function.buildNode('or', [ - nodeTypes.function.buildNode('is', null, 'foo'), - nodeTypes.function.buildNode('is', null, 'bar'), - ]) - ); - const actual = ast.fromKueryExpression('not (foo or bar)'); - expect(actual).to.eql(expected); - }); - - it('"and" should have a higher precedence than "or"', function () { - const expected = nodeTypes.function.buildNode('or', [ - nodeTypes.function.buildNode('is', null, 'foo'), - nodeTypes.function.buildNode('or', [ - nodeTypes.function.buildNode('and', [ - nodeTypes.function.buildNode('is', null, 'bar'), - nodeTypes.function.buildNode('is', null, 'baz'), - ]), - nodeTypes.function.buildNode('is', null, 'qux'), - ]) - ]); - const actual = ast.fromKueryExpression('foo or bar and baz or qux'); - expect(actual).to.eql(expected); - }); - - it('should support grouping to override default precedence', function () { - const expected = nodeTypes.function.buildNode('and', [ - nodeTypes.function.buildNode('or', [ - nodeTypes.function.buildNode('is', null, 'foo'), - nodeTypes.function.buildNode('is', null, 'bar'), - ]), - nodeTypes.function.buildNode('is', null, 'baz'), - ]); - const actual = ast.fromKueryExpression('(foo or bar) and baz'); - expect(actual).to.eql(expected); - }); - - it('should support matching against specific fields', function () { - const expected = nodeTypes.function.buildNode('is', 'foo', 'bar'); - const actual = ast.fromKueryExpression('foo:bar'); - expect(actual).to.eql(expected); - }); - - it('should also not split on whitespace when matching specific fields', function () { - const expected = nodeTypes.function.buildNode('is', 'foo', 'bar baz'); - const actual = ast.fromKueryExpression('foo:bar baz'); - expect(actual).to.eql(expected); - }); - - it('should treat quoted values as phrases', function () { - const expected = nodeTypes.function.buildNode('is', 'foo', 'bar baz', true); - const actual = ast.fromKueryExpression('foo:"bar baz"'); - expect(actual).to.eql(expected); - }); - - it('should support a shorthand for matching multiple values against a single field', function () { - const expected = nodeTypes.function.buildNode('or', [ - nodeTypes.function.buildNode('is', 'foo', 'bar'), - nodeTypes.function.buildNode('is', 'foo', 'baz'), - ]); - const actual = ast.fromKueryExpression('foo:(bar or baz)'); - expect(actual).to.eql(expected); - }); - - it('should support "and" and "not" operators and grouping in the shorthand as well', function () { - const expected = nodeTypes.function.buildNode('and', [ - nodeTypes.function.buildNode('or', [ - nodeTypes.function.buildNode('is', 'foo', 'bar'), - nodeTypes.function.buildNode('is', 'foo', 'baz'), - ]), - nodeTypes.function.buildNode('not', - nodeTypes.function.buildNode('is', 'foo', 'qux') - ), - ]); - const actual = ast.fromKueryExpression('foo:((bar or baz) and not qux)'); - expect(actual).to.eql(expected); - }); - - it('should support exclusive range operators', function () { - const expected = nodeTypes.function.buildNode('and', [ - nodeTypes.function.buildNode('range', 'bytes', { - gt: 1000, - }), - nodeTypes.function.buildNode('range', 'bytes', { - lt: 8000, - }), - ]); - const actual = ast.fromKueryExpression('bytes > 1000 and bytes < 8000'); - expect(actual).to.eql(expected); - }); - - it('should support inclusive range operators', function () { - const expected = nodeTypes.function.buildNode('and', [ - nodeTypes.function.buildNode('range', 'bytes', { - gte: 1000, - }), - nodeTypes.function.buildNode('range', 'bytes', { - lte: 8000, - }), - ]); - const actual = ast.fromKueryExpression('bytes >= 1000 and bytes <= 8000'); - expect(actual).to.eql(expected); - }); - - it('should support wildcards in field names', function () { - const expected = nodeTypes.function.buildNode('is', 'machine*', 'osx'); - const actual = ast.fromKueryExpression('machine*:osx'); - expect(actual).to.eql(expected); - }); - - it('should support wildcards in values', function () { - const expected = nodeTypes.function.buildNode('is', 'foo', 'ba*'); - const actual = ast.fromKueryExpression('foo:ba*'); - expect(actual).to.eql(expected); - }); - - it('should create an exists "is" query when a field is given and "*" is the value', function () { - const expected = nodeTypes.function.buildNode('is', 'foo', '*'); - const actual = ast.fromKueryExpression('foo:*'); - expect(actual).to.eql(expected); - }); - - it('should support nested queries indicated by curly braces', () => { - const expected = nodeTypes.function.buildNode( - 'nested', - 'nestedField', - nodeTypes.function.buildNode('is', 'childOfNested', 'foo') - ); - const actual = ast.fromKueryExpression('nestedField:{ childOfNested: foo }'); - expect(actual).to.eql(expected); - }); - - it('should support nested subqueries and subqueries inside nested queries', () => { - const expected = nodeTypes.function.buildNode( - 'and', - [ - nodeTypes.function.buildNode('is', 'response', '200'), - nodeTypes.function.buildNode( - 'nested', - 'nestedField', - nodeTypes.function.buildNode('or', [ - nodeTypes.function.buildNode('is', 'childOfNested', 'foo'), - nodeTypes.function.buildNode('is', 'childOfNested', 'bar'), - ]) - )]); - const actual = ast.fromKueryExpression('response:200 and nestedField:{ childOfNested:foo or childOfNested:bar }'); - expect(actual).to.eql(expected); - }); - - it('should support nested sub-queries inside paren groups', () => { - const expected = nodeTypes.function.buildNode( - 'and', - [ - nodeTypes.function.buildNode('is', 'response', '200'), - nodeTypes.function.buildNode('or', [ - nodeTypes.function.buildNode( - 'nested', - 'nestedField', - nodeTypes.function.buildNode('is', 'childOfNested', 'foo') - ), - nodeTypes.function.buildNode( - 'nested', - 'nestedField', - nodeTypes.function.buildNode('is', 'childOfNested', 'bar') - ), - ]) - ]); - const actual = ast.fromKueryExpression('response:200 and ( nestedField:{ childOfNested:foo } or nestedField:{ childOfNested:bar } )'); - expect(actual).to.eql(expected); - }); - - it('should support nested groups inside other nested groups', () => { - const expected = nodeTypes.function.buildNode( - 'nested', - 'nestedField', - nodeTypes.function.buildNode( - 'nested', - 'nestedChild', - nodeTypes.function.buildNode('is', 'doublyNestedChild', 'foo') - ) - ); - const actual = ast.fromKueryExpression('nestedField:{ nestedChild:{ doublyNestedChild:foo } }'); - expect(actual).to.eql(expected); - }); - }); - - describe('fromLiteralExpression', function () { - - it('should create literal nodes for unquoted values with correct primitive types', function () { - const stringLiteral = nodeTypes.literal.buildNode('foo'); - const booleanFalseLiteral = nodeTypes.literal.buildNode(false); - const booleanTrueLiteral = nodeTypes.literal.buildNode(true); - const numberLiteral = nodeTypes.literal.buildNode(42); - - expect(ast.fromLiteralExpression('foo')).to.eql(stringLiteral); - expect(ast.fromLiteralExpression('true')).to.eql(booleanTrueLiteral); - expect(ast.fromLiteralExpression('false')).to.eql(booleanFalseLiteral); - expect(ast.fromLiteralExpression('42')).to.eql(numberLiteral); - }); - - it('should allow escaping of special characters with a backslash', function () { - const expected = nodeTypes.literal.buildNode('\\():<>"*'); - // yo dawg - const actual = ast.fromLiteralExpression('\\\\\\(\\)\\:\\<\\>\\"\\*'); - expect(actual).to.eql(expected); - }); - - it('should support double quoted strings that do not need escapes except for quotes', function () { - const expected = nodeTypes.literal.buildNode('\\():<>"*'); - const actual = ast.fromLiteralExpression('"\\():<>\\"*"'); - expect(actual).to.eql(expected); - }); - - it('should support escaped backslashes inside quoted strings', function () { - const expected = nodeTypes.literal.buildNode('\\'); - const actual = ast.fromLiteralExpression('"\\\\"'); - expect(actual).to.eql(expected); - }); - - it('should detect wildcards and build wildcard AST nodes', function () { - const expected = nodeTypes.wildcard.buildNode('foo*bar'); - const actual = ast.fromLiteralExpression('foo*bar'); - expect(actual).to.eql(expected); - }); - }); - - describe('toElasticsearchQuery', function () { - - it('should return the given node type\'s ES query representation', function () { - const node = nodeTypes.function.buildNode('exists', 'response'); - const expected = nodeTypes.function.toElasticsearchQuery(node, indexPattern); - const result = ast.toElasticsearchQuery(node, indexPattern); - expect(result).to.eql(expected); - }); - - it('should return an empty "and" function for undefined nodes and unknown node types', function () { - const expected = nodeTypes.function.toElasticsearchQuery(nodeTypes.function.buildNode('and', [])); - - expect(ast.toElasticsearchQuery()).to.eql(expected); - - const noTypeNode = nodeTypes.function.buildNode('exists', 'foo'); - delete noTypeNode.type; - expect(ast.toElasticsearchQuery(noTypeNode)).to.eql(expected); - - const unknownTypeNode = nodeTypes.function.buildNode('exists', 'foo'); - unknownTypeNode.type = 'notValid'; - expect(ast.toElasticsearchQuery(unknownTypeNode)).to.eql(expected); - }); - - it('should return the given node type\'s ES query representation including a time zone parameter when one is provided', function () { - const config = { dateFormatTZ: 'America/Phoenix' }; - const node = nodeTypes.function.buildNode('is', '@timestamp', '"2018-04-03T19:04:17"'); - const expected = nodeTypes.function.toElasticsearchQuery(node, indexPattern, config); - const result = ast.toElasticsearchQuery(node, indexPattern, config); - expect(result).to.eql(expected); - }); - - }); - - describe('doesKueryExpressionHaveLuceneSyntaxError', function () { - it('should return true for Lucene ranges', function () { - const result = ast.doesKueryExpressionHaveLuceneSyntaxError('bar: [1 TO 10]'); - expect(result).to.eql(true); - }); - - it('should return false for KQL ranges', function () { - const result = ast.doesKueryExpressionHaveLuceneSyntaxError('bar < 1'); - expect(result).to.eql(false); - }); - - it('should return true for Lucene exists', function () { - const result = ast.doesKueryExpressionHaveLuceneSyntaxError('_exists_: bar'); - expect(result).to.eql(true); - }); - - it('should return false for KQL exists', function () { - const result = ast.doesKueryExpressionHaveLuceneSyntaxError('bar:*'); - expect(result).to.eql(false); - }); - - it('should return true for Lucene wildcards', function () { - const result = ast.doesKueryExpressionHaveLuceneSyntaxError('bar: ba?'); - expect(result).to.eql(true); - }); - - it('should return false for KQL wildcards', function () { - const result = ast.doesKueryExpressionHaveLuceneSyntaxError('bar: ba*'); - expect(result).to.eql(false); - }); - - it('should return true for Lucene regex', function () { - const result = ast.doesKueryExpressionHaveLuceneSyntaxError('bar: /ba.*/'); - expect(result).to.eql(true); - }); - - it('should return true for Lucene fuzziness', function () { - const result = ast.doesKueryExpressionHaveLuceneSyntaxError('bar: ba~'); - expect(result).to.eql(true); - }); - - it('should return true for Lucene proximity', function () { - const result = ast.doesKueryExpressionHaveLuceneSyntaxError('bar: "ba"~2'); - expect(result).to.eql(true); - }); - - it('should return true for Lucene boosting', function () { - const result = ast.doesKueryExpressionHaveLuceneSyntaxError('bar: ba^2'); - expect(result).to.eql(true); - }); - - it('should return true for Lucene + operator', function () { - const result = ast.doesKueryExpressionHaveLuceneSyntaxError('+foo: bar'); - expect(result).to.eql(true); - }); - - it('should return true for Lucene - operators', function () { - const result = ast.doesKueryExpressionHaveLuceneSyntaxError('-foo: bar'); - expect(result).to.eql(true); - }); - - it('should return true for Lucene && operators', function () { - const result = ast.doesKueryExpressionHaveLuceneSyntaxError('foo: bar && baz: qux'); - expect(result).to.eql(true); - }); - - it('should return true for Lucene || operators', function () { - const result = ast.doesKueryExpressionHaveLuceneSyntaxError('foo: bar || baz: qux'); - expect(result).to.eql(true); - }); - - it('should return true for mixed KQL/Lucene queries', function () { - const result = ast.doesKueryExpressionHaveLuceneSyntaxError('foo: bar and (baz: qux || bag)'); - expect(result).to.eql(true); - }); - }); - -}); diff --git a/packages/kbn-es-query/src/kuery/ast/ast.d.ts b/packages/kbn-es-query/src/kuery/ast/ast.d.ts deleted file mode 100644 index ef3d0ee8288746..00000000000000 --- a/packages/kbn-es-query/src/kuery/ast/ast.d.ts +++ /dev/null @@ -1,50 +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 { JsonObject } from '..'; - -/** - * WARNING: these typings are incomplete - */ - -export type KueryNode = any; - -export type DslQuery = any; - -export interface KueryParseOptions { - helpers: { - [key: string]: any; - }; - startRule: string; - allowLeadingWildcards: boolean; -} - -export function fromKueryExpression( - expression: string | DslQuery, - parseOptions?: Partial -): KueryNode; - -export function toElasticsearchQuery( - node: KueryNode, - indexPattern?: any, - config?: Record, - context?: Record -): JsonObject; - -export function doesKueryExpressionHaveLuceneSyntaxError(expression: string): boolean; diff --git a/packages/kbn-es-query/src/kuery/ast/index.d.ts b/packages/kbn-es-query/src/kuery/ast/index.d.ts deleted file mode 100644 index 9e68d01d046cca..00000000000000 --- a/packages/kbn-es-query/src/kuery/ast/index.d.ts +++ /dev/null @@ -1,20 +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 '../ast/ast'; diff --git a/packages/kbn-es-query/src/kuery/functions/__tests__/geo_bounding_box.js b/packages/kbn-es-query/src/kuery/functions/__tests__/geo_bounding_box.js deleted file mode 100644 index 7afa0fcce1bfeb..00000000000000 --- a/packages/kbn-es-query/src/kuery/functions/__tests__/geo_bounding_box.js +++ /dev/null @@ -1,120 +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 expect from '@kbn/expect'; -import * as geoBoundingBox from '../geo_bounding_box'; -import { nodeTypes } from '../../node_types'; -import indexPatternResponse from '../../../__fixtures__/index_pattern_response.json'; - -let indexPattern; -const params = { - bottomRight: { - lat: 50.73, - lon: -135.35 - }, - topLeft: { - lat: 73.12, - lon: -174.37 - } -}; - -describe('kuery functions', function () { - describe('geoBoundingBox', function () { - - beforeEach(() => { - indexPattern = indexPatternResponse; - }); - - describe('buildNodeParams', function () { - - it('should return an "arguments" param', function () { - const result = geoBoundingBox.buildNodeParams('geo', params); - expect(result).to.only.have.keys('arguments'); - }); - - it('arguments should contain the provided fieldName as a literal', function () { - const result = geoBoundingBox.buildNodeParams('geo', params); - const { arguments: [ fieldName ] } = result; - - expect(fieldName).to.have.property('type', 'literal'); - expect(fieldName).to.have.property('value', 'geo'); - }); - - it('arguments should contain the provided params as named arguments with "lat, lon" string values', function () { - const result = geoBoundingBox.buildNodeParams('geo', params); - const { arguments: [ , ...args ] } = result; - - args.map((param) => { - expect(param).to.have.property('type', 'namedArg'); - expect(['bottomRight', 'topLeft'].includes(param.name)).to.be(true); - expect(param.value.type).to.be('literal'); - - const expectedParam = params[param.name]; - const expectedLatLon = `${expectedParam.lat}, ${expectedParam.lon}`; - expect(param.value.value).to.be(expectedLatLon); - }); - }); - - }); - - describe('toElasticsearchQuery', function () { - - it('should return an ES geo_bounding_box query representing the given node', function () { - const node = nodeTypes.function.buildNode('geoBoundingBox', 'geo', params); - const result = geoBoundingBox.toElasticsearchQuery(node, indexPattern); - expect(result).to.have.property('geo_bounding_box'); - expect(result.geo_bounding_box.geo).to.have.property('top_left', '73.12, -174.37'); - expect(result.geo_bounding_box.geo).to.have.property('bottom_right', '50.73, -135.35'); - }); - - it('should return an ES geo_bounding_box query without an index pattern', function () { - const node = nodeTypes.function.buildNode('geoBoundingBox', 'geo', params); - const result = geoBoundingBox.toElasticsearchQuery(node); - expect(result).to.have.property('geo_bounding_box'); - expect(result.geo_bounding_box.geo).to.have.property('top_left', '73.12, -174.37'); - expect(result.geo_bounding_box.geo).to.have.property('bottom_right', '50.73, -135.35'); - }); - - it('should use the ignore_unmapped parameter', function () { - const node = nodeTypes.function.buildNode('geoBoundingBox', 'geo', params); - const result = geoBoundingBox.toElasticsearchQuery(node, indexPattern); - expect(result.geo_bounding_box.ignore_unmapped).to.be(true); - }); - - it('should throw an error for scripted fields', function () { - const node = nodeTypes.function.buildNode('geoBoundingBox', 'script number', params); - expect(geoBoundingBox.toElasticsearchQuery) - .withArgs(node, indexPattern).to.throwException(/Geo bounding box query does not support scripted fields/); - }); - - it('should use a provided nested context to create a full field name', function () { - const node = nodeTypes.function.buildNode('geoBoundingBox', 'geo', params); - const result = geoBoundingBox.toElasticsearchQuery( - node, - indexPattern, - {}, - { nested: { path: 'nestedField' } } - ); - expect(result).to.have.property('geo_bounding_box'); - expect(result.geo_bounding_box).to.have.property('nestedField.geo'); - }); - - }); - }); -}); diff --git a/packages/kbn-es-query/src/kuery/functions/__tests__/geo_polygon.js b/packages/kbn-es-query/src/kuery/functions/__tests__/geo_polygon.js deleted file mode 100644 index c1f2fae0bb3e1f..00000000000000 --- a/packages/kbn-es-query/src/kuery/functions/__tests__/geo_polygon.js +++ /dev/null @@ -1,131 +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 expect from '@kbn/expect'; -import * as geoPolygon from '../geo_polygon'; -import { nodeTypes } from '../../node_types'; -import indexPatternResponse from '../../../__fixtures__/index_pattern_response.json'; - - -let indexPattern; -const points = [ - { - lat: 69.77, - lon: -171.56 - }, - { - lat: 50.06, - lon: -169.10 - }, - { - lat: 69.16, - lon: -125.85 - } -]; - -describe('kuery functions', function () { - - describe('geoPolygon', function () { - - - beforeEach(() => { - indexPattern = indexPatternResponse; - }); - - describe('buildNodeParams', function () { - - it('should return an "arguments" param', function () { - const result = geoPolygon.buildNodeParams('geo', points); - expect(result).to.only.have.keys('arguments'); - }); - - it('arguments should contain the provided fieldName as a literal', function () { - const result = geoPolygon.buildNodeParams('geo', points); - const { arguments: [ fieldName ] } = result; - - expect(fieldName).to.have.property('type', 'literal'); - expect(fieldName).to.have.property('value', 'geo'); - }); - - it('arguments should contain the provided points literal "lat, lon" string values', function () { - const result = geoPolygon.buildNodeParams('geo', points); - const { arguments: [ , ...args ] } = result; - - args.forEach((param, index) => { - expect(param).to.have.property('type', 'literal'); - const expectedPoint = points[index]; - const expectedLatLon = `${expectedPoint.lat}, ${expectedPoint.lon}`; - expect(param.value).to.be(expectedLatLon); - }); - }); - - }); - - describe('toElasticsearchQuery', function () { - - it('should return an ES geo_polygon query representing the given node', function () { - const node = nodeTypes.function.buildNode('geoPolygon', 'geo', points); - const result = geoPolygon.toElasticsearchQuery(node, indexPattern); - expect(result).to.have.property('geo_polygon'); - expect(result.geo_polygon.geo).to.have.property('points'); - - result.geo_polygon.geo.points.forEach((point, index) => { - const expectedLatLon = `${points[index].lat}, ${points[index].lon}`; - expect(point).to.be(expectedLatLon); - }); - }); - - it('should return an ES geo_polygon query without an index pattern', function () { - const node = nodeTypes.function.buildNode('geoPolygon', 'geo', points); - const result = geoPolygon.toElasticsearchQuery(node); - expect(result).to.have.property('geo_polygon'); - expect(result.geo_polygon.geo).to.have.property('points'); - - result.geo_polygon.geo.points.forEach((point, index) => { - const expectedLatLon = `${points[index].lat}, ${points[index].lon}`; - expect(point).to.be(expectedLatLon); - }); - }); - - it('should use the ignore_unmapped parameter', function () { - const node = nodeTypes.function.buildNode('geoPolygon', 'geo', points); - const result = geoPolygon.toElasticsearchQuery(node, indexPattern); - expect(result.geo_polygon.ignore_unmapped).to.be(true); - }); - - it('should throw an error for scripted fields', function () { - const node = nodeTypes.function.buildNode('geoPolygon', 'script number', points); - expect(geoPolygon.toElasticsearchQuery) - .withArgs(node, indexPattern).to.throwException(/Geo polygon query does not support scripted fields/); - }); - - it('should use a provided nested context to create a full field name', function () { - const node = nodeTypes.function.buildNode('geoPolygon', 'geo', points); - const result = geoPolygon.toElasticsearchQuery( - node, - indexPattern, - {}, - { nested: { path: 'nestedField' } } - ); - expect(result).to.have.property('geo_polygon'); - expect(result.geo_polygon).to.have.property('nestedField.geo'); - }); - }); - }); -}); diff --git a/packages/kbn-es-query/src/kuery/functions/__tests__/is.js b/packages/kbn-es-query/src/kuery/functions/__tests__/is.js deleted file mode 100644 index b2f3d7ec16a658..00000000000000 --- a/packages/kbn-es-query/src/kuery/functions/__tests__/is.js +++ /dev/null @@ -1,310 +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 expect from '@kbn/expect'; -import * as is from '../is'; -import { nodeTypes } from '../../node_types'; -import indexPatternResponse from '../../../__fixtures__/index_pattern_response.json'; - -let indexPattern; - -describe('kuery functions', function () { - - describe('is', function () { - - - beforeEach(() => { - indexPattern = indexPatternResponse; - }); - - describe('buildNodeParams', function () { - - it('fieldName and value should be required arguments', function () { - expect(is.buildNodeParams).to.throwException(/fieldName is a required argument/); - expect(is.buildNodeParams).withArgs('foo').to.throwException(/value is a required argument/); - }); - - it('arguments should contain the provided fieldName and value as literals', function () { - const { arguments: [fieldName, value] } = is.buildNodeParams('response', 200); - - expect(fieldName).to.have.property('type', 'literal'); - expect(fieldName).to.have.property('value', 'response'); - - expect(value).to.have.property('type', 'literal'); - expect(value).to.have.property('value', 200); - }); - - it('should detect wildcards in the provided arguments', function () { - const { arguments: [fieldName, value] } = is.buildNodeParams('machine*', 'win*'); - - expect(fieldName).to.have.property('type', 'wildcard'); - expect(value).to.have.property('type', 'wildcard'); - }); - - it('should default to a non-phrase query', function () { - const { arguments: [, , isPhrase] } = is.buildNodeParams('response', 200); - expect(isPhrase.value).to.be(false); - }); - - it('should allow specification of a phrase query', function () { - const { arguments: [, , isPhrase] } = is.buildNodeParams('response', 200, true); - expect(isPhrase.value).to.be(true); - }); - }); - - describe('toElasticsearchQuery', function () { - - it('should return an ES match_all query when fieldName and value are both "*"', function () { - const expected = { - match_all: {} - }; - - const node = nodeTypes.function.buildNode('is', '*', '*'); - const result = is.toElasticsearchQuery(node, indexPattern); - expect(result).to.eql(expected); - }); - - it('should return an ES multi_match query using default_field when fieldName is null', function () { - const expected = { - multi_match: { - query: 200, - type: 'best_fields', - lenient: true, - } - }; - - const node = nodeTypes.function.buildNode('is', null, 200); - const result = is.toElasticsearchQuery(node, indexPattern); - expect(result).to.eql(expected); - }); - - it('should return an ES query_string query using default_field when fieldName is null and value contains a wildcard', function () { - const expected = { - query_string: { - query: 'jpg*', - } - }; - - const node = nodeTypes.function.buildNode('is', null, 'jpg*'); - const result = is.toElasticsearchQuery(node, indexPattern); - expect(result).to.eql(expected); - }); - - it('should return an ES bool query with a sub-query for each field when fieldName is "*"', function () { - const node = nodeTypes.function.buildNode('is', '*', 200); - const result = is.toElasticsearchQuery(node, indexPattern); - expect(result).to.have.property('bool'); - expect(result.bool.should).to.have.length(indexPattern.fields.length); - }); - - it('should return an ES exists query when value is "*"', function () { - const expected = { - bool: { - should: [ - { exists: { field: 'extension' } }, - ], - minimum_should_match: 1 - } - }; - - const node = nodeTypes.function.buildNode('is', 'extension', '*'); - const result = is.toElasticsearchQuery(node, indexPattern); - expect(result).to.eql(expected); - }); - - it('should return an ES match query when a concrete fieldName and value are provided', function () { - const expected = { - bool: { - should: [ - { match: { extension: 'jpg' } }, - ], - minimum_should_match: 1 - } - }; - - const node = nodeTypes.function.buildNode('is', 'extension', 'jpg'); - const result = is.toElasticsearchQuery(node, indexPattern); - expect(result).to.eql(expected); - }); - - it('should return an ES match query when a concrete fieldName and value are provided without an index pattern', function () { - const expected = { - bool: { - should: [ - { match: { extension: 'jpg' } }, - ], - minimum_should_match: 1 - } - }; - - const node = nodeTypes.function.buildNode('is', 'extension', 'jpg'); - const result = is.toElasticsearchQuery(node); - expect(result).to.eql(expected); - }); - - it('should support creation of phrase queries', function () { - const expected = { - bool: { - should: [ - { match_phrase: { extension: 'jpg' } }, - ], - minimum_should_match: 1 - } - }; - - const node = nodeTypes.function.buildNode('is', 'extension', 'jpg', true); - const result = is.toElasticsearchQuery(node, indexPattern); - expect(result).to.eql(expected); - }); - - it('should create a query_string query for wildcard values', function () { - const expected = { - bool: { - should: [ - { - query_string: { - fields: ['extension'], - query: 'jpg*' - } - }, - ], - minimum_should_match: 1 - } - }; - - const node = nodeTypes.function.buildNode('is', 'extension', 'jpg*'); - const result = is.toElasticsearchQuery(node, indexPattern); - expect(result).to.eql(expected); - }); - - it('should support scripted fields', function () { - const node = nodeTypes.function.buildNode('is', 'script string', 'foo'); - const result = is.toElasticsearchQuery(node, indexPattern); - expect(result.bool.should[0]).to.have.key('script'); - }); - - it('should support date fields without a dateFormat provided', function () { - const expected = { - bool: { - should: [ - { - range: { - '@timestamp': { - gte: '2018-04-03T19:04:17', - lte: '2018-04-03T19:04:17', - } - } - } - ], - minimum_should_match: 1 - } - }; - - const node = nodeTypes.function.buildNode('is', '@timestamp', '"2018-04-03T19:04:17"'); - const result = is.toElasticsearchQuery(node, indexPattern); - expect(result).to.eql(expected); - }); - - it('should support date fields with a dateFormat provided', function () { - const config = { dateFormatTZ: 'America/Phoenix' }; - const expected = { - bool: { - should: [ - { - range: { - '@timestamp': { - gte: '2018-04-03T19:04:17', - lte: '2018-04-03T19:04:17', - time_zone: 'America/Phoenix', - } - } - } - ], - minimum_should_match: 1 - } - }; - - const node = nodeTypes.function.buildNode('is', '@timestamp', '"2018-04-03T19:04:17"'); - const result = is.toElasticsearchQuery(node, indexPattern, config); - expect(result).to.eql(expected); - }); - - it('should use a provided nested context to create a full field name', function () { - const expected = { - bool: { - should: [ - { match: { 'nestedField.extension': 'jpg' } }, - ], - minimum_should_match: 1 - } - }; - - const node = nodeTypes.function.buildNode('is', 'extension', 'jpg'); - const result = is.toElasticsearchQuery( - node, - indexPattern, - {}, - { nested: { path: 'nestedField' } } - ); - expect(result).to.eql(expected); - }); - - it('should support wildcard field names', function () { - const expected = { - bool: { - should: [ - { match: { extension: 'jpg' } }, - ], - minimum_should_match: 1 - } - }; - - const node = nodeTypes.function.buildNode('is', 'ext*', 'jpg'); - const result = is.toElasticsearchQuery(node, indexPattern); - expect(result).to.eql(expected); - }); - - it('should automatically add a nested query when a wildcard field name covers a nested field', () => { - const expected = { - bool: { - should: [ - { - nested: { - path: 'nestedField.nestedChild', - query: { - match: { - 'nestedField.nestedChild.doublyNestedChild': 'foo' - } - }, - score_mode: 'none' - } - } - ], - minimum_should_match: 1 - } - }; - - - const node = nodeTypes.function.buildNode('is', '*doublyNested*', 'foo'); - const result = is.toElasticsearchQuery(node, indexPattern); - expect(result).to.eql(expected); - }); - }); - }); -}); diff --git a/packages/kbn-es-query/src/kuery/functions/__tests__/utils/get_full_field_name_node.js b/packages/kbn-es-query/src/kuery/functions/__tests__/utils/get_full_field_name_node.js deleted file mode 100644 index dae15979a161cd..00000000000000 --- a/packages/kbn-es-query/src/kuery/functions/__tests__/utils/get_full_field_name_node.js +++ /dev/null @@ -1,88 +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 expect from '@kbn/expect'; -import { nodeTypes } from '../../../node_types'; -import indexPatternResponse from '../../../../__fixtures__/index_pattern_response.json'; -import { getFullFieldNameNode } from '../../utils/get_full_field_name_node'; - -let indexPattern; - -describe('getFullFieldNameNode', function () { - - beforeEach(() => { - indexPattern = indexPatternResponse; - }); - - it('should return unchanged name node if no nested path is passed in', () => { - const nameNode = nodeTypes.literal.buildNode('notNested'); - const result = getFullFieldNameNode(nameNode, indexPattern); - expect(result).to.eql(nameNode); - }); - - it('should add the nested path if it is valid according to the index pattern', () => { - const nameNode = nodeTypes.literal.buildNode('child'); - const result = getFullFieldNameNode(nameNode, indexPattern, 'nestedField'); - expect(result).to.eql(nodeTypes.literal.buildNode('nestedField.child')); - }); - - it('should throw an error if a path is provided for a non-nested field', () => { - const nameNode = nodeTypes.literal.buildNode('os'); - expect(getFullFieldNameNode) - .withArgs(nameNode, indexPattern, 'machine') - .to - .throwException(/machine.os is not a nested field but is in nested group "machine" in the KQL expression/); - }); - - it('should throw an error if a nested field is not passed with a path', () => { - const nameNode = nodeTypes.literal.buildNode('nestedField.child'); - expect(getFullFieldNameNode) - .withArgs(nameNode, indexPattern) - .to - .throwException(/nestedField.child is a nested field, but is not in a nested group in the KQL expression./); - }); - - it('should throw an error if a nested field is passed with the wrong path', () => { - const nameNode = nodeTypes.literal.buildNode('nestedChild.doublyNestedChild'); - expect(getFullFieldNameNode) - .withArgs(nameNode, indexPattern, 'nestedField') - .to - // eslint-disable-next-line max-len - .throwException(/Nested field nestedField.nestedChild.doublyNestedChild is being queried with the incorrect nested path. The correct path is nestedField.nestedChild/); - }); - - it('should skip error checking for wildcard names', () => { - const nameNode = nodeTypes.wildcard.buildNode('nested*'); - const result = getFullFieldNameNode(nameNode, indexPattern); - expect(result).to.eql(nameNode); - }); - - it('should skip error checking if no index pattern is passed in', () => { - const nameNode = nodeTypes.literal.buildNode('os'); - expect(getFullFieldNameNode) - .withArgs(nameNode, null, 'machine') - .to - .not - .throwException(); - - const result = getFullFieldNameNode(nameNode, null, 'machine'); - expect(result).to.eql(nodeTypes.literal.buildNode('machine.os')); - }); - -}); diff --git a/packages/kbn-es-query/src/kuery/node_types/__tests__/function.js b/packages/kbn-es-query/src/kuery/node_types/__tests__/function.js deleted file mode 100644 index de00c083fc8304..00000000000000 --- a/packages/kbn-es-query/src/kuery/node_types/__tests__/function.js +++ /dev/null @@ -1,80 +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 * as functionType from '../function'; -import _ from 'lodash'; -import expect from '@kbn/expect'; -import * as isFunction from '../../functions/is'; -import indexPatternResponse from '../../../__fixtures__/index_pattern_response.json'; - -import { nodeTypes } from '../../node_types'; - -describe('kuery node types', function () { - - describe('function', function () { - - let indexPattern; - - beforeEach(() => { - indexPattern = indexPatternResponse; - }); - - describe('buildNode', function () { - - it('should return a node representing the given kuery function', function () { - const result = functionType.buildNode('is', 'extension', 'jpg'); - expect(result).to.have.property('type', 'function'); - expect(result).to.have.property('function', 'is'); - expect(result).to.have.property('arguments'); - }); - - }); - - describe('buildNodeWithArgumentNodes', function () { - - it('should return a function node with the given argument list untouched', function () { - const fieldNameLiteral = nodeTypes.literal.buildNode('extension'); - const valueLiteral = nodeTypes.literal.buildNode('jpg'); - const argumentNodes = [fieldNameLiteral, valueLiteral]; - const result = functionType.buildNodeWithArgumentNodes('is', argumentNodes); - - expect(result).to.have.property('type', 'function'); - expect(result).to.have.property('function', 'is'); - expect(result).to.have.property('arguments'); - expect(result.arguments).to.be(argumentNodes); - expect(result.arguments).to.eql(argumentNodes); - }); - - }); - - describe('toElasticsearchQuery', function () { - - it('should return the given function type\'s ES query representation', function () { - const node = functionType.buildNode('is', 'extension', 'jpg'); - const expected = isFunction.toElasticsearchQuery(node, indexPattern); - const result = functionType.toElasticsearchQuery(node, indexPattern); - expect(_.isEqual(expected, result)).to.be(true); - }); - - }); - - - }); - -}); diff --git a/packages/kbn-es-query/src/kuery/node_types/__tests__/named_arg.js b/packages/kbn-es-query/src/kuery/node_types/__tests__/named_arg.js deleted file mode 100644 index cfb8f6d5274dbe..00000000000000 --- a/packages/kbn-es-query/src/kuery/node_types/__tests__/named_arg.js +++ /dev/null @@ -1,62 +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 expect from '@kbn/expect'; -import * as namedArg from '../named_arg'; -import { nodeTypes } from '../../node_types'; - -describe('kuery node types', function () { - - describe('named arg', function () { - - describe('buildNode', function () { - - it('should return a node representing a named argument with the given value', function () { - const result = namedArg.buildNode('fieldName', 'foo'); - expect(result).to.have.property('type', 'namedArg'); - expect(result).to.have.property('name', 'fieldName'); - expect(result).to.have.property('value'); - - const literalValue = result.value; - expect(literalValue).to.have.property('type', 'literal'); - expect(literalValue).to.have.property('value', 'foo'); - }); - - it('should support literal nodes as values', function () { - const value = nodeTypes.literal.buildNode('foo'); - const result = namedArg.buildNode('fieldName', value); - expect(result.value).to.be(value); - expect(result.value).to.eql(value); - }); - - }); - - describe('toElasticsearchQuery', function () { - - it('should return the argument value represented by the given node', function () { - const node = namedArg.buildNode('fieldName', 'foo'); - const result = namedArg.toElasticsearchQuery(node); - expect(result).to.be('foo'); - }); - - }); - - }); - -}); diff --git a/packages/kbn-es-query/src/kuery/node_types/__tests__/wildcard.js b/packages/kbn-es-query/src/kuery/node_types/__tests__/wildcard.js deleted file mode 100644 index 0c4379378c6d6f..00000000000000 --- a/packages/kbn-es-query/src/kuery/node_types/__tests__/wildcard.js +++ /dev/null @@ -1,107 +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 expect from '@kbn/expect'; -import * as wildcard from '../wildcard'; - -describe('kuery node types', function () { - - describe('wildcard', function () { - - describe('buildNode', function () { - - it('should accept a string argument representing a wildcard string', function () { - const wildcardValue = `foo${wildcard.wildcardSymbol}bar`; - const result = wildcard.buildNode(wildcardValue); - expect(result).to.have.property('type', 'wildcard'); - expect(result).to.have.property('value', wildcardValue); - }); - - it('should accept and parse a wildcard string', function () { - const result = wildcard.buildNode('foo*bar'); - expect(result).to.have.property('type', 'wildcard'); - expect(result.value).to.be(`foo${wildcard.wildcardSymbol}bar`); - }); - - }); - - describe('toElasticsearchQuery', function () { - - it('should return the string representation of the wildcard literal', function () { - const node = wildcard.buildNode('foo*bar'); - const result = wildcard.toElasticsearchQuery(node); - expect(result).to.be('foo*bar'); - }); - - }); - - describe('toQueryStringQuery', function () { - - it('should return the string representation of the wildcard literal', function () { - const node = wildcard.buildNode('foo*bar'); - const result = wildcard.toQueryStringQuery(node); - expect(result).to.be('foo*bar'); - }); - - it('should escape query_string query special characters other than wildcard', function () { - const node = wildcard.buildNode('+foo*bar'); - const result = wildcard.toQueryStringQuery(node); - expect(result).to.be('\\+foo*bar'); - }); - - }); - - describe('test', function () { - - it('should return a boolean indicating whether the string matches the given wildcard node', function () { - const node = wildcard.buildNode('foo*bar'); - expect(wildcard.test(node, 'foobar')).to.be(true); - expect(wildcard.test(node, 'foobazbar')).to.be(true); - expect(wildcard.test(node, 'foobar')).to.be(true); - - expect(wildcard.test(node, 'fooqux')).to.be(false); - expect(wildcard.test(node, 'bazbar')).to.be(false); - }); - - it('should return a true even when the string has newlines or tabs', function () { - const node = wildcard.buildNode('foo*bar'); - expect(wildcard.test(node, 'foo\nbar')).to.be(true); - expect(wildcard.test(node, 'foo\tbar')).to.be(true); - }); - }); - - describe('hasLeadingWildcard', function () { - it('should determine whether a wildcard node contains a leading wildcard', function () { - const node = wildcard.buildNode('foo*bar'); - expect(wildcard.hasLeadingWildcard(node)).to.be(false); - - const leadingWildcardNode = wildcard.buildNode('*foobar'); - expect(wildcard.hasLeadingWildcard(leadingWildcardNode)).to.be(true); - }); - - // Lone wildcards become exists queries, so we aren't worried about their performance - it('should not consider a lone wildcard to be a leading wildcard', function () { - const leadingWildcardNode = wildcard.buildNode('*'); - expect(wildcard.hasLeadingWildcard(leadingWildcardNode)).to.be(false); - }); - }); - - }); - -}); diff --git a/packages/kbn-es-query/src/utils/__tests__/get_time_zone_from_settings.js b/packages/kbn-es-query/src/utils/__tests__/get_time_zone_from_settings.js deleted file mode 100644 index 6deaccadfdb76c..00000000000000 --- a/packages/kbn-es-query/src/utils/__tests__/get_time_zone_from_settings.js +++ /dev/null @@ -1,36 +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 expect from '@kbn/expect'; -import { getTimeZoneFromSettings } from '../get_time_zone_from_settings'; - -describe('get timezone from settings', function () { - - it('should return the config timezone if the time zone is set', function () { - const result = getTimeZoneFromSettings('America/Chicago'); - expect(result).to.eql('America/Chicago'); - }); - - it('should return the system timezone if the time zone is set to "Browser"', function () { - const result = getTimeZoneFromSettings('Browser'); - expect(result).to.not.equal('Browser'); - }); - -}); - diff --git a/packages/kbn-es-query/src/utils/filters.js b/packages/kbn-es-query/src/utils/filters.js deleted file mode 100644 index 6e4f5c342688ce..00000000000000 --- a/packages/kbn-es-query/src/utils/filters.js +++ /dev/null @@ -1,133 +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 { pick, get, reduce, map } from 'lodash'; - -/** @deprecated - * @see src/plugins/data/public/es_query/filters/phrase_filter.ts - * Code was already moved into src/plugins/data/public. - * This method will be removed after moving 'es_query' into new platform - * */ -export const getConvertedValueForField = (field, value) => { - if (typeof value !== 'boolean' && field.type === 'boolean') { - if ([1, 'true'].includes(value)) { - return true; - } else if ([0, 'false'].includes(value)) { - return false; - } else { - throw new Error(`${value} is not a valid boolean value for boolean field ${field.name}`); - } - } - return value; -}; - -/** @deprecated - * @see src/plugins/data/public/es_query/filters/phrase_filter.ts - * Code was already moved into src/plugins/data/public. - * This method will be removed after moving 'es_query' into new platform - * */ -export const buildInlineScriptForPhraseFilter = (scriptedField) => { - // We must wrap painless scripts in a lambda in case they're more than a simple expression - if (scriptedField.lang === 'painless') { - return ( - `boolean compare(Supplier s, def v) {return s.get() == v;}` + - `compare(() -> { ${scriptedField.script} }, params.value);` - ); - } else { - return `(${scriptedField.script}) == value`; - } -}; - -/** @deprecated - * @see src/plugins/data/public/es_query/filters/phrase_filter.ts - * Code was already moved into src/plugins/data/public. - * This method will be removed after moving 'es_query' into new platform - * */ -export function getPhraseScript(field, value) { - const convertedValue = getConvertedValueForField(field, value); - const script = buildInlineScriptForPhraseFilter(field); - - return { - script: { - source: script, - lang: field.lang, - params: { - value: convertedValue, - }, - }, - }; -} - -/** @deprecated - * @see src/plugins/data/public/es_query/filters/range_filter.ts - * Code was already moved into src/plugins/data/public. - * This method will be removed after moving 'kuery' into new platform - * */ -export function getRangeScript(field, params) { - const operators = { - gt: '>', - gte: '>=', - lte: '<=', - lt: '<', - }; - const comparators = { - gt: 'boolean gt(Supplier s, def v) {return s.get() > v}', - gte: 'boolean gte(Supplier s, def v) {return s.get() >= v}', - lte: 'boolean lte(Supplier s, def v) {return s.get() <= v}', - lt: 'boolean lt(Supplier s, def v) {return s.get() < v}', - }; - - const dateComparators = { - gt: 'boolean gt(Supplier s, def v) {return s.get().toInstant().isAfter(Instant.parse(v))}', - gte: 'boolean gte(Supplier s, def v) {return !s.get().toInstant().isBefore(Instant.parse(v))}', - lte: 'boolean lte(Supplier s, def v) {return !s.get().toInstant().isAfter(Instant.parse(v))}', - lt: 'boolean lt(Supplier s, def v) {return s.get().toInstant().isBefore(Instant.parse(v))}', - }; - - const knownParams = pick(params, (val, key) => { - return key in operators; - }); - let script = map(knownParams, (val, key) => { - return '(' + field.script + ')' + get(operators, key) + key; - }).join(' && '); - - // We must wrap painless scripts in a lambda in case they're more than a simple expression - if (field.lang === 'painless') { - const comp = field.type === 'date' ? dateComparators : comparators; - const currentComparators = reduce( - knownParams, - (acc, val, key) => acc.concat(get(comp, key)), - [] - ).join(' '); - - const comparisons = map(knownParams, (val, key) => { - return `${key}(() -> { ${field.script} }, params.${key})`; - }).join(' && '); - - script = `${currentComparators}${comparisons}`; - } - - return { - script: { - source: script, - params: knownParams, - lang: field.lang, - }, - }; -} diff --git a/packages/kbn-es-query/src/utils/get_time_zone_from_settings.js b/packages/kbn-es-query/src/utils/get_time_zone_from_settings.js deleted file mode 100644 index 1a06941ece1274..00000000000000 --- a/packages/kbn-es-query/src/utils/get_time_zone_from_settings.js +++ /dev/null @@ -1,28 +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 moment from 'moment-timezone'; -const detectedTimezone = moment.tz.guess(); - -export function getTimeZoneFromSettings(dateFormatTZ) { - if (dateFormatTZ === 'Browser') { - return detectedTimezone; - } - return dateFormatTZ; -} diff --git a/packages/kbn-es-query/src/utils/index.js b/packages/kbn-es-query/src/utils/index.js deleted file mode 100644 index 27f51c1f44cf2f..00000000000000 --- a/packages/kbn-es-query/src/utils/index.js +++ /dev/null @@ -1,20 +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 './get_time_zone_from_settings'; diff --git a/packages/kbn-es-query/tasks/build_cli.js b/packages/kbn-es-query/tasks/build_cli.js deleted file mode 100644 index 2a43c4d10e0070..00000000000000 --- a/packages/kbn-es-query/tasks/build_cli.js +++ /dev/null @@ -1,103 +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. - */ - -const { resolve } = require('path'); - -const getopts = require('getopts'); -const del = require('del'); -const supportsColor = require('supports-color'); -const { ToolingLog, withProcRunner, pickLevelFromFlags } = require('@kbn/dev-utils'); - -const ROOT_DIR = resolve(__dirname, '..'); -const BUILD_DIR = resolve(ROOT_DIR, 'target'); - -const padRight = (width, str) => - str.length >= width ? str : `${str}${' '.repeat(width - str.length)}`; - -const unknownFlags = []; -const flags = getopts(process.argv, { - boolean: ['watch', 'help', 'source-maps'], - unknown(name) { - unknownFlags.push(name); - }, -}); - -const log = new ToolingLog({ - level: pickLevelFromFlags(flags), - writeTo: process.stdout, -}); - -if (unknownFlags.length) { - log.error(`Unknown flag(s): ${unknownFlags.join(', ')}`); - flags.help = true; - process.exitCode = 1; -} - -if (flags.help) { - log.info(` - Simple build tool for @kbn/es-query package - - --watch Run in watch mode - --source-maps Include sourcemaps - --help Show this message - `); - process.exit(); -} - -withProcRunner(log, async proc => { - log.info('Deleting old output'); - await del(BUILD_DIR); - - const cwd = ROOT_DIR; - const env = { ...process.env }; - if (supportsColor.stdout) { - env.FORCE_COLOR = 'true'; - } - - log.info(`Starting babel and typescript${flags.watch ? ' in watch mode' : ''}`); - await Promise.all([ - ...['public', 'server'].map(subTask => - proc.run(padRight(12, `babel:${subTask}`), { - cmd: 'babel', - args: [ - 'src', - '--config-file', - require.resolve('../babel.config.js'), - '--out-dir', - resolve(BUILD_DIR, subTask), - '--extensions', - '.js,.ts,.tsx', - ...(flags.watch ? ['--watch'] : ['--quiet']), - ...(flags['source-maps'] ? ['--source-map', 'inline'] : []), - ], - wait: true, - cwd, - env: { - ...env, - BABEL_ENV: subTask, - }, - }) - ), - ]); - - log.success('Complete'); -}).catch(error => { - log.error(error); - process.exit(1); -}); diff --git a/packages/kbn-es-query/tsconfig.browser.json b/packages/kbn-es-query/tsconfig.browser.json deleted file mode 100644 index 4a914074712661..00000000000000 --- a/packages/kbn-es-query/tsconfig.browser.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "extends": "../../tsconfig.browser.json", - "compilerOptions": { - "rootDir": "./src", - "outDir": "./target/public" - }, - "include": [ - "index.d.ts", - "src/**/*.ts" - ] -} diff --git a/packages/kbn-es-query/tsconfig.json b/packages/kbn-es-query/tsconfig.json deleted file mode 100644 index 05f51bbccd2ff3..00000000000000 --- a/packages/kbn-es-query/tsconfig.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "extends": "../../tsconfig.json", - "compilerOptions": { - "rootDir": "./src", - "outDir": "./target/server" - }, - "include": [ - "index.d.ts", - "src/**/*.ts" - ] -} diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index 16a634b2d32872..30a98c9046ff50 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -846,7 +846,7 @@ export class SavedObjectsClient { bulkUpdate(objects?: SavedObjectsBulkUpdateObject[]): Promise>; create: (type: string, attributes: T, options?: SavedObjectsCreateOptions) => Promise>; delete: (type: string, id: string) => Promise<{}>; - find: (options: Pick) => Promise>; + find: (options: Pick) => Promise>; get: (type: string, id: string) => Promise>; update(type: string, id: string, attributes: T, { version, migrationVersion, references }?: SavedObjectsUpdateOptions): Promise>; } diff --git a/src/core/server/saved_objects/service/lib/filter_utils.test.ts b/src/core/server/saved_objects/service/lib/filter_utils.test.ts index 4c4f321695d706..13a132ab9dd67b 100644 --- a/src/core/server/saved_objects/service/lib/filter_utils.test.ts +++ b/src/core/server/saved_objects/service/lib/filter_utils.test.ts @@ -17,7 +17,7 @@ * under the License. */ -import { fromKueryExpression } from '@kbn/es-query'; +import { esKuery } from '../../../../../plugins/data/server'; import { validateFilterKueryNode, validateConvertFilterToKueryNode } from './filter_utils'; @@ -64,7 +64,7 @@ describe('Filter Utils', () => { test('Validate a simple filter', () => { expect( validateConvertFilterToKueryNode(['foo'], 'foo.attributes.title: "best"', mockMappings) - ).toEqual(fromKueryExpression('foo.title: "best"')); + ).toEqual(esKuery.fromKueryExpression('foo.title: "best"')); }); test('Assemble filter kuery node saved object attributes with one saved object type', () => { expect( @@ -74,7 +74,7 @@ describe('Filter Utils', () => { mockMappings ) ).toEqual( - fromKueryExpression( + esKuery.fromKueryExpression( '(type: foo and updatedAt: 5678654567) and foo.bytes > 1000 and foo.bytes < 8000 and foo.title: "best" and (foo.description: t* or foo.description :*)' ) ); @@ -88,7 +88,7 @@ describe('Filter Utils', () => { mockMappings ) ).toEqual( - fromKueryExpression( + esKuery.fromKueryExpression( '(type: foo and updatedAt: 5678654567) and foo.bytes > 1000 and foo.bytes < 8000 and foo.title: "best" and (foo.description: t* or foo.description :*)' ) ); @@ -102,7 +102,7 @@ describe('Filter Utils', () => { mockMappings ) ).toEqual( - fromKueryExpression( + esKuery.fromKueryExpression( '((type: bar and updatedAt: 5678654567) or (type: foo and updatedAt: 5678654567)) and foo.bytes > 1000 and foo.bytes < 8000 and foo.title: "best" and (foo.description: t* or bar.description :*)' ) ); @@ -130,7 +130,7 @@ describe('Filter Utils', () => { describe('#validateFilterKueryNode', () => { test('Validate filter query through KueryNode - happy path', () => { const validationObject = validateFilterKueryNode( - fromKueryExpression( + esKuery.fromKueryExpression( 'foo.updatedAt: 5678654567 and foo.attributes.bytes > 1000 and foo.attributes.bytes < 8000 and foo.attributes.title: "best" and (foo.attributes.description: t* or foo.attributes.description :*)' ), ['foo'], @@ -185,7 +185,7 @@ describe('Filter Utils', () => { test('Return Error if key is not wrapper by a saved object type', () => { const validationObject = validateFilterKueryNode( - fromKueryExpression( + esKuery.fromKueryExpression( 'updatedAt: 5678654567 and foo.attributes.bytes > 1000 and foo.attributes.bytes < 8000 and foo.attributes.title: "best" and (foo.attributes.description: t* or foo.attributes.description :*)' ), ['foo'], @@ -240,7 +240,7 @@ describe('Filter Utils', () => { test('Return Error if key of a saved object type is not wrapped with attributes', () => { const validationObject = validateFilterKueryNode( - fromKueryExpression( + esKuery.fromKueryExpression( 'foo.updatedAt: 5678654567 and foo.attributes.bytes > 1000 and foo.bytes < 8000 and foo.attributes.title: "best" and (foo.attributes.description: t* or foo.description :*)' ), ['foo'], @@ -297,7 +297,7 @@ describe('Filter Utils', () => { test('Return Error if filter is not using an allowed type', () => { const validationObject = validateFilterKueryNode( - fromKueryExpression( + esKuery.fromKueryExpression( 'bar.updatedAt: 5678654567 and foo.attributes.bytes > 1000 and foo.attributes.bytes < 8000 and foo.attributes.title: "best" and (foo.attributes.description: t* or foo.attributes.description :*)' ), ['foo'], @@ -352,7 +352,7 @@ describe('Filter Utils', () => { test('Return Error if filter is using an non-existing key in the index patterns of the saved object type', () => { const validationObject = validateFilterKueryNode( - fromKueryExpression( + esKuery.fromKueryExpression( 'foo.updatedAt33: 5678654567 and foo.attributes.bytes > 1000 and foo.attributes.bytes < 8000 and foo.attributes.header: "best" and (foo.attributes.description: t* or foo.attributes.description :*)' ), ['foo'], @@ -408,7 +408,7 @@ describe('Filter Utils', () => { test('Return Error if filter is using an non-existing key null key', () => { const validationObject = validateFilterKueryNode( - fromKueryExpression('foo.attributes.description: hello AND bye'), + esKuery.fromKueryExpression('foo.attributes.description: hello AND bye'), ['foo'], mockMappings ); diff --git a/src/core/server/saved_objects/service/lib/filter_utils.ts b/src/core/server/saved_objects/service/lib/filter_utils.ts index e331d3eff990f7..3cf499de541ee4 100644 --- a/src/core/server/saved_objects/service/lib/filter_utils.ts +++ b/src/core/server/saved_objects/service/lib/filter_utils.ts @@ -17,18 +17,18 @@ * under the License. */ -import { fromKueryExpression, KueryNode, nodeTypes } from '@kbn/es-query'; import { get, set } from 'lodash'; import { SavedObjectsErrorHelpers } from './errors'; import { IndexMapping } from '../../mappings'; +import { esKuery } from '../../../../../plugins/data/server'; export const validateConvertFilterToKueryNode = ( allowedTypes: string[], filter: string, indexMapping: IndexMapping -): KueryNode => { +): esKuery.KueryNode | undefined => { if (filter && filter.length > 0 && indexMapping) { - const filterKueryNode = fromKueryExpression(filter); + const filterKueryNode = esKuery.fromKueryExpression(filter); const validationFilterKuery = validateFilterKueryNode( filterKueryNode, @@ -54,7 +54,7 @@ export const validateConvertFilterToKueryNode = ( validationFilterKuery.forEach(item => { const path: string[] = item.astPath.length === 0 ? [] : item.astPath.split('.'); - const existingKueryNode: KueryNode = + const existingKueryNode: esKuery.KueryNode = path.length === 0 ? filterKueryNode : get(filterKueryNode, path); if (item.isSavedObjectAttr) { existingKueryNode.arguments[0].value = existingKueryNode.arguments[0].value.split('.')[1]; @@ -63,8 +63,8 @@ export const validateConvertFilterToKueryNode = ( set( filterKueryNode, path, - nodeTypes.function.buildNode('and', [ - nodeTypes.function.buildNode('is', 'type', itemType[0]), + esKuery.nodeTypes.function.buildNode('and', [ + esKuery.nodeTypes.function.buildNode('is', 'type', itemType[0]), existingKueryNode, ]) ); @@ -79,7 +79,6 @@ export const validateConvertFilterToKueryNode = ( }); return filterKueryNode; } - return null; }; interface ValidateFilterKueryNode { @@ -91,41 +90,44 @@ interface ValidateFilterKueryNode { } export const validateFilterKueryNode = ( - astFilter: KueryNode, + astFilter: esKuery.KueryNode, types: string[], indexMapping: IndexMapping, storeValue: boolean = false, path: string = 'arguments' ): ValidateFilterKueryNode[] => { - return astFilter.arguments.reduce((kueryNode: string[], ast: KueryNode, index: number) => { - if (ast.arguments) { - const myPath = `${path}.${index}`; - return [ - ...kueryNode, - ...validateFilterKueryNode( - ast, - types, - indexMapping, - ast.type === 'function' && ['is', 'range'].includes(ast.function), - `${myPath}.arguments` - ), - ]; - } - if (storeValue && index === 0) { - const splitPath = path.split('.'); - return [ - ...kueryNode, - { - astPath: splitPath.slice(0, splitPath.length - 1).join('.'), - error: hasFilterKeyError(ast.value, types, indexMapping), - isSavedObjectAttr: isSavedObjectAttr(ast.value, indexMapping), - key: ast.value, - type: getType(ast.value), - }, - ]; - } - return kueryNode; - }, []); + return astFilter.arguments.reduce( + (kueryNode: string[], ast: esKuery.KueryNode, index: number) => { + if (ast.arguments) { + const myPath = `${path}.${index}`; + return [ + ...kueryNode, + ...validateFilterKueryNode( + ast, + types, + indexMapping, + ast.type === 'function' && ['is', 'range'].includes(ast.function), + `${myPath}.arguments` + ), + ]; + } + if (storeValue && index === 0) { + const splitPath = path.split('.'); + return [ + ...kueryNode, + { + astPath: splitPath.slice(0, splitPath.length - 1).join('.'), + error: hasFilterKeyError(ast.value, types, indexMapping), + isSavedObjectAttr: isSavedObjectAttr(ast.value, indexMapping), + key: ast.value, + type: getType(ast.value), + }, + ]; + } + return kueryNode; + }, + [] + ); }; const getType = (key: string | undefined | null) => diff --git a/src/core/server/saved_objects/service/lib/repository.test.js b/src/core/server/saved_objects/service/lib/repository.test.js index 79a3e573ab98c9..3d81c2c2efd526 100644 --- a/src/core/server/saved_objects/service/lib/repository.test.js +++ b/src/core/server/saved_objects/service/lib/repository.test.js @@ -1289,8 +1289,7 @@ describe('SavedObjectsRepository', () => { type: 'foo', id: '1', }, - indexPattern: undefined, - kueryNode: null, + kueryNode: undefined, }; await savedObjectsRepository.find(relevantOpts); diff --git a/src/core/server/saved_objects/service/lib/repository.ts b/src/core/server/saved_objects/service/lib/repository.ts index 51d4a8ad50ad63..e8f1fb16461c1d 100644 --- a/src/core/server/saved_objects/service/lib/repository.ts +++ b/src/core/server/saved_objects/service/lib/repository.ts @@ -448,11 +448,11 @@ export class SavedObjectsRepository { } let kueryNode; + try { - kueryNode = - filter && filter !== '' - ? validateConvertFilterToKueryNode(allowedTypes, filter, this._mappings) - : null; + if (filter) { + kueryNode = validateConvertFilterToKueryNode(allowedTypes, filter, this._mappings); + } } catch (e) { if (e.name === 'KQLSyntaxError') { throw SavedObjectsErrorHelpers.createBadRequestError('KQLSyntaxError: ' + e.message); diff --git a/src/core/server/saved_objects/service/lib/search_dsl/query_params.ts b/src/core/server/saved_objects/service/lib/search_dsl/query_params.ts index bee35b899d83c5..cfeb258c2f03b4 100644 --- a/src/core/server/saved_objects/service/lib/search_dsl/query_params.ts +++ b/src/core/server/saved_objects/service/lib/search_dsl/query_params.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import { toElasticsearchQuery, KueryNode } from '@kbn/es-query'; +import { esKuery } from '../../../../../../plugins/data/server'; import { getRootPropertiesObjects, IndexMapping } from '../../../mappings'; import { SavedObjectsSchema } from '../../../schema'; @@ -91,7 +91,7 @@ interface QueryParams { searchFields?: string[]; defaultSearchOperator?: string; hasReference?: HasReferenceQueryParams; - kueryNode?: KueryNode; + kueryNode?: esKuery.KueryNode; } /** @@ -111,7 +111,7 @@ export function getQueryParams({ const types = getTypes(mappings, type); const bool: any = { filter: [ - ...(kueryNode != null ? [toElasticsearchQuery(kueryNode)] : []), + ...(kueryNode != null ? [esKuery.toElasticsearchQuery(kueryNode)] : []), { bool: { must: hasReference diff --git a/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.ts b/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.ts index 868ca51a76eab8..f2bbc3ef564a14 100644 --- a/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.ts +++ b/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.ts @@ -17,13 +17,13 @@ * under the License. */ -import { KueryNode } from '@kbn/es-query'; import Boom from 'boom'; import { IndexMapping } from '../../../mappings'; import { SavedObjectsSchema } from '../../../schema'; import { getQueryParams } from './query_params'; import { getSortingParams } from './sorting_params'; +import { esKuery } from '../../../../../../plugins/data/server'; interface GetSearchDslOptions { type: string | string[]; @@ -37,7 +37,7 @@ interface GetSearchDslOptions { type: string; id: string; }; - kueryNode?: KueryNode; + kueryNode?: esKuery.KueryNode; } export function getSearchDsl( diff --git a/src/legacy/core_plugins/data/public/query/query_bar/components/query_bar_top_row.tsx b/src/legacy/core_plugins/data/public/query/query_bar/components/query_bar_top_row.tsx index 1bf8ac086d3411..ed3c2413b0eb49 100644 --- a/src/legacy/core_plugins/data/public/query/query_bar/components/query_bar_top_row.tsx +++ b/src/legacy/core_plugins/data/public/query/query_bar/components/query_bar_top_row.tsx @@ -18,11 +18,8 @@ */ import dateMath from '@elastic/datemath'; -import { doesKueryExpressionHaveLuceneSyntaxError } from '@kbn/es-query'; - import classNames from 'classnames'; import React, { useState } from 'react'; - import { EuiButton, EuiFlexGroup, @@ -42,9 +39,9 @@ import { Query, PersistedLog, getQueryLog, + esKuery, } from '../../../../../../../plugins/data/public'; import { useKibana, toMountPoint } from '../../../../../../../plugins/kibana_react/public'; - import { IndexPattern } from '../../../index_patterns'; import { QueryBarInput } from './query_bar_input'; @@ -300,7 +297,7 @@ function QueryBarTopRowUI(props: Props) { language === 'kuery' && typeof query === 'string' && (!storage || !storage.get('kibana.luceneSyntaxWarningOptOut')) && - doesKueryExpressionHaveLuceneSyntaxError(query) + esKuery.doesKueryExpressionHaveLuceneSyntaxError(query) ) { const toast = notifications!.toasts.addWarning({ title: intl.formatMessage({ diff --git a/src/legacy/core_plugins/timelion/public/vis/timelion_request_handler.ts b/src/legacy/core_plugins/timelion/public/vis/timelion_request_handler.ts index 74111bf7948775..14cd3d0083e6ab 100644 --- a/src/legacy/core_plugins/timelion/public/vis/timelion_request_handler.ts +++ b/src/legacy/core_plugins/timelion/public/vis/timelion_request_handler.ts @@ -83,7 +83,7 @@ export function getTimelionRequestHandler(dependencies: TimelionVisualizationDep sheet: [expression], extended: { es: { - filter: esQuery.buildEsQuery(null, query, filters, esQueryConfigs), + filter: esQuery.buildEsQuery(undefined, query, filters, esQueryConfigs), }, }, time: { diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_editor.js b/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_editor.js index 2b42c22ad7c435..1d42b773369334 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_editor.js +++ b/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_editor.js @@ -22,7 +22,6 @@ import React, { Component } from 'react'; import * as Rx from 'rxjs'; import { share } from 'rxjs/operators'; import { isEqual, isEmpty, debounce } from 'lodash'; -import { fromKueryExpression } from '@kbn/es-query'; import { VisEditorVisualization } from './vis_editor_visualization'; import { Visualization } from './visualization'; import { VisPicker } from './vis_picker'; @@ -30,6 +29,7 @@ import { PanelConfig } from './panel_config'; import { createBrushHandler } from '../lib/create_brush_handler'; import { fetchFields } from '../lib/fetch_fields'; import { extractIndexPatterns } from '../../common/extract_index_patterns'; +import { esKuery } from '../../../../../plugins/data/public'; import { npStart } from 'ui/new_platform'; @@ -88,7 +88,7 @@ export class VisEditor extends Component { if (filterQuery && filterQuery.language === 'kuery') { try { const queryOptions = this.coreContext.uiSettings.get('query:allowLeadingWildcards'); - fromKueryExpression(filterQuery.query, { allowLeadingWildcards: queryOptions }); + esKuery.fromKueryExpression(filterQuery.query, { allowLeadingWildcards: queryOptions }); } catch (error) { return false; } diff --git a/src/legacy/core_plugins/vis_type_vega/public/vega_request_handler.ts b/src/legacy/core_plugins/vis_type_vega/public/vega_request_handler.ts index 83ae31bf874001..26380bf2b9d94f 100644 --- a/src/legacy/core_plugins/vis_type_vega/public/vega_request_handler.ts +++ b/src/legacy/core_plugins/vis_type_vega/public/vega_request_handler.ts @@ -49,7 +49,7 @@ export function createVegaRequestHandler({ timeCache.setTimeRange(timeRange); const esQueryConfigs = esQuery.getEsQueryConfig(uiSettings); - const filtersDsl = esQuery.buildEsQuery(null, query, filters, esQueryConfigs); + const filtersDsl = esQuery.buildEsQuery(undefined, query, filters, esQueryConfigs); const vp = new VegaParser(visParams.spec, searchCache, timeCache, filtersDsl, serviceSettings); return vp.parseAsync(); diff --git a/src/plugins/data/common/es_query/es_query/build_es_query.test.ts b/src/plugins/data/common/es_query/es_query/build_es_query.test.ts index 3db23051b6ced9..405754ffcb572d 100644 --- a/src/plugins/data/common/es_query/es_query/build_es_query.test.ts +++ b/src/plugins/data/common/es_query/es_query/build_es_query.test.ts @@ -18,7 +18,7 @@ */ import { buildEsQuery } from './build_es_query'; -import { fromKueryExpression, toElasticsearchQuery } from '@kbn/es-query'; +import { fromKueryExpression, toElasticsearchQuery } from '../kuery'; import { luceneStringToDsl } from './lucene_string_to_dsl'; import { decorateQuery } from './decorate_query'; import { IIndexPattern } from '../../index_patterns'; diff --git a/src/plugins/data/common/es_query/es_query/build_es_query.ts b/src/plugins/data/common/es_query/es_query/build_es_query.ts index b7544967936603..e4f5f1f9e216c8 100644 --- a/src/plugins/data/common/es_query/es_query/build_es_query.ts +++ b/src/plugins/data/common/es_query/es_query/build_es_query.ts @@ -41,7 +41,7 @@ export interface EsQueryConfig { * config contains dateformat:tz */ export function buildEsQuery( - indexPattern: IIndexPattern | null, + indexPattern: IIndexPattern | undefined, queries: Query | Query[], filters: Filter | Filter[], config: EsQueryConfig = { diff --git a/src/plugins/data/common/es_query/es_query/filter_matches_index.test.ts b/src/plugins/data/common/es_query/es_query/filter_matches_index.test.ts index 6e03c665290ae9..669c5a62af7263 100644 --- a/src/plugins/data/common/es_query/es_query/filter_matches_index.test.ts +++ b/src/plugins/data/common/es_query/es_query/filter_matches_index.test.ts @@ -31,9 +31,8 @@ describe('filterMatchesIndex', () => { it('should return true if no index pattern is passed', () => { const filter = { meta: { index: 'foo', key: 'bar' } } as Filter; - const indexPattern = null; - expect(filterMatchesIndex(filter, indexPattern)).toBe(true); + expect(filterMatchesIndex(filter, undefined)).toBe(true); }); it('should return true if the filter key matches a field name', () => { diff --git a/src/plugins/data/common/es_query/es_query/filter_matches_index.ts b/src/plugins/data/common/es_query/es_query/filter_matches_index.ts index 9b68f5088c4479..a9cd3d8b7ba268 100644 --- a/src/plugins/data/common/es_query/es_query/filter_matches_index.ts +++ b/src/plugins/data/common/es_query/es_query/filter_matches_index.ts @@ -25,7 +25,7 @@ import { Filter } from '../filters'; * this to check if `filter.meta.index` matches `indexPattern.id` instead, but that's a breaking * change. */ -export function filterMatchesIndex(filter: Filter, indexPattern: IIndexPattern | null) { +export function filterMatchesIndex(filter: Filter, indexPattern?: IIndexPattern | null) { if (!filter.meta?.key || !indexPattern) { return true; } diff --git a/src/plugins/data/common/es_query/es_query/from_filters.ts b/src/plugins/data/common/es_query/es_query/from_filters.ts index 1e0957d8165908..e33040485bf47d 100644 --- a/src/plugins/data/common/es_query/es_query/from_filters.ts +++ b/src/plugins/data/common/es_query/es_query/from_filters.ts @@ -54,7 +54,7 @@ const translateToQuery = (filter: Filter) => { export const buildQueryFromFilters = ( filters: Filter[] = [], - indexPattern: IIndexPattern | null, + indexPattern: IIndexPattern | undefined, ignoreFilterIfFieldNotInIndex: boolean = false ) => { filters = filters.filter(filter => filter && !isFilterDisabled(filter)); diff --git a/src/plugins/data/common/es_query/es_query/from_kuery.test.ts b/src/plugins/data/common/es_query/es_query/from_kuery.test.ts index 000815b51f6208..4574cd5ffd0cbd 100644 --- a/src/plugins/data/common/es_query/es_query/from_kuery.test.ts +++ b/src/plugins/data/common/es_query/es_query/from_kuery.test.ts @@ -18,7 +18,7 @@ */ import { buildQueryFromKuery } from './from_kuery'; -import { fromKueryExpression, toElasticsearchQuery } from '@kbn/es-query'; +import { fromKueryExpression, toElasticsearchQuery } from '../kuery'; import { IIndexPattern } from '../../index_patterns'; import { fields } from '../../index_patterns/mocks'; import { Query } from '../../query/types'; @@ -30,7 +30,7 @@ describe('build query', () => { describe('buildQueryFromKuery', () => { test('should return the parameters of an Elasticsearch bool query', () => { - const result = buildQueryFromKuery(null, [], true); + const result = buildQueryFromKuery(undefined, [], true); const expected = { must: [], filter: [], diff --git a/src/plugins/data/common/es_query/es_query/from_kuery.ts b/src/plugins/data/common/es_query/es_query/from_kuery.ts index f91c3d97b95b43..f4ec0fe0b34c5e 100644 --- a/src/plugins/data/common/es_query/es_query/from_kuery.ts +++ b/src/plugins/data/common/es_query/es_query/from_kuery.ts @@ -17,12 +17,12 @@ * under the License. */ -import { fromKueryExpression, toElasticsearchQuery, nodeTypes, KueryNode } from '@kbn/es-query'; +import { fromKueryExpression, toElasticsearchQuery, nodeTypes, KueryNode } from '../kuery'; import { IIndexPattern } from '../../index_patterns'; import { Query } from '../../query/types'; export function buildQueryFromKuery( - indexPattern: IIndexPattern | null, + indexPattern: IIndexPattern | undefined, queries: Query[] = [], allowLeadingWildcards: boolean = false, dateFormatTZ?: string @@ -35,22 +35,20 @@ export function buildQueryFromKuery( } function buildQuery( - indexPattern: IIndexPattern | null, + indexPattern: IIndexPattern | undefined, queryASTs: KueryNode[], config: Record = {} ) { - const compoundQueryAST: KueryNode = nodeTypes.function.buildNode('and', queryASTs); - const kueryQuery: Record = toElasticsearchQuery( - compoundQueryAST, - indexPattern, - config - ); + const compoundQueryAST = nodeTypes.function.buildNode('and', queryASTs); + const kueryQuery = toElasticsearchQuery(compoundQueryAST, indexPattern, config); - return { - must: [], - filter: [], - should: [], - must_not: [], - ...kueryQuery.bool, - }; + return Object.assign( + { + must: [], + filter: [], + should: [], + must_not: [], + }, + kueryQuery.bool + ); } diff --git a/src/plugins/data/common/es_query/es_query/migrate_filter.test.ts b/src/plugins/data/common/es_query/es_query/migrate_filter.test.ts index 4617ee1a1c43d3..e01240da87543b 100644 --- a/src/plugins/data/common/es_query/es_query/migrate_filter.test.ts +++ b/src/plugins/data/common/es_query/es_query/migrate_filter.test.ts @@ -40,7 +40,7 @@ describe('migrateFilter', function() { } as unknown) as PhraseFilter; it('should migrate match filters of type phrase', function() { - const migratedFilter = migrateFilter(oldMatchPhraseFilter, null); + const migratedFilter = migrateFilter(oldMatchPhraseFilter, undefined); expect(isEqual(migratedFilter, newMatchPhraseFilter)).toBe(true); }); @@ -48,7 +48,7 @@ describe('migrateFilter', function() { it('should not modify the original filter', function() { const oldMatchPhraseFilterCopy = clone(oldMatchPhraseFilter, true); - migrateFilter(oldMatchPhraseFilter, null); + migrateFilter(oldMatchPhraseFilter, undefined); expect(isEqual(oldMatchPhraseFilter, oldMatchPhraseFilterCopy)).toBe(true); }); @@ -57,7 +57,7 @@ describe('migrateFilter', function() { const originalFilter = { match_all: {}, } as MatchAllFilter; - const migratedFilter = migrateFilter(originalFilter, null); + const migratedFilter = migrateFilter(originalFilter, undefined); expect(migratedFilter).toBe(originalFilter); expect(isEqual(migratedFilter, originalFilter)).toBe(true); diff --git a/src/plugins/data/common/es_query/es_query/migrate_filter.ts b/src/plugins/data/common/es_query/es_query/migrate_filter.ts index 258ab9e7031319..fdc40768ebe41c 100644 --- a/src/plugins/data/common/es_query/es_query/migrate_filter.ts +++ b/src/plugins/data/common/es_query/es_query/migrate_filter.ts @@ -43,7 +43,7 @@ function isMatchPhraseFilter(filter: any): filter is DeprecatedMatchPhraseFilter return Boolean(fieldName && get(filter, ['match', fieldName, 'type']) === 'phrase'); } -export function migrateFilter(filter: Filter, indexPattern: IIndexPattern | null) { +export function migrateFilter(filter: Filter, indexPattern?: IIndexPattern) { if (isMatchPhraseFilter(filter)) { const fieldName = Object.keys(filter.match)[0]; const params: Record = get(filter, ['match', fieldName]); diff --git a/src/plugins/data/common/es_query/index.ts b/src/plugins/data/common/es_query/index.ts index 56eb45c4b1dcaf..937fe09903b6bc 100644 --- a/src/plugins/data/common/es_query/index.ts +++ b/src/plugins/data/common/es_query/index.ts @@ -18,6 +18,7 @@ */ import * as esQuery from './es_query'; import * as esFilters from './filters'; +import * as esKuery from './kuery'; import * as utils from './utils'; -export { esFilters, esQuery, utils }; +export { esFilters, esQuery, utils, esKuery }; diff --git a/packages/kbn-es-query/src/kuery/ast/kuery.js b/src/plugins/data/common/es_query/kuery/ast/_generated_/kuery.js similarity index 100% rename from packages/kbn-es-query/src/kuery/ast/kuery.js rename to src/plugins/data/common/es_query/kuery/ast/_generated_/kuery.js diff --git a/src/plugins/data/common/es_query/kuery/ast/ast.test.ts b/src/plugins/data/common/es_query/kuery/ast/ast.test.ts new file mode 100644 index 00000000000000..e441420760475c --- /dev/null +++ b/src/plugins/data/common/es_query/kuery/ast/ast.test.ts @@ -0,0 +1,421 @@ +/* + * 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 { + fromKueryExpression, + fromLiteralExpression, + toElasticsearchQuery, + doesKueryExpressionHaveLuceneSyntaxError, +} from './ast'; +import { nodeTypes } from '../node_types'; +import { fields } from '../../../index_patterns/mocks'; +import { IIndexPattern } from '../../../index_patterns'; +import { KueryNode } from '../types'; + +describe('kuery AST API', () => { + let indexPattern: IIndexPattern; + + beforeEach(() => { + indexPattern = ({ + fields, + } as unknown) as IIndexPattern; + }); + + describe('fromKueryExpression', () => { + test('should return a match all "is" function for whitespace', () => { + const expected = nodeTypes.function.buildNode('is', '*', '*'); + const actual = fromKueryExpression(' '); + expect(actual).toEqual(expected); + }); + + test('should return an "is" function with a null field for single literals', () => { + const expected = nodeTypes.function.buildNode('is', null, 'foo'); + const actual = fromKueryExpression('foo'); + expect(actual).toEqual(expected); + }); + + test('should ignore extraneous whitespace at the beginning and end of the query', () => { + const expected = nodeTypes.function.buildNode('is', null, 'foo'); + const actual = fromKueryExpression(' foo '); + expect(actual).toEqual(expected); + }); + + test('should not split on whitespace', () => { + const expected = nodeTypes.function.buildNode('is', null, 'foo bar'); + const actual = fromKueryExpression('foo bar'); + expect(actual).toEqual(expected); + }); + + test('should support "and" as a binary operator', () => { + const expected = nodeTypes.function.buildNode('and', [ + nodeTypes.function.buildNode('is', null, 'foo'), + nodeTypes.function.buildNode('is', null, 'bar'), + ]); + const actual = fromKueryExpression('foo and bar'); + expect(actual).toEqual(expected); + }); + + test('should support "or" as a binary operator', () => { + const expected = nodeTypes.function.buildNode('or', [ + nodeTypes.function.buildNode('is', null, 'foo'), + nodeTypes.function.buildNode('is', null, 'bar'), + ]); + const actual = fromKueryExpression('foo or bar'); + expect(actual).toEqual(expected); + }); + + test('should support negation of queries with a "not" prefix', () => { + const expected = nodeTypes.function.buildNode( + 'not', + nodeTypes.function.buildNode('or', [ + nodeTypes.function.buildNode('is', null, 'foo'), + nodeTypes.function.buildNode('is', null, 'bar'), + ]) + ); + const actual = fromKueryExpression('not (foo or bar)'); + expect(actual).toEqual(expected); + }); + + test('"and" should have a higher precedence than "or"', () => { + const expected = nodeTypes.function.buildNode('or', [ + nodeTypes.function.buildNode('is', null, 'foo'), + nodeTypes.function.buildNode('or', [ + nodeTypes.function.buildNode('and', [ + nodeTypes.function.buildNode('is', null, 'bar'), + nodeTypes.function.buildNode('is', null, 'baz'), + ]), + nodeTypes.function.buildNode('is', null, 'qux'), + ]), + ]); + const actual = fromKueryExpression('foo or bar and baz or qux'); + expect(actual).toEqual(expected); + }); + + test('should support grouping to override default precedence', () => { + const expected = nodeTypes.function.buildNode('and', [ + nodeTypes.function.buildNode('or', [ + nodeTypes.function.buildNode('is', null, 'foo'), + nodeTypes.function.buildNode('is', null, 'bar'), + ]), + nodeTypes.function.buildNode('is', null, 'baz'), + ]); + const actual = fromKueryExpression('(foo or bar) and baz'); + expect(actual).toEqual(expected); + }); + + test('should support matching against specific fields', () => { + const expected = nodeTypes.function.buildNode('is', 'foo', 'bar'); + const actual = fromKueryExpression('foo:bar'); + expect(actual).toEqual(expected); + }); + + test('should also not split on whitespace when matching specific fields', () => { + const expected = nodeTypes.function.buildNode('is', 'foo', 'bar baz'); + const actual = fromKueryExpression('foo:bar baz'); + expect(actual).toEqual(expected); + }); + + test('should treat quoted values as phrases', () => { + const expected = nodeTypes.function.buildNode('is', 'foo', 'bar baz', true); + const actual = fromKueryExpression('foo:"bar baz"'); + expect(actual).toEqual(expected); + }); + + test('should support a shorthand for matching multiple values against a single field', () => { + const expected = nodeTypes.function.buildNode('or', [ + nodeTypes.function.buildNode('is', 'foo', 'bar'), + nodeTypes.function.buildNode('is', 'foo', 'baz'), + ]); + const actual = fromKueryExpression('foo:(bar or baz)'); + expect(actual).toEqual(expected); + }); + + test('should support "and" and "not" operators and grouping in the shorthand as well', () => { + const expected = nodeTypes.function.buildNode('and', [ + nodeTypes.function.buildNode('or', [ + nodeTypes.function.buildNode('is', 'foo', 'bar'), + nodeTypes.function.buildNode('is', 'foo', 'baz'), + ]), + nodeTypes.function.buildNode('not', nodeTypes.function.buildNode('is', 'foo', 'qux')), + ]); + const actual = fromKueryExpression('foo:((bar or baz) and not qux)'); + expect(actual).toEqual(expected); + }); + + test('should support exclusive range operators', () => { + const expected = nodeTypes.function.buildNode('and', [ + nodeTypes.function.buildNode('range', 'bytes', { + gt: 1000, + }), + nodeTypes.function.buildNode('range', 'bytes', { + lt: 8000, + }), + ]); + const actual = fromKueryExpression('bytes > 1000 and bytes < 8000'); + expect(actual).toEqual(expected); + }); + + test('should support inclusive range operators', () => { + const expected = nodeTypes.function.buildNode('and', [ + nodeTypes.function.buildNode('range', 'bytes', { + gte: 1000, + }), + nodeTypes.function.buildNode('range', 'bytes', { + lte: 8000, + }), + ]); + const actual = fromKueryExpression('bytes >= 1000 and bytes <= 8000'); + expect(actual).toEqual(expected); + }); + + test('should support wildcards in field names', () => { + const expected = nodeTypes.function.buildNode('is', 'machine*', 'osx'); + const actual = fromKueryExpression('machine*:osx'); + expect(actual).toEqual(expected); + }); + + test('should support wildcards in values', () => { + const expected = nodeTypes.function.buildNode('is', 'foo', 'ba*'); + const actual = fromKueryExpression('foo:ba*'); + expect(actual).toEqual(expected); + }); + + test('should create an exists "is" query when a field is given and "*" is the value', () => { + const expected = nodeTypes.function.buildNode('is', 'foo', '*'); + const actual = fromKueryExpression('foo:*'); + expect(actual).toEqual(expected); + }); + + test('should support nested queries indicated by curly braces', () => { + const expected = nodeTypes.function.buildNode( + 'nested', + 'nestedField', + nodeTypes.function.buildNode('is', 'childOfNested', 'foo') + ); + const actual = fromKueryExpression('nestedField:{ childOfNested: foo }'); + expect(actual).toEqual(expected); + }); + + test('should support nested subqueries and subqueries inside nested queries', () => { + const expected = nodeTypes.function.buildNode('and', [ + nodeTypes.function.buildNode('is', 'response', '200'), + nodeTypes.function.buildNode( + 'nested', + 'nestedField', + nodeTypes.function.buildNode('or', [ + nodeTypes.function.buildNode('is', 'childOfNested', 'foo'), + nodeTypes.function.buildNode('is', 'childOfNested', 'bar'), + ]) + ), + ]); + const actual = fromKueryExpression( + 'response:200 and nestedField:{ childOfNested:foo or childOfNested:bar }' + ); + expect(actual).toEqual(expected); + }); + + test('should support nested sub-queries inside paren groups', () => { + const expected = nodeTypes.function.buildNode('and', [ + nodeTypes.function.buildNode('is', 'response', '200'), + nodeTypes.function.buildNode('or', [ + nodeTypes.function.buildNode( + 'nested', + 'nestedField', + nodeTypes.function.buildNode('is', 'childOfNested', 'foo') + ), + nodeTypes.function.buildNode( + 'nested', + 'nestedField', + nodeTypes.function.buildNode('is', 'childOfNested', 'bar') + ), + ]), + ]); + const actual = fromKueryExpression( + 'response:200 and ( nestedField:{ childOfNested:foo } or nestedField:{ childOfNested:bar } )' + ); + expect(actual).toEqual(expected); + }); + + test('should support nested groups inside other nested groups', () => { + const expected = nodeTypes.function.buildNode( + 'nested', + 'nestedField', + nodeTypes.function.buildNode( + 'nested', + 'nestedChild', + nodeTypes.function.buildNode('is', 'doublyNestedChild', 'foo') + ) + ); + const actual = fromKueryExpression('nestedField:{ nestedChild:{ doublyNestedChild:foo } }'); + expect(actual).toEqual(expected); + }); + }); + + describe('fromLiteralExpression', () => { + test('should create literal nodes for unquoted values with correct primitive types', () => { + const stringLiteral = nodeTypes.literal.buildNode('foo'); + const booleanFalseLiteral = nodeTypes.literal.buildNode(false); + const booleanTrueLiteral = nodeTypes.literal.buildNode(true); + const numberLiteral = nodeTypes.literal.buildNode(42); + + expect(fromLiteralExpression('foo')).toEqual(stringLiteral); + expect(fromLiteralExpression('true')).toEqual(booleanTrueLiteral); + expect(fromLiteralExpression('false')).toEqual(booleanFalseLiteral); + expect(fromLiteralExpression('42')).toEqual(numberLiteral); + }); + + test('should allow escaping of special characters with a backslash', () => { + const expected = nodeTypes.literal.buildNode('\\():<>"*'); + // yo dawg + const actual = fromLiteralExpression('\\\\\\(\\)\\:\\<\\>\\"\\*'); + expect(actual).toEqual(expected); + }); + + test('should support double quoted strings that do not need escapes except for quotes', () => { + const expected = nodeTypes.literal.buildNode('\\():<>"*'); + const actual = fromLiteralExpression('"\\():<>\\"*"'); + expect(actual).toEqual(expected); + }); + + test('should support escaped backslashes inside quoted strings', () => { + const expected = nodeTypes.literal.buildNode('\\'); + const actual = fromLiteralExpression('"\\\\"'); + expect(actual).toEqual(expected); + }); + + test('should detect wildcards and build wildcard AST nodes', () => { + const expected = nodeTypes.wildcard.buildNode('foo*bar'); + const actual = fromLiteralExpression('foo*bar'); + expect(actual).toEqual(expected); + }); + }); + + describe('toElasticsearchQuery', () => { + test("should return the given node type's ES query representation", () => { + const node = nodeTypes.function.buildNode('exists', 'response'); + const expected = nodeTypes.function.toElasticsearchQuery(node, indexPattern); + const result = toElasticsearchQuery(node, indexPattern); + expect(result).toEqual(expected); + }); + + test('should return an empty "and" function for undefined nodes and unknown node types', () => { + const expected = nodeTypes.function.toElasticsearchQuery( + nodeTypes.function.buildNode('and', []), + indexPattern + ); + + expect(toElasticsearchQuery((null as unknown) as KueryNode, undefined)).toEqual(expected); + + const noTypeNode = nodeTypes.function.buildNode('exists', 'foo'); + delete noTypeNode.type; + expect(toElasticsearchQuery(noTypeNode)).toEqual(expected); + + const unknownTypeNode = nodeTypes.function.buildNode('exists', 'foo'); + + // @ts-ignore + unknownTypeNode.type = 'notValid'; + expect(toElasticsearchQuery(unknownTypeNode)).toEqual(expected); + }); + + test("should return the given node type's ES query representation including a time zone parameter when one is provided", () => { + const config = { dateFormatTZ: 'America/Phoenix' }; + const node = nodeTypes.function.buildNode('is', '@timestamp', '"2018-04-03T19:04:17"'); + const expected = nodeTypes.function.toElasticsearchQuery(node, indexPattern, config); + const result = toElasticsearchQuery(node, indexPattern, config); + expect(result).toEqual(expected); + }); + }); + + describe('doesKueryExpressionHaveLuceneSyntaxError', () => { + test('should return true for Lucene ranges', () => { + const result = doesKueryExpressionHaveLuceneSyntaxError('bar: [1 TO 10]'); + expect(result).toEqual(true); + }); + + test('should return false for KQL ranges', () => { + const result = doesKueryExpressionHaveLuceneSyntaxError('bar < 1'); + expect(result).toEqual(false); + }); + + test('should return true for Lucene exists', () => { + const result = doesKueryExpressionHaveLuceneSyntaxError('_exists_: bar'); + expect(result).toEqual(true); + }); + + test('should return false for KQL exists', () => { + const result = doesKueryExpressionHaveLuceneSyntaxError('bar:*'); + expect(result).toEqual(false); + }); + + test('should return true for Lucene wildcards', () => { + const result = doesKueryExpressionHaveLuceneSyntaxError('bar: ba?'); + expect(result).toEqual(true); + }); + + test('should return false for KQL wildcards', () => { + const result = doesKueryExpressionHaveLuceneSyntaxError('bar: ba*'); + expect(result).toEqual(false); + }); + + test('should return true for Lucene regex', () => { + const result = doesKueryExpressionHaveLuceneSyntaxError('bar: /ba.*/'); + expect(result).toEqual(true); + }); + + test('should return true for Lucene fuzziness', () => { + const result = doesKueryExpressionHaveLuceneSyntaxError('bar: ba~'); + expect(result).toEqual(true); + }); + + test('should return true for Lucene proximity', () => { + const result = doesKueryExpressionHaveLuceneSyntaxError('bar: "ba"~2'); + expect(result).toEqual(true); + }); + + test('should return true for Lucene boosting', () => { + const result = doesKueryExpressionHaveLuceneSyntaxError('bar: ba^2'); + expect(result).toEqual(true); + }); + + test('should return true for Lucene + operator', () => { + const result = doesKueryExpressionHaveLuceneSyntaxError('+foo: bar'); + expect(result).toEqual(true); + }); + + test('should return true for Lucene - operators', () => { + const result = doesKueryExpressionHaveLuceneSyntaxError('-foo: bar'); + expect(result).toEqual(true); + }); + + test('should return true for Lucene && operators', () => { + const result = doesKueryExpressionHaveLuceneSyntaxError('foo: bar && baz: qux'); + expect(result).toEqual(true); + }); + + test('should return true for Lucene || operators', () => { + const result = doesKueryExpressionHaveLuceneSyntaxError('foo: bar || baz: qux'); + expect(result).toEqual(true); + }); + + test('should return true for mixed KQL/Lucene queries', () => { + const result = doesKueryExpressionHaveLuceneSyntaxError('foo: bar and (baz: qux || bag)'); + expect(result).toEqual(true); + }); + }); +}); diff --git a/packages/kbn-es-query/src/kuery/ast/ast.js b/src/plugins/data/common/es_query/kuery/ast/ast.ts similarity index 53% rename from packages/kbn-es-query/src/kuery/ast/ast.js rename to src/plugins/data/common/es_query/kuery/ast/ast.ts index 1688995d46f80e..253f4326179725 100644 --- a/packages/kbn-es-query/src/kuery/ast/ast.js +++ b/src/plugins/data/common/es_query/kuery/ast/ast.ts @@ -17,21 +17,44 @@ * under the License. */ -import _ from 'lodash'; import { nodeTypes } from '../node_types/index'; -import { parse as parseKuery } from './kuery'; -import { KQLSyntaxError } from '../errors'; +import { KQLSyntaxError } from '../kuery_syntax_error'; +import { KueryNode, JsonObject, DslQuery, KueryParseOptions } from '../types'; +import { IIndexPattern } from '../../../index_patterns/types'; -export function fromLiteralExpression(expression, parseOptions) { - parseOptions = { - ...parseOptions, - startRule: 'Literal', - }; +// @ts-ignore +import { parse as parseKuery } from './_generated_/kuery'; - return fromExpression(expression, parseOptions, parseKuery); -} +const fromExpression = ( + expression: string | DslQuery, + parseOptions: Partial = {}, + parse: Function = parseKuery +): KueryNode => { + if (typeof expression === 'undefined') { + throw new Error('expression must be a string, got undefined instead'); + } + + return parse(expression, { ...parseOptions, helpers: { nodeTypes } }); +}; + +export const fromLiteralExpression = ( + expression: string | DslQuery, + parseOptions: Partial = {} +): KueryNode => { + return fromExpression( + expression, + { + ...parseOptions, + startRule: 'Literal', + }, + parseKuery + ); +}; -export function fromKueryExpression(expression, parseOptions) { +export const fromKueryExpression = ( + expression: string | DslQuery, + parseOptions: Partial = {} +): KueryNode => { try { return fromExpression(expression, parseOptions, parseKuery); } catch (error) { @@ -41,20 +64,18 @@ export function fromKueryExpression(expression, parseOptions) { throw error; } } -} +}; -function fromExpression(expression, parseOptions = {}, parse = parseKuery) { - if (_.isUndefined(expression)) { - throw new Error('expression must be a string, got undefined instead'); +export const doesKueryExpressionHaveLuceneSyntaxError = ( + expression: string | DslQuery +): boolean => { + try { + fromExpression(expression, { errorOnLuceneSyntax: true }, parseKuery); + return false; + } catch (e) { + return e.message.startsWith('Lucene'); } - - parseOptions = { - ...parseOptions, - helpers: { nodeTypes }, - }; - - return parse(expression, parseOptions); -} +}; /** * @params {String} indexPattern @@ -63,19 +84,17 @@ function fromExpression(expression, parseOptions = {}, parse = parseKuery) { * IndexPattern isn't required, but if you pass one in, we can be more intelligent * about how we craft the queries (e.g. scripted fields) */ -export function toElasticsearchQuery(node, indexPattern, config = {}, context = {}) { +export const toElasticsearchQuery = ( + node: KueryNode, + indexPattern?: IIndexPattern, + config?: Record, + context?: Record +): JsonObject => { if (!node || !node.type || !nodeTypes[node.type]) { - return toElasticsearchQuery(nodeTypes.function.buildNode('and', [])); + return toElasticsearchQuery(nodeTypes.function.buildNode('and', []), indexPattern); } - return nodeTypes[node.type].toElasticsearchQuery(node, indexPattern, config, context); -} + const nodeType = (nodeTypes[node.type] as unknown) as any; -export function doesKueryExpressionHaveLuceneSyntaxError(expression) { - try { - fromExpression(expression, { errorOnLuceneSyntax: true }, parseKuery); - return false; - } catch (e) { - return (e.message.startsWith('Lucene')); - } -} + return nodeType.toElasticsearchQuery(node, indexPattern, config, context); +}; diff --git a/packages/kbn-es-query/src/kuery/ast/index.js b/src/plugins/data/common/es_query/kuery/ast/index.ts similarity index 100% rename from packages/kbn-es-query/src/kuery/ast/index.js rename to src/plugins/data/common/es_query/kuery/ast/index.ts diff --git a/packages/kbn-es-query/src/kuery/ast/kuery.peg b/src/plugins/data/common/es_query/kuery/ast/kuery.peg similarity index 100% rename from packages/kbn-es-query/src/kuery/ast/kuery.peg rename to src/plugins/data/common/es_query/kuery/ast/kuery.peg diff --git a/packages/kbn-es-query/src/kuery/functions/and.js b/src/plugins/data/common/es_query/kuery/functions/and.js similarity index 100% rename from packages/kbn-es-query/src/kuery/functions/and.js rename to src/plugins/data/common/es_query/kuery/functions/and.js diff --git a/packages/kbn-es-query/src/kuery/functions/__tests__/and.js b/src/plugins/data/common/es_query/kuery/functions/and.test.ts similarity index 50% rename from packages/kbn-es-query/src/kuery/functions/__tests__/and.js rename to src/plugins/data/common/es_query/kuery/functions/and.test.ts index 07289a878e8c1e..133e691b27dbad 100644 --- a/packages/kbn-es-query/src/kuery/functions/__tests__/and.js +++ b/src/plugins/data/common/es_query/kuery/functions/and.test.ts @@ -17,43 +17,53 @@ * under the License. */ -import expect from '@kbn/expect'; -import * as and from '../and'; -import { nodeTypes } from '../../node_types'; -import * as ast from '../../ast'; -import indexPatternResponse from '../../../__fixtures__/index_pattern_response.json'; +import { nodeTypes } from '../node_types'; +import { fields } from '../../../index_patterns/mocks'; +import { IIndexPattern } from '../../../index_patterns'; +import * as ast from '../ast'; -let indexPattern; +// @ts-ignore +import * as and from './and'; const childNode1 = nodeTypes.function.buildNode('is', 'machine.os', 'osx'); const childNode2 = nodeTypes.function.buildNode('is', 'extension', 'jpg'); -describe('kuery functions', function () { - describe('and', function () { +describe('kuery functions', () => { + describe('and', () => { + let indexPattern: IIndexPattern; beforeEach(() => { - indexPattern = indexPatternResponse; + indexPattern = ({ + fields, + } as unknown) as IIndexPattern; }); - describe('buildNodeParams', function () { - - it('arguments should contain the unmodified child nodes', function () { + describe('buildNodeParams', () => { + test('arguments should contain the unmodified child nodes', () => { const result = and.buildNodeParams([childNode1, childNode2]); - const { arguments: [ actualChildNode1, actualChildNode2 ] } = result; - expect(actualChildNode1).to.be(childNode1); - expect(actualChildNode2).to.be(childNode2); + const { + arguments: [actualChildNode1, actualChildNode2], + } = result; + + expect(actualChildNode1).toBe(childNode1); + expect(actualChildNode2).toBe(childNode2); }); }); - describe('toElasticsearchQuery', function () { - - it('should wrap subqueries in an ES bool query\'s filter clause', function () { + describe('toElasticsearchQuery', () => { + test("should wrap subqueries in an ES bool query's filter clause", () => { const node = nodeTypes.function.buildNode('and', [childNode1, childNode2]); const result = and.toElasticsearchQuery(node, indexPattern); - expect(result).to.only.have.keys('bool'); - expect(result.bool).to.only.have.keys('filter'); - expect(result.bool.filter).to.eql( - [childNode1, childNode2].map((childNode) => ast.toElasticsearchQuery(childNode, indexPattern)) + + expect(result).toHaveProperty('bool'); + expect(Object.keys(result).length).toBe(1); + expect(result.bool).toHaveProperty('filter'); + expect(Object.keys(result.bool).length).toBe(1); + + expect(result.bool.filter).toEqual( + [childNode1, childNode2].map(childNode => + ast.toElasticsearchQuery(childNode, indexPattern) + ) ); }); }); diff --git a/packages/kbn-es-query/src/kuery/functions/exists.js b/src/plugins/data/common/es_query/kuery/functions/exists.js similarity index 100% rename from packages/kbn-es-query/src/kuery/functions/exists.js rename to src/plugins/data/common/es_query/kuery/functions/exists.js diff --git a/packages/kbn-es-query/src/kuery/functions/__tests__/exists.js b/src/plugins/data/common/es_query/kuery/functions/exists.test.ts similarity index 51% rename from packages/kbn-es-query/src/kuery/functions/__tests__/exists.js rename to src/plugins/data/common/es_query/kuery/functions/exists.test.ts index ee4cfab94e614d..8443436cf4cfb1 100644 --- a/packages/kbn-es-query/src/kuery/functions/__tests__/exists.js +++ b/src/plugins/data/common/es_query/kuery/functions/exists.test.ts @@ -17,67 +17,73 @@ * under the License. */ -import expect from '@kbn/expect'; -import * as exists from '../exists'; -import { nodeTypes } from '../../node_types'; -import _ from 'lodash'; -import indexPatternResponse from '../../../__fixtures__/index_pattern_response.json'; +import { nodeTypes } from '../node_types'; +import { fields } from '../../../index_patterns/mocks'; +import { IIndexPattern } from '../../../index_patterns'; +// @ts-ignore +import * as exists from './exists'; -let indexPattern; - -describe('kuery functions', function () { - describe('exists', function () { +describe('kuery functions', () => { + describe('exists', () => { + let indexPattern: IIndexPattern; beforeEach(() => { - indexPattern = indexPatternResponse; + indexPattern = ({ + fields, + } as unknown) as IIndexPattern; }); - describe('buildNodeParams', function () { - it('should return a single "arguments" param', function () { + describe('buildNodeParams', () => { + test('should return a single "arguments" param', () => { const result = exists.buildNodeParams('response'); - expect(result).to.only.have.key('arguments'); + + expect(result).toHaveProperty('arguments'); + expect(Object.keys(result).length).toBe(1); }); - it('arguments should contain the provided fieldName as a literal', function () { - const { arguments: [ arg ] } = exists.buildNodeParams('response'); - expect(arg).to.have.property('type', 'literal'); - expect(arg).to.have.property('value', 'response'); + test('arguments should contain the provided fieldName as a literal', () => { + const { + arguments: [arg], + } = exists.buildNodeParams('response'); + + expect(arg).toHaveProperty('type', 'literal'); + expect(arg).toHaveProperty('value', 'response'); }); }); - describe('toElasticsearchQuery', function () { - it('should return an ES exists query', function () { + describe('toElasticsearchQuery', () => { + test('should return an ES exists query', () => { const expected = { - exists: { field: 'response' } + exists: { field: 'response' }, }; - const existsNode = nodeTypes.function.buildNode('exists', 'response'); const result = exists.toElasticsearchQuery(existsNode, indexPattern); - expect(_.isEqual(expected, result)).to.be(true); + + expect(expected).toEqual(result); }); - it('should return an ES exists query without an index pattern', function () { + test('should return an ES exists query without an index pattern', () => { const expected = { - exists: { field: 'response' } + exists: { field: 'response' }, }; - const existsNode = nodeTypes.function.buildNode('exists', 'response'); const result = exists.toElasticsearchQuery(existsNode); - expect(_.isEqual(expected, result)).to.be(true); + + expect(expected).toEqual(result); }); - it('should throw an error for scripted fields', function () { + test('should throw an error for scripted fields', () => { const existsNode = nodeTypes.function.buildNode('exists', 'script string'); - expect(exists.toElasticsearchQuery) - .withArgs(existsNode, indexPattern).to.throwException(/Exists query does not support scripted fields/); + expect(() => exists.toElasticsearchQuery(existsNode, indexPattern)).toThrowError( + /Exists query does not support scripted fields/ + ); }); - it('should use a provided nested context to create a full field name', function () { + test('should use a provided nested context to create a full field name', () => { const expected = { - exists: { field: 'nestedField.response' } + exists: { field: 'nestedField.response' }, }; - const existsNode = nodeTypes.function.buildNode('exists', 'response'); const result = exists.toElasticsearchQuery( existsNode, @@ -85,7 +91,8 @@ describe('kuery functions', function () { {}, { nested: { path: 'nestedField' } } ); - expect(_.isEqual(expected, result)).to.be(true); + + expect(expected).toEqual(result); }); }); }); diff --git a/packages/kbn-es-query/src/kuery/functions/geo_bounding_box.js b/src/plugins/data/common/es_query/kuery/functions/geo_bounding_box.js similarity index 100% rename from packages/kbn-es-query/src/kuery/functions/geo_bounding_box.js rename to src/plugins/data/common/es_query/kuery/functions/geo_bounding_box.js diff --git a/src/plugins/data/common/es_query/kuery/functions/geo_bounding_box.test.ts b/src/plugins/data/common/es_query/kuery/functions/geo_bounding_box.test.ts new file mode 100644 index 00000000000000..cf287ff2c437ac --- /dev/null +++ b/src/plugins/data/common/es_query/kuery/functions/geo_bounding_box.test.ts @@ -0,0 +1,133 @@ +/* + * 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 { get } from 'lodash'; +import { nodeTypes } from '../node_types'; +import { fields } from '../../../index_patterns/mocks'; +import { IIndexPattern } from '../../../index_patterns'; + +// @ts-ignore +import * as geoBoundingBox from './geo_bounding_box'; + +const params = { + bottomRight: { + lat: 50.73, + lon: -135.35, + }, + topLeft: { + lat: 73.12, + lon: -174.37, + }, +}; + +describe('kuery functions', () => { + describe('geoBoundingBox', () => { + let indexPattern: IIndexPattern; + + beforeEach(() => { + indexPattern = ({ + fields, + } as unknown) as IIndexPattern; + }); + + describe('buildNodeParams', () => { + test('should return an "arguments" param', () => { + const result = geoBoundingBox.buildNodeParams('geo', params); + + expect(result).toHaveProperty('arguments'); + expect(Object.keys(result).length).toBe(1); + }); + + test('arguments should contain the provided fieldName as a literal', () => { + const result = geoBoundingBox.buildNodeParams('geo', params); + const { + arguments: [fieldName], + } = result; + + expect(fieldName).toHaveProperty('type', 'literal'); + expect(fieldName).toHaveProperty('value', 'geo'); + }); + + test('arguments should contain the provided params as named arguments with "lat, lon" string values', () => { + const result = geoBoundingBox.buildNodeParams('geo', params); + const { + arguments: [, ...args], + } = result; + + args.map((param: any) => { + expect(param).toHaveProperty('type', 'namedArg'); + expect(['bottomRight', 'topLeft'].includes(param.name)).toBe(true); + expect(param.value.type).toBe('literal'); + + const { lat, lon } = get(params, param.name); + + expect(param.value.value).toBe(`${lat}, ${lon}`); + }); + }); + }); + + describe('toElasticsearchQuery', () => { + test('should return an ES geo_bounding_box query representing the given node', () => { + const node = nodeTypes.function.buildNode('geoBoundingBox', 'geo', params); + const result = geoBoundingBox.toElasticsearchQuery(node, indexPattern); + + expect(result).toHaveProperty('geo_bounding_box'); + expect(result.geo_bounding_box.geo).toHaveProperty('top_left', '73.12, -174.37'); + expect(result.geo_bounding_box.geo).toHaveProperty('bottom_right', '50.73, -135.35'); + }); + + test('should return an ES geo_bounding_box query without an index pattern', () => { + const node = nodeTypes.function.buildNode('geoBoundingBox', 'geo', params); + const result = geoBoundingBox.toElasticsearchQuery(node); + + expect(result).toHaveProperty('geo_bounding_box'); + expect(result.geo_bounding_box.geo).toHaveProperty('top_left', '73.12, -174.37'); + expect(result.geo_bounding_box.geo).toHaveProperty('bottom_right', '50.73, -135.35'); + }); + + test('should use the ignore_unmapped parameter', () => { + const node = nodeTypes.function.buildNode('geoBoundingBox', 'geo', params); + const result = geoBoundingBox.toElasticsearchQuery(node, indexPattern); + + expect(result.geo_bounding_box.ignore_unmapped).toBe(true); + }); + + test('should throw an error for scripted fields', () => { + const node = nodeTypes.function.buildNode('geoBoundingBox', 'script number', params); + + expect(() => geoBoundingBox.toElasticsearchQuery(node, indexPattern)).toThrowError( + /Geo bounding box query does not support scripted fields/ + ); + }); + + test('should use a provided nested context to create a full field name', () => { + const node = nodeTypes.function.buildNode('geoBoundingBox', 'geo', params); + const result = geoBoundingBox.toElasticsearchQuery( + node, + indexPattern, + {}, + { nested: { path: 'nestedField' } } + ); + + expect(result).toHaveProperty('geo_bounding_box'); + expect(result.geo_bounding_box['nestedField.geo']).toBeDefined(); + }); + }); + }); +}); diff --git a/packages/kbn-es-query/src/kuery/functions/geo_polygon.js b/src/plugins/data/common/es_query/kuery/functions/geo_polygon.js similarity index 100% rename from packages/kbn-es-query/src/kuery/functions/geo_polygon.js rename to src/plugins/data/common/es_query/kuery/functions/geo_polygon.js diff --git a/src/plugins/data/common/es_query/kuery/functions/geo_polygon.test.ts b/src/plugins/data/common/es_query/kuery/functions/geo_polygon.test.ts new file mode 100644 index 00000000000000..84500cb4ade7e6 --- /dev/null +++ b/src/plugins/data/common/es_query/kuery/functions/geo_polygon.test.ts @@ -0,0 +1,143 @@ +/* + * 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 { nodeTypes } from '../node_types'; +import { fields } from '../../../index_patterns/mocks'; +import { IIndexPattern } from '../../../index_patterns'; + +// @ts-ignore +import * as geoPolygon from './geo_polygon'; + +const points = [ + { + lat: 69.77, + lon: -171.56, + }, + { + lat: 50.06, + lon: -169.1, + }, + { + lat: 69.16, + lon: -125.85, + }, +]; + +describe('kuery functions', () => { + describe('geoPolygon', () => { + let indexPattern: IIndexPattern; + + beforeEach(() => { + indexPattern = ({ + fields, + } as unknown) as IIndexPattern; + }); + + describe('buildNodeParams', () => { + test('should return an "arguments" param', () => { + const result = geoPolygon.buildNodeParams('geo', points); + + expect(result).toHaveProperty('arguments'); + expect(Object.keys(result).length).toBe(1); + }); + + test('arguments should contain the provided fieldName as a literal', () => { + const result = geoPolygon.buildNodeParams('geo', points); + const { + arguments: [fieldName], + } = result; + + expect(fieldName).toHaveProperty('type', 'literal'); + expect(fieldName).toHaveProperty('value', 'geo'); + }); + + test('arguments should contain the provided points literal "lat, lon" string values', () => { + const result = geoPolygon.buildNodeParams('geo', points); + const { + arguments: [, ...args], + } = result; + + args.forEach((param: any, index: number) => { + const expectedPoint = points[index]; + const expectedLatLon = `${expectedPoint.lat}, ${expectedPoint.lon}`; + + expect(param).toHaveProperty('type', 'literal'); + expect(param.value).toBe(expectedLatLon); + }); + }); + }); + + describe('toElasticsearchQuery', () => { + test('should return an ES geo_polygon query representing the given node', () => { + const node = nodeTypes.function.buildNode('geoPolygon', 'geo', points); + const result = geoPolygon.toElasticsearchQuery(node, indexPattern); + + expect(result).toHaveProperty('geo_polygon'); + expect(result.geo_polygon.geo).toHaveProperty('points'); + + result.geo_polygon.geo.points.forEach((point: any, index: number) => { + const expectedLatLon = `${points[index].lat}, ${points[index].lon}`; + + expect(point).toBe(expectedLatLon); + }); + }); + + test('should return an ES geo_polygon query without an index pattern', () => { + const node = nodeTypes.function.buildNode('geoPolygon', 'geo', points); + const result = geoPolygon.toElasticsearchQuery(node); + + expect(result).toHaveProperty('geo_polygon'); + expect(result.geo_polygon.geo).toHaveProperty('points'); + + result.geo_polygon.geo.points.forEach((point: any, index: number) => { + const expectedLatLon = `${points[index].lat}, ${points[index].lon}`; + + expect(point).toBe(expectedLatLon); + }); + }); + + test('should use the ignore_unmapped parameter', () => { + const node = nodeTypes.function.buildNode('geoPolygon', 'geo', points); + const result = geoPolygon.toElasticsearchQuery(node, indexPattern); + + expect(result.geo_polygon.ignore_unmapped).toBe(true); + }); + + test('should throw an error for scripted fields', () => { + const node = nodeTypes.function.buildNode('geoPolygon', 'script number', points); + expect(() => geoPolygon.toElasticsearchQuery(node, indexPattern)).toThrowError( + /Geo polygon query does not support scripted fields/ + ); + }); + + test('should use a provided nested context to create a full field name', () => { + const node = nodeTypes.function.buildNode('geoPolygon', 'geo', points); + const result = geoPolygon.toElasticsearchQuery( + node, + indexPattern, + {}, + { nested: { path: 'nestedField' } } + ); + + expect(result).toHaveProperty('geo_polygon'); + expect(result.geo_polygon['nestedField.geo']).toBeDefined(); + }); + }); + }); +}); diff --git a/packages/kbn-es-query/src/kuery/functions/index.js b/src/plugins/data/common/es_query/kuery/functions/index.js similarity index 100% rename from packages/kbn-es-query/src/kuery/functions/index.js rename to src/plugins/data/common/es_query/kuery/functions/index.js diff --git a/packages/kbn-es-query/src/kuery/functions/is.js b/src/plugins/data/common/es_query/kuery/functions/is.js similarity index 95% rename from packages/kbn-es-query/src/kuery/functions/is.js rename to src/plugins/data/common/es_query/kuery/functions/is.js index 63ade9e8793a7b..4f2f298c4707d1 100644 --- a/packages/kbn-es-query/src/kuery/functions/is.js +++ b/src/plugins/data/common/es_query/kuery/functions/is.js @@ -17,20 +17,22 @@ * under the License. */ -import _ from 'lodash'; -import * as ast from '../ast'; -import * as literal from '../node_types/literal'; -import * as wildcard from '../node_types/wildcard'; -import { getPhraseScript } from '../../utils/filters'; +import { get, isUndefined } from 'lodash'; +import { getPhraseScript } from '../../filters'; import { getFields } from './utils/get_fields'; import { getTimeZoneFromSettings } from '../../utils/get_time_zone_from_settings'; import { getFullFieldNameNode } from './utils/get_full_field_name_node'; +import * as ast from '../ast'; + +import * as literal from '../node_types/literal'; +import * as wildcard from '../node_types/wildcard'; + export function buildNodeParams(fieldName, value, isPhrase = false) { - if (_.isUndefined(fieldName)) { + if (isUndefined(fieldName)) { throw new Error('fieldName is a required argument'); } - if (_.isUndefined(value)) { + if (isUndefined(value)) { throw new Error('value is a required argument'); } const fieldNode = typeof fieldName === 'string' ? ast.fromLiteralExpression(fieldName) : literal.buildNode(fieldName); @@ -45,7 +47,7 @@ export function toElasticsearchQuery(node, indexPattern = null, config = {}, con const { arguments: [fieldNameArg, valueArg, isPhraseArg] } = node; const fullFieldNameArg = getFullFieldNameNode(fieldNameArg, indexPattern, context.nested ? context.nested.path : undefined); const fieldName = ast.toElasticsearchQuery(fullFieldNameArg); - const value = !_.isUndefined(valueArg) ? ast.toElasticsearchQuery(valueArg) : valueArg; + const value = !isUndefined(valueArg) ? ast.toElasticsearchQuery(valueArg) : valueArg; const type = isPhraseArg.value ? 'phrase' : 'best_fields'; if (fullFieldNameArg.value === null) { if (valueArg.type === 'wildcard') { @@ -94,7 +96,7 @@ export function toElasticsearchQuery(node, indexPattern = null, config = {}, con // users handle this themselves so we automatically add nested queries in this scenario. if ( !(fullFieldNameArg.type === 'wildcard') - || !_.get(field, 'subType.nested') + || !get(field, 'subType.nested') || context.nested ) { return query; diff --git a/src/plugins/data/common/es_query/kuery/functions/is.test.ts b/src/plugins/data/common/es_query/kuery/functions/is.test.ts new file mode 100644 index 00000000000000..df147bad54a34b --- /dev/null +++ b/src/plugins/data/common/es_query/kuery/functions/is.test.ts @@ -0,0 +1,305 @@ +/* + * 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 { nodeTypes } from '../node_types'; +import { fields } from '../../../index_patterns/mocks'; + +// @ts-ignore +import * as is from './is'; +import { IIndexPattern } from '../../../index_patterns'; + +describe('kuery functions', () => { + describe('is', () => { + let indexPattern: IIndexPattern; + + beforeEach(() => { + indexPattern = ({ + fields, + } as unknown) as IIndexPattern; + }); + + describe('buildNodeParams', () => { + test('fieldName and value should be required arguments', () => { + expect(() => is.buildNodeParams()).toThrowError(/fieldName is a required argument/); + expect(() => is.buildNodeParams('foo')).toThrowError(/value is a required argument/); + }); + + test('arguments should contain the provided fieldName and value as literals', () => { + const { + arguments: [fieldName, value], + } = is.buildNodeParams('response', 200); + + expect(fieldName).toHaveProperty('type', 'literal'); + expect(fieldName).toHaveProperty('value', 'response'); + expect(value).toHaveProperty('type', 'literal'); + expect(value).toHaveProperty('value', 200); + }); + + test('should detect wildcards in the provided arguments', () => { + const { + arguments: [fieldName, value], + } = is.buildNodeParams('machine*', 'win*'); + + expect(fieldName).toHaveProperty('type', 'wildcard'); + expect(value).toHaveProperty('type', 'wildcard'); + }); + + test('should default to a non-phrase query', () => { + const { + arguments: [, , isPhrase], + } = is.buildNodeParams('response', 200); + expect(isPhrase.value).toBe(false); + }); + + test('should allow specification of a phrase query', () => { + const { + arguments: [, , isPhrase], + } = is.buildNodeParams('response', 200, true); + expect(isPhrase.value).toBe(true); + }); + }); + + describe('toElasticsearchQuery', () => { + test('should return an ES match_all query when fieldName and value are both "*"', () => { + const expected = { + match_all: {}, + }; + const node = nodeTypes.function.buildNode('is', '*', '*'); + const result = is.toElasticsearchQuery(node, indexPattern); + + expect(result).toEqual(expected); + }); + + test('should return an ES multi_match query using default_field when fieldName is null', () => { + const expected = { + multi_match: { + query: 200, + type: 'best_fields', + lenient: true, + }, + }; + const node = nodeTypes.function.buildNode('is', null, 200); + const result = is.toElasticsearchQuery(node, indexPattern); + + expect(result).toEqual(expected); + }); + + test('should return an ES query_string query using default_field when fieldName is null and value contains a wildcard', () => { + const expected = { + query_string: { + query: 'jpg*', + }, + }; + const node = nodeTypes.function.buildNode('is', null, 'jpg*'); + const result = is.toElasticsearchQuery(node, indexPattern); + + expect(result).toEqual(expected); + }); + + test('should return an ES bool query with a sub-query for each field when fieldName is "*"', () => { + const node = nodeTypes.function.buildNode('is', '*', 200); + const result = is.toElasticsearchQuery(node, indexPattern); + + expect(result).toHaveProperty('bool'); + expect(result.bool.should.length).toBe(indexPattern.fields.length); + }); + + test('should return an ES exists query when value is "*"', () => { + const expected = { + bool: { + should: [{ exists: { field: 'extension' } }], + minimum_should_match: 1, + }, + }; + const node = nodeTypes.function.buildNode('is', 'extension', '*'); + const result = is.toElasticsearchQuery(node, indexPattern); + + expect(result).toEqual(expected); + }); + + test('should return an ES match query when a concrete fieldName and value are provided', () => { + const expected = { + bool: { + should: [{ match: { extension: 'jpg' } }], + minimum_should_match: 1, + }, + }; + const node = nodeTypes.function.buildNode('is', 'extension', 'jpg'); + const result = is.toElasticsearchQuery(node, indexPattern); + + expect(result).toEqual(expected); + }); + + test('should return an ES match query when a concrete fieldName and value are provided without an index pattern', () => { + const expected = { + bool: { + should: [{ match: { extension: 'jpg' } }], + minimum_should_match: 1, + }, + }; + const node = nodeTypes.function.buildNode('is', 'extension', 'jpg'); + const result = is.toElasticsearchQuery(node); + + expect(result).toEqual(expected); + }); + + test('should support creation of phrase queries', () => { + const expected = { + bool: { + should: [{ match_phrase: { extension: 'jpg' } }], + minimum_should_match: 1, + }, + }; + const node = nodeTypes.function.buildNode('is', 'extension', 'jpg', true); + const result = is.toElasticsearchQuery(node, indexPattern); + + expect(result).toEqual(expected); + }); + + test('should create a query_string query for wildcard values', () => { + const expected = { + bool: { + should: [ + { + query_string: { + fields: ['extension'], + query: 'jpg*', + }, + }, + ], + minimum_should_match: 1, + }, + }; + const node = nodeTypes.function.buildNode('is', 'extension', 'jpg*'); + const result = is.toElasticsearchQuery(node, indexPattern); + + expect(result).toEqual(expected); + }); + + test('should support scripted fields', () => { + const node = nodeTypes.function.buildNode('is', 'script string', 'foo'); + const result = is.toElasticsearchQuery(node, indexPattern); + + expect(result.bool.should[0]).toHaveProperty('script'); + }); + + test('should support date fields without a dateFormat provided', () => { + const expected = { + bool: { + should: [ + { + range: { + '@timestamp': { + gte: '2018-04-03T19:04:17', + lte: '2018-04-03T19:04:17', + }, + }, + }, + ], + minimum_should_match: 1, + }, + }; + const node = nodeTypes.function.buildNode('is', '@timestamp', '"2018-04-03T19:04:17"'); + const result = is.toElasticsearchQuery(node, indexPattern); + + expect(result).toEqual(expected); + }); + + test('should support date fields with a dateFormat provided', () => { + const config = { dateFormatTZ: 'America/Phoenix' }; + const expected = { + bool: { + should: [ + { + range: { + '@timestamp': { + gte: '2018-04-03T19:04:17', + lte: '2018-04-03T19:04:17', + time_zone: 'America/Phoenix', + }, + }, + }, + ], + minimum_should_match: 1, + }, + }; + const node = nodeTypes.function.buildNode('is', '@timestamp', '"2018-04-03T19:04:17"'); + const result = is.toElasticsearchQuery(node, indexPattern, config); + + expect(result).toEqual(expected); + }); + + test('should use a provided nested context to create a full field name', () => { + const expected = { + bool: { + should: [{ match: { 'nestedField.extension': 'jpg' } }], + minimum_should_match: 1, + }, + }; + const node = nodeTypes.function.buildNode('is', 'extension', 'jpg'); + const result = is.toElasticsearchQuery( + node, + indexPattern, + {}, + { nested: { path: 'nestedField' } } + ); + + expect(result).toEqual(expected); + }); + + test('should support wildcard field names', () => { + const expected = { + bool: { + should: [{ match: { extension: 'jpg' } }], + minimum_should_match: 1, + }, + }; + const node = nodeTypes.function.buildNode('is', 'ext*', 'jpg'); + const result = is.toElasticsearchQuery(node, indexPattern); + + expect(result).toEqual(expected); + }); + + test('should automatically add a nested query when a wildcard field name covers a nested field', () => { + const expected = { + bool: { + should: [ + { + nested: { + path: 'nestedField.nestedChild', + query: { + match: { + 'nestedField.nestedChild.doublyNestedChild': 'foo', + }, + }, + score_mode: 'none', + }, + }, + ], + minimum_should_match: 1, + }, + }; + const node = nodeTypes.function.buildNode('is', '*doublyNested*', 'foo'); + const result = is.toElasticsearchQuery(node, indexPattern); + + expect(result).toEqual(expected); + }); + }); + }); +}); diff --git a/packages/kbn-es-query/src/kuery/functions/nested.js b/src/plugins/data/common/es_query/kuery/functions/nested.js similarity index 100% rename from packages/kbn-es-query/src/kuery/functions/nested.js rename to src/plugins/data/common/es_query/kuery/functions/nested.js diff --git a/packages/kbn-es-query/src/kuery/functions/__tests__/nested.js b/src/plugins/data/common/es_query/kuery/functions/nested.test.ts similarity index 54% rename from packages/kbn-es-query/src/kuery/functions/__tests__/nested.js rename to src/plugins/data/common/es_query/kuery/functions/nested.test.ts index 5ba73e485ddf1d..945a36d304a057 100644 --- a/packages/kbn-es-query/src/kuery/functions/__tests__/nested.js +++ b/src/plugins/data/common/es_query/kuery/functions/nested.test.ts @@ -17,52 +17,60 @@ * under the License. */ -import expect from '@kbn/expect'; -import * as nested from '../nested'; -import { nodeTypes } from '../../node_types'; -import * as ast from '../../ast'; -import indexPatternResponse from '../../../__fixtures__/index_pattern_response.json'; +import { nodeTypes } from '../node_types'; +import { fields } from '../../../index_patterns/mocks'; +import { IIndexPattern } from '../../../index_patterns'; -let indexPattern; +import * as ast from '../ast'; + +// @ts-ignore +import * as nested from './nested'; const childNode = nodeTypes.function.buildNode('is', 'child', 'foo'); -describe('kuery functions', function () { - describe('nested', function () { +describe('kuery functions', () => { + describe('nested', () => { + let indexPattern: IIndexPattern; beforeEach(() => { - indexPattern = indexPatternResponse; + indexPattern = ({ + fields, + } as unknown) as IIndexPattern; }); - describe('buildNodeParams', function () { - - it('arguments should contain the unmodified child nodes', function () { + describe('buildNodeParams', () => { + test('arguments should contain the unmodified child nodes', () => { const result = nested.buildNodeParams('nestedField', childNode); - const { arguments: [ resultPath, resultChildNode ] } = result; - expect(ast.toElasticsearchQuery(resultPath)).to.be('nestedField'); - expect(resultChildNode).to.be(childNode); + const { + arguments: [resultPath, resultChildNode], + } = result; + + expect(ast.toElasticsearchQuery(resultPath)).toBe('nestedField'); + expect(resultChildNode).toBe(childNode); }); }); - describe('toElasticsearchQuery', function () { - - it('should wrap subqueries in an ES nested query', function () { + describe('toElasticsearchQuery', () => { + test('should wrap subqueries in an ES nested query', () => { const node = nodeTypes.function.buildNode('nested', 'nestedField', childNode); const result = nested.toElasticsearchQuery(node, indexPattern); - expect(result).to.only.have.keys('nested'); - expect(result.nested.path).to.be('nestedField'); - expect(result.nested.score_mode).to.be('none'); + + expect(result).toHaveProperty('nested'); + expect(Object.keys(result).length).toBe(1); + + expect(result.nested.path).toBe('nestedField'); + expect(result.nested.score_mode).toBe('none'); }); - it('should pass the nested path to subqueries so the full field name can be used', function () { + test('should pass the nested path to subqueries so the full field name can be used', () => { const node = nodeTypes.function.buildNode('nested', 'nestedField', childNode); const result = nested.toElasticsearchQuery(node, indexPattern); const expectedSubQuery = ast.toElasticsearchQuery( nodeTypes.function.buildNode('is', 'nestedField.child', 'foo') ); - expect(result.nested.query).to.eql(expectedSubQuery); - }); + expect(result.nested.query).toEqual(expectedSubQuery); + }); }); }); }); diff --git a/packages/kbn-es-query/src/kuery/functions/not.js b/src/plugins/data/common/es_query/kuery/functions/not.js similarity index 100% rename from packages/kbn-es-query/src/kuery/functions/not.js rename to src/plugins/data/common/es_query/kuery/functions/not.js diff --git a/packages/kbn-es-query/src/kuery/functions/__tests__/not.js b/src/plugins/data/common/es_query/kuery/functions/not.test.ts similarity index 50% rename from packages/kbn-es-query/src/kuery/functions/__tests__/not.js rename to src/plugins/data/common/es_query/kuery/functions/not.test.ts index 7a2d7fa39c1528..01c1976b939ea0 100644 --- a/packages/kbn-es-query/src/kuery/functions/__tests__/not.js +++ b/src/plugins/data/common/es_query/kuery/functions/not.test.ts @@ -17,44 +17,50 @@ * under the License. */ -import expect from '@kbn/expect'; -import * as not from '../not'; -import { nodeTypes } from '../../node_types'; -import * as ast from '../../ast'; -import indexPatternResponse from '../../../__fixtures__/index_pattern_response.json'; +import { nodeTypes } from '../node_types'; +import { fields } from '../../../index_patterns/mocks'; +import { IIndexPattern } from '../../../index_patterns'; -let indexPattern; +import * as ast from '../ast'; -const childNode = nodeTypes.function.buildNode('is', 'extension', 'jpg'); +// @ts-ignore +import * as not from './not'; -describe('kuery functions', function () { +const childNode = nodeTypes.function.buildNode('is', 'extension', 'jpg'); - describe('not', function () { +describe('kuery functions', () => { + describe('not', () => { + let indexPattern: IIndexPattern; beforeEach(() => { - indexPattern = indexPatternResponse; + indexPattern = ({ + fields, + } as unknown) as IIndexPattern; }); - describe('buildNodeParams', function () { + describe('buildNodeParams', () => { + test('arguments should contain the unmodified child node', () => { + const { + arguments: [actualChild], + } = not.buildNodeParams(childNode); - it('arguments should contain the unmodified child node', function () { - const { arguments: [ actualChild ] } = not.buildNodeParams(childNode); - expect(actualChild).to.be(childNode); + expect(actualChild).toBe(childNode); }); - - }); - describe('toElasticsearchQuery', function () { - - it('should wrap a subquery in an ES bool query\'s must_not clause', function () { + describe('toElasticsearchQuery', () => { + test("should wrap a subquery in an ES bool query's must_not clause", () => { const node = nodeTypes.function.buildNode('not', childNode); const result = not.toElasticsearchQuery(node, indexPattern); - expect(result).to.only.have.keys('bool'); - expect(result.bool).to.only.have.keys('must_not'); - expect(result.bool.must_not).to.eql(ast.toElasticsearchQuery(childNode, indexPattern)); - }); + expect(result).toHaveProperty('bool'); + expect(Object.keys(result).length).toBe(1); + + expect(result.bool).toHaveProperty('must_not'); + expect(Object.keys(result.bool).length).toBe(1); + + expect(result.bool.must_not).toEqual(ast.toElasticsearchQuery(childNode, indexPattern)); + }); }); }); }); diff --git a/packages/kbn-es-query/src/kuery/functions/or.js b/src/plugins/data/common/es_query/kuery/functions/or.js similarity index 100% rename from packages/kbn-es-query/src/kuery/functions/or.js rename to src/plugins/data/common/es_query/kuery/functions/or.js diff --git a/packages/kbn-es-query/src/kuery/functions/__tests__/or.js b/src/plugins/data/common/es_query/kuery/functions/or.test.ts similarity index 52% rename from packages/kbn-es-query/src/kuery/functions/__tests__/or.js rename to src/plugins/data/common/es_query/kuery/functions/or.test.ts index f24f24b98e7fbd..a6590546e5fc5c 100644 --- a/packages/kbn-es-query/src/kuery/functions/__tests__/or.js +++ b/src/plugins/data/common/es_query/kuery/functions/or.test.ts @@ -17,56 +17,61 @@ * under the License. */ -import expect from '@kbn/expect'; -import * as or from '../or'; -import { nodeTypes } from '../../node_types'; -import * as ast from '../../ast'; -import indexPatternResponse from '../../../__fixtures__/index_pattern_response.json'; +import { nodeTypes } from '../node_types'; +import { fields } from '../../../index_patterns/mocks'; +import { IIndexPattern } from '../../../index_patterns'; -let indexPattern; +import * as ast from '../ast'; + +// @ts-ignore +import * as or from './or'; const childNode1 = nodeTypes.function.buildNode('is', 'machine.os', 'osx'); const childNode2 = nodeTypes.function.buildNode('is', 'extension', 'jpg'); -describe('kuery functions', function () { - - describe('or', function () { +describe('kuery functions', () => { + describe('or', () => { + let indexPattern: IIndexPattern; beforeEach(() => { - indexPattern = indexPatternResponse; + indexPattern = ({ + fields, + } as unknown) as IIndexPattern; }); - - describe('buildNodeParams', function () { - - it('arguments should contain the unmodified child nodes', function () { + describe('buildNodeParams', () => { + test('arguments should contain the unmodified child nodes', () => { const result = or.buildNodeParams([childNode1, childNode2]); - const { arguments: [ actualChildNode1, actualChildNode2 ] } = result; - expect(actualChildNode1).to.be(childNode1); - expect(actualChildNode2).to.be(childNode2); - }); + const { + arguments: [actualChildNode1, actualChildNode2], + } = result; + expect(actualChildNode1).toBe(childNode1); + expect(actualChildNode2).toBe(childNode2); + }); }); - describe('toElasticsearchQuery', function () { - - it('should wrap subqueries in an ES bool query\'s should clause', function () { + describe('toElasticsearchQuery', () => { + test("should wrap subqueries in an ES bool query's should clause", () => { const node = nodeTypes.function.buildNode('or', [childNode1, childNode2]); const result = or.toElasticsearchQuery(node, indexPattern); - expect(result).to.only.have.keys('bool'); - expect(result.bool).to.have.keys('should'); - expect(result.bool.should).to.eql( - [childNode1, childNode2].map((childNode) => ast.toElasticsearchQuery(childNode, indexPattern)) + + expect(result).toHaveProperty('bool'); + expect(Object.keys(result).length).toBe(1); + expect(result.bool).toHaveProperty('should'); + expect(result.bool.should).toEqual( + [childNode1, childNode2].map(childNode => + ast.toElasticsearchQuery(childNode, indexPattern) + ) ); }); - it('should require one of the clauses to match', function () { + test('should require one of the clauses to match', () => { const node = nodeTypes.function.buildNode('or', [childNode1, childNode2]); const result = or.toElasticsearchQuery(node, indexPattern); - expect(result.bool).to.have.property('minimum_should_match', 1); - }); + expect(result.bool).toHaveProperty('minimum_should_match', 1); + }); }); - }); }); diff --git a/packages/kbn-es-query/src/kuery/functions/range.js b/src/plugins/data/common/es_query/kuery/functions/range.js similarity index 98% rename from packages/kbn-es-query/src/kuery/functions/range.js rename to src/plugins/data/common/es_query/kuery/functions/range.js index f7719998ad5240..80181cfc003f1c 100644 --- a/packages/kbn-es-query/src/kuery/functions/range.js +++ b/src/plugins/data/common/es_query/kuery/functions/range.js @@ -20,7 +20,7 @@ import _ from 'lodash'; import { nodeTypes } from '../node_types'; import * as ast from '../ast'; -import { getRangeScript } from '../../utils/filters'; +import { getRangeScript } from '../../filters'; import { getFields } from './utils/get_fields'; import { getTimeZoneFromSettings } from '../../utils/get_time_zone_from_settings'; import { getFullFieldNameNode } from './utils/get_full_field_name_node'; diff --git a/packages/kbn-es-query/src/kuery/functions/__tests__/range.js b/src/plugins/data/common/es_query/kuery/functions/range.test.ts similarity index 54% rename from packages/kbn-es-query/src/kuery/functions/__tests__/range.js rename to src/plugins/data/common/es_query/kuery/functions/range.test.ts index 2361e8bb667691..ed8e40830df021 100644 --- a/packages/kbn-es-query/src/kuery/functions/__tests__/range.js +++ b/src/plugins/data/common/es_query/kuery/functions/range.test.ts @@ -17,53 +17,57 @@ * under the License. */ -import expect from '@kbn/expect'; -import * as range from '../range'; -import { nodeTypes } from '../../node_types'; -import indexPatternResponse from '../../../__fixtures__/index_pattern_response.json'; +import { get } from 'lodash'; +import { nodeTypes } from '../node_types'; +import { fields } from '../../../index_patterns/mocks'; +import { IIndexPattern } from '../../../index_patterns'; +import { RangeFilterParams } from '../../filters'; -let indexPattern; - -describe('kuery functions', function () { - - describe('range', function () { +// @ts-ignore +import * as range from './range'; +describe('kuery functions', () => { + describe('range', () => { + let indexPattern: IIndexPattern; beforeEach(() => { - indexPattern = indexPatternResponse; + indexPattern = ({ + fields, + } as unknown) as IIndexPattern; }); - describe('buildNodeParams', function () { - - it('arguments should contain the provided fieldName as a literal', function () { + describe('buildNodeParams', () => { + test('arguments should contain the provided fieldName as a literal', () => { const result = range.buildNodeParams('bytes', { gt: 1000, lt: 8000 }); - const { arguments: [fieldName] } = result; + const { + arguments: [fieldName], + } = result; - expect(fieldName).to.have.property('type', 'literal'); - expect(fieldName).to.have.property('value', 'bytes'); + expect(fieldName).toHaveProperty('type', 'literal'); + expect(fieldName).toHaveProperty('value', 'bytes'); }); - it('arguments should contain the provided params as named arguments', function () { - const givenParams = { gt: 1000, lt: 8000, format: 'epoch_millis' }; + test('arguments should contain the provided params as named arguments', () => { + const givenParams: RangeFilterParams = { gt: 1000, lt: 8000, format: 'epoch_millis' }; const result = range.buildNodeParams('bytes', givenParams); - const { arguments: [, ...params] } = result; + const { + arguments: [, ...params], + } = result; - expect(params).to.be.an('array'); - expect(params).to.not.be.empty(); + expect(Array.isArray(params)).toBeTruthy(); + expect(params.length).toBeGreaterThan(1); - params.map((param) => { - expect(param).to.have.property('type', 'namedArg'); - expect(['gt', 'lt', 'format'].includes(param.name)).to.be(true); - expect(param.value.type).to.be('literal'); - expect(param.value.value).to.be(givenParams[param.name]); + params.map((param: any) => { + expect(param).toHaveProperty('type', 'namedArg'); + expect(['gt', 'lt', 'format'].includes(param.name)).toBe(true); + expect(param.value.type).toBe('literal'); + expect(param.value.value).toBe(get(givenParams, param.name)); }); }); - }); - describe('toElasticsearchQuery', function () { - - it('should return an ES range query for the node\'s field and params', function () { + describe('toElasticsearchQuery', () => { + test("should return an ES range query for the node's field and params", () => { const expected = { bool: { should: [ @@ -71,21 +75,21 @@ describe('kuery functions', function () { range: { bytes: { gt: 1000, - lt: 8000 - } - } - } + lt: 8000, + }, + }, + }, ], - minimum_should_match: 1 - } + minimum_should_match: 1, + }, }; - const node = nodeTypes.function.buildNode('range', 'bytes', { gt: 1000, lt: 8000 }); const result = range.toElasticsearchQuery(node, indexPattern); - expect(result).to.eql(expected); + + expect(result).toEqual(expected); }); - it('should return an ES range query without an index pattern', function () { + test('should return an ES range query without an index pattern', () => { const expected = { bool: { should: [ @@ -93,21 +97,22 @@ describe('kuery functions', function () { range: { bytes: { gt: 1000, - lt: 8000 - } - } - } + lt: 8000, + }, + }, + }, ], - minimum_should_match: 1 - } + minimum_should_match: 1, + }, }; const node = nodeTypes.function.buildNode('range', 'bytes', { gt: 1000, lt: 8000 }); const result = range.toElasticsearchQuery(node); - expect(result).to.eql(expected); + + expect(result).toEqual(expected); }); - it('should support wildcard field names', function () { + test('should support wildcard field names', () => { const expected = { bool: { should: [ @@ -115,27 +120,29 @@ describe('kuery functions', function () { range: { bytes: { gt: 1000, - lt: 8000 - } - } - } + lt: 8000, + }, + }, + }, ], - minimum_should_match: 1 - } + minimum_should_match: 1, + }, }; const node = nodeTypes.function.buildNode('range', 'byt*', { gt: 1000, lt: 8000 }); const result = range.toElasticsearchQuery(node, indexPattern); - expect(result).to.eql(expected); + + expect(result).toEqual(expected); }); - it('should support scripted fields', function () { + test('should support scripted fields', () => { const node = nodeTypes.function.buildNode('range', 'script number', { gt: 1000, lt: 8000 }); const result = range.toElasticsearchQuery(node, indexPattern); - expect(result.bool.should[0]).to.have.key('script'); + + expect(result.bool.should[0]).toHaveProperty('script'); }); - it('should support date fields without a dateFormat provided', function () { + test('should support date fields without a dateFormat provided', () => { const expected = { bool: { should: [ @@ -144,20 +151,23 @@ describe('kuery functions', function () { '@timestamp': { gt: '2018-01-03T19:04:17', lt: '2018-04-03T19:04:17', - } - } - } + }, + }, + }, ], - minimum_should_match: 1 - } + minimum_should_match: 1, + }, }; - - const node = nodeTypes.function.buildNode('range', '@timestamp', { gt: '2018-01-03T19:04:17', lt: '2018-04-03T19:04:17' }); + const node = nodeTypes.function.buildNode('range', '@timestamp', { + gt: '2018-01-03T19:04:17', + lt: '2018-04-03T19:04:17', + }); const result = range.toElasticsearchQuery(node, indexPattern); - expect(result).to.eql(expected); + + expect(result).toEqual(expected); }); - it('should support date fields with a dateFormat provided', function () { + test('should support date fields with a dateFormat provided', () => { const config = { dateFormatTZ: 'America/Phoenix' }; const expected = { bool: { @@ -168,20 +178,23 @@ describe('kuery functions', function () { gt: '2018-01-03T19:04:17', lt: '2018-04-03T19:04:17', time_zone: 'America/Phoenix', - } - } - } + }, + }, + }, ], - minimum_should_match: 1 - } + minimum_should_match: 1, + }, }; - - const node = nodeTypes.function.buildNode('range', '@timestamp', { gt: '2018-01-03T19:04:17', lt: '2018-04-03T19:04:17' }); + const node = nodeTypes.function.buildNode('range', '@timestamp', { + gt: '2018-01-03T19:04:17', + lt: '2018-04-03T19:04:17', + }); const result = range.toElasticsearchQuery(node, indexPattern, config); - expect(result).to.eql(expected); + + expect(result).toEqual(expected); }); - it('should use a provided nested context to create a full field name', function () { + test('should use a provided nested context to create a full field name', () => { const expected = { bool: { should: [ @@ -189,15 +202,14 @@ describe('kuery functions', function () { range: { 'nestedField.bytes': { gt: 1000, - lt: 8000 - } - } - } + lt: 8000, + }, + }, + }, ], - minimum_should_match: 1 - } + minimum_should_match: 1, + }, }; - const node = nodeTypes.function.buildNode('range', 'bytes', { gt: 1000, lt: 8000 }); const result = range.toElasticsearchQuery( node, @@ -205,10 +217,11 @@ describe('kuery functions', function () { {}, { nested: { path: 'nestedField' } } ); - expect(result).to.eql(expected); + + expect(result).toEqual(expected); }); - it('should automatically add a nested query when a wildcard field name covers a nested field', function () { + test('should automatically add a nested query when a wildcard field name covers a nested field', () => { const expected = { bool: { should: [ @@ -219,21 +232,24 @@ describe('kuery functions', function () { range: { 'nestedField.nestedChild.doublyNestedChild': { gt: 1000, - lt: 8000 - } - } + lt: 8000, + }, + }, }, - score_mode: 'none' - } - } + score_mode: 'none', + }, + }, ], - minimum_should_match: 1 - } + minimum_should_match: 1, + }, }; - - const node = nodeTypes.function.buildNode('range', '*doublyNested*', { gt: 1000, lt: 8000 }); + const node = nodeTypes.function.buildNode('range', '*doublyNested*', { + gt: 1000, + lt: 8000, + }); const result = range.toElasticsearchQuery(node, indexPattern); - expect(result).to.eql(expected); + + expect(result).toEqual(expected); }); }); }); diff --git a/packages/kbn-es-query/src/kuery/functions/utils/get_fields.js b/src/plugins/data/common/es_query/kuery/functions/utils/get_fields.js similarity index 100% rename from packages/kbn-es-query/src/kuery/functions/utils/get_fields.js rename to src/plugins/data/common/es_query/kuery/functions/utils/get_fields.js diff --git a/packages/kbn-es-query/src/kuery/functions/__tests__/utils/get_fields.js b/src/plugins/data/common/es_query/kuery/functions/utils/get_fields.test.ts similarity index 52% rename from packages/kbn-es-query/src/kuery/functions/__tests__/utils/get_fields.js rename to src/plugins/data/common/es_query/kuery/functions/utils/get_fields.test.ts index 7718479130a8af..d48f0943082c92 100644 --- a/packages/kbn-es-query/src/kuery/functions/__tests__/utils/get_fields.js +++ b/src/plugins/data/common/es_query/kuery/functions/utils/get_fields.test.ts @@ -17,39 +17,41 @@ * under the License. */ -import { getFields } from '../../utils/get_fields'; -import expect from '@kbn/expect'; -import indexPatternResponse from '../../../../__fixtures__/index_pattern_response.json'; +import { fields } from '../../../../index_patterns/mocks'; -import { nodeTypes } from '../../..'; +import { nodeTypes } from '../../index'; +import { IIndexPattern, IFieldType } from '../../../../index_patterns'; -let indexPattern; - -describe('getFields', function () { +// @ts-ignore +import { getFields } from './get_fields'; +describe('getFields', () => { + let indexPattern: IIndexPattern; beforeEach(() => { - indexPattern = indexPatternResponse; + indexPattern = ({ + fields, + } as unknown) as IIndexPattern; }); - describe('field names without a wildcard', function () { - - it('should return an empty array if the field does not exist in the index pattern', function () { + describe('field names without a wildcard', () => { + test('should return an empty array if the field does not exist in the index pattern', () => { const fieldNameNode = nodeTypes.literal.buildNode('nonExistentField'); - const expected = []; const actual = getFields(fieldNameNode, indexPattern); - expect(actual).to.eql(expected); + + expect(actual).toEqual([]); }); - it('should return the single matching field in an array', function () { + test('should return the single matching field in an array', () => { const fieldNameNode = nodeTypes.literal.buildNode('extension'); const results = getFields(fieldNameNode, indexPattern); - expect(results).to.be.an('array'); - expect(results).to.have.length(1); - expect(results[0].name).to.be('extension'); + + expect(results).toHaveLength(1); + expect(Array.isArray(results)).toBeTruthy(); + expect(results[0].name).toBe('extension'); }); - it('should not match a wildcard in a literal node', function () { + test('should not match a wildcard in a literal node', () => { const indexPatternWithWildField = { title: 'wildIndex', fields: [ @@ -61,37 +63,32 @@ describe('getFields', function () { const fieldNameNode = nodeTypes.literal.buildNode('foo*'); const results = getFields(fieldNameNode, indexPatternWithWildField); - expect(results).to.be.an('array'); - expect(results).to.have.length(1); - expect(results[0].name).to.be('foo*'); - // ensure the wildcard is not actually being parsed - const expected = []; + expect(results).toHaveLength(1); + expect(Array.isArray(results)).toBeTruthy(); + expect(results[0].name).toBe('foo*'); + const actual = getFields(nodeTypes.literal.buildNode('fo*'), indexPatternWithWildField); - expect(actual).to.eql(expected); + expect(actual).toEqual([]); }); }); - describe('field name patterns with a wildcard', function () { - - it('should return an empty array if it does not match any fields in the index pattern', function () { + describe('field name patterns with a wildcard', () => { + test('should return an empty array if test does not match any fields in the index pattern', () => { const fieldNameNode = nodeTypes.wildcard.buildNode('nonExistent*'); - const expected = []; const actual = getFields(fieldNameNode, indexPattern); - expect(actual).to.eql(expected); + + expect(actual).toEqual([]); }); - it('should return all fields that match the pattern in an array', function () { + test('should return all fields that match the pattern in an array', () => { const fieldNameNode = nodeTypes.wildcard.buildNode('machine*'); const results = getFields(fieldNameNode, indexPattern); - expect(results).to.be.an('array'); - expect(results).to.have.length(2); - expect(results.find((field) => { - return field.name === 'machine.os'; - })).to.be.ok(); - expect(results.find((field) => { - return field.name === 'machine.os.raw'; - })).to.be.ok(); + + expect(Array.isArray(results)).toBeTruthy(); + expect(results).toHaveLength(2); + expect(results.find((field: IFieldType) => field.name === 'machine.os')).toBeDefined(); + expect(results.find((field: IFieldType) => field.name === 'machine.os.raw')).toBeDefined(); }); }); }); diff --git a/packages/kbn-es-query/src/kuery/functions/utils/get_full_field_name_node.js b/src/plugins/data/common/es_query/kuery/functions/utils/get_full_field_name_node.js similarity index 100% rename from packages/kbn-es-query/src/kuery/functions/utils/get_full_field_name_node.js rename to src/plugins/data/common/es_query/kuery/functions/utils/get_full_field_name_node.js diff --git a/src/plugins/data/common/es_query/kuery/functions/utils/get_full_field_name_node.test.ts b/src/plugins/data/common/es_query/kuery/functions/utils/get_full_field_name_node.test.ts new file mode 100644 index 00000000000000..e138e22b76ad30 --- /dev/null +++ b/src/plugins/data/common/es_query/kuery/functions/utils/get_full_field_name_node.test.ts @@ -0,0 +1,87 @@ +/* + * 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 { nodeTypes } from '../../node_types'; +import { fields } from '../../../../index_patterns/mocks'; +import { IIndexPattern } from '../../../../index_patterns'; + +// @ts-ignore +import { getFullFieldNameNode } from './get_full_field_name_node'; + +describe('getFullFieldNameNode', function() { + let indexPattern: IIndexPattern; + + beforeEach(() => { + indexPattern = ({ + fields, + } as unknown) as IIndexPattern; + }); + + test('should return unchanged name node if no nested path is passed in', () => { + const nameNode = nodeTypes.literal.buildNode('notNested'); + const result = getFullFieldNameNode(nameNode, indexPattern); + + expect(result).toEqual(nameNode); + }); + + test('should add the nested path if test is valid according to the index pattern', () => { + const nameNode = nodeTypes.literal.buildNode('child'); + const result = getFullFieldNameNode(nameNode, indexPattern, 'nestedField'); + + expect(result).toEqual(nodeTypes.literal.buildNode('nestedField.child')); + }); + + test('should throw an error if a path is provided for a non-nested field', () => { + const nameNode = nodeTypes.literal.buildNode('os'); + expect(() => getFullFieldNameNode(nameNode, indexPattern, 'machine')).toThrowError( + /machine.os is not a nested field but is in nested group "machine" in the KQL expression/ + ); + }); + + test('should throw an error if a nested field is not passed with a path', () => { + const nameNode = nodeTypes.literal.buildNode('nestedField.child'); + + expect(() => getFullFieldNameNode(nameNode, indexPattern)).toThrowError( + /nestedField.child is a nested field, but is not in a nested group in the KQL expression./ + ); + }); + + test('should throw an error if a nested field is passed with the wrong path', () => { + const nameNode = nodeTypes.literal.buildNode('nestedChild.doublyNestedChild'); + + expect(() => getFullFieldNameNode(nameNode, indexPattern, 'nestedField')).toThrowError( + /Nested field nestedField.nestedChild.doublyNestedChild is being queried with the incorrect nested path. The correct path is nestedField.nestedChild/ + ); + }); + + test('should skip error checking for wildcard names', () => { + const nameNode = nodeTypes.wildcard.buildNode('nested*'); + const result = getFullFieldNameNode(nameNode, indexPattern); + + expect(result).toEqual(nameNode); + }); + + test('should skip error checking if no index pattern is passed in', () => { + const nameNode = nodeTypes.literal.buildNode('os'); + expect(() => getFullFieldNameNode(nameNode, null, 'machine')).not.toThrowError(); + + const result = getFullFieldNameNode(nameNode, null, 'machine'); + expect(result).toEqual(nodeTypes.literal.buildNode('machine.os')); + }); +}); diff --git a/packages/kbn-es-query/src/kuery/index.js b/src/plugins/data/common/es_query/kuery/index.ts similarity index 91% rename from packages/kbn-es-query/src/kuery/index.js rename to src/plugins/data/common/es_query/kuery/index.ts index e0cacada7f274e..4184dea62ef2c4 100644 --- a/packages/kbn-es-query/src/kuery/index.js +++ b/src/plugins/data/common/es_query/kuery/index.ts @@ -17,6 +17,8 @@ * under the License. */ -export * from './ast'; +export { KQLSyntaxError } from './kuery_syntax_error'; export { nodeTypes } from './node_types'; -export * from './errors'; +export * from './ast'; + +export * from './types'; diff --git a/packages/kbn-es-query/src/kuery/errors/index.test.js b/src/plugins/data/common/es_query/kuery/kuery_syntax_error.test.ts similarity index 66% rename from packages/kbn-es-query/src/kuery/errors/index.test.js rename to src/plugins/data/common/es_query/kuery/kuery_syntax_error.test.ts index d8040e464b696b..cfe2f86e813cad 100644 --- a/packages/kbn-es-query/src/kuery/errors/index.test.js +++ b/src/plugins/data/common/es_query/kuery/kuery_syntax_error.test.ts @@ -17,89 +17,92 @@ * under the License. */ -import { fromKueryExpression } from '../ast'; - +import { fromKueryExpression } from './ast'; describe('kql syntax errors', () => { - it('should throw an error for a field query missing a value', () => { expect(() => { fromKueryExpression('response:'); - }).toThrow('Expected "(", "{", value, whitespace but end of input found.\n' + - 'response:\n' + - '---------^'); + }).toThrow( + 'Expected "(", "{", value, whitespace but end of input found.\n' + + 'response:\n' + + '---------^' + ); }); it('should throw an error for an OR query missing a right side sub-query', () => { expect(() => { fromKueryExpression('response:200 or '); - }).toThrow('Expected "(", NOT, field name, value but end of input found.\n' + - 'response:200 or \n' + - '----------------^'); + }).toThrow( + 'Expected "(", NOT, field name, value but end of input found.\n' + + 'response:200 or \n' + + '----------------^' + ); }); it('should throw an error for an OR list of values missing a right side sub-query', () => { expect(() => { fromKueryExpression('response:(200 or )'); - }).toThrow('Expected "(", NOT, value but ")" found.\n' + - 'response:(200 or )\n' + - '-----------------^'); + }).toThrow( + 'Expected "(", NOT, value but ")" found.\n' + 'response:(200 or )\n' + '-----------------^' + ); }); it('should throw an error for a NOT query missing a sub-query', () => { expect(() => { fromKueryExpression('response:200 and not '); - }).toThrow('Expected "(", field name, value but end of input found.\n' + - 'response:200 and not \n' + - '---------------------^'); + }).toThrow( + 'Expected "(", field name, value but end of input found.\n' + + 'response:200 and not \n' + + '---------------------^' + ); }); it('should throw an error for a NOT list missing a sub-query', () => { expect(() => { fromKueryExpression('response:(200 and not )'); - }).toThrow('Expected "(", value but ")" found.\n' + - 'response:(200 and not )\n' + - '----------------------^'); + }).toThrow( + 'Expected "(", value but ")" found.\n' + + 'response:(200 and not )\n' + + '----------------------^' + ); }); it('should throw an error for unbalanced quotes', () => { expect(() => { fromKueryExpression('foo:"ba '); - }).toThrow('Expected "(", "{", value, whitespace but """ found.\n' + - 'foo:"ba \n' + - '----^'); + }).toThrow('Expected "(", "{", value, whitespace but """ found.\n' + 'foo:"ba \n' + '----^'); }); it('should throw an error for unescaped quotes in a quoted string', () => { expect(() => { fromKueryExpression('foo:"ba "r"'); - }).toThrow('Expected AND, OR, end of input, whitespace but "r" found.\n' + - 'foo:"ba "r"\n' + - '---------^'); + }).toThrow( + 'Expected AND, OR, end of input, whitespace but "r" found.\n' + 'foo:"ba "r"\n' + '---------^' + ); }); it('should throw an error for unescaped special characters in literals', () => { expect(() => { fromKueryExpression('foo:ba:r'); - }).toThrow('Expected AND, OR, end of input, whitespace but ":" found.\n' + - 'foo:ba:r\n' + - '------^'); + }).toThrow( + 'Expected AND, OR, end of input, whitespace but ":" found.\n' + 'foo:ba:r\n' + '------^' + ); }); it('should throw an error for range queries missing a value', () => { expect(() => { fromKueryExpression('foo > '); - }).toThrow('Expected literal, whitespace but end of input found.\n' + - 'foo > \n' + - '------^'); + }).toThrow('Expected literal, whitespace but end of input found.\n' + 'foo > \n' + '------^'); }); it('should throw an error for range queries missing a field', () => { expect(() => { fromKueryExpression('< 1000'); - }).toThrow('Expected "(", NOT, end of input, field name, value, whitespace but "<" found.\n' + - '< 1000\n' + - '^'); + }).toThrow( + 'Expected "(", NOT, end of input, field name, value, whitespace but "<" found.\n' + + '< 1000\n' + + '^' + ); }); - }); diff --git a/packages/kbn-es-query/src/kuery/errors/index.js b/src/plugins/data/common/es_query/kuery/kuery_syntax_error.ts similarity index 55% rename from packages/kbn-es-query/src/kuery/errors/index.js rename to src/plugins/data/common/es_query/kuery/kuery_syntax_error.ts index 82e1aee7b775a1..7c90119fcc1bc3 100644 --- a/packages/kbn-es-query/src/kuery/errors/index.js +++ b/src/plugins/data/common/es_query/kuery/kuery_syntax_error.ts @@ -20,35 +20,46 @@ import { repeat } from 'lodash'; import { i18n } from '@kbn/i18n'; -const endOfInputText = i18n.translate('kbnESQuery.kql.errors.endOfInputText', { +const endOfInputText = i18n.translate('data.common.esQuery.kql.errors.endOfInputText', { defaultMessage: 'end of input', }); -export class KQLSyntaxError extends Error { +const grammarRuleTranslations: Record = { + fieldName: i18n.translate('data.common.esQuery.kql.errors.fieldNameText', { + defaultMessage: 'field name', + }), + value: i18n.translate('data.common.esQuery.kql.errors.valueText', { + defaultMessage: 'value', + }), + literal: i18n.translate('data.common.esQuery.kql.errors.literalText', { + defaultMessage: 'literal', + }), + whitespace: i18n.translate('data.common.esQuery.kql.errors.whitespaceText', { + defaultMessage: 'whitespace', + }), +}; + +interface KQLSyntaxErrorData extends Error { + found: string; + expected: KQLSyntaxErrorExpected[]; + location: any; +} - constructor(error, expression) { - const grammarRuleTranslations = { - fieldName: i18n.translate('kbnESQuery.kql.errors.fieldNameText', { - defaultMessage: 'field name', - }), - value: i18n.translate('kbnESQuery.kql.errors.valueText', { - defaultMessage: 'value', - }), - literal: i18n.translate('kbnESQuery.kql.errors.literalText', { - defaultMessage: 'literal', - }), - whitespace: i18n.translate('kbnESQuery.kql.errors.whitespaceText', { - defaultMessage: 'whitespace', - }), - }; +interface KQLSyntaxErrorExpected { + description: string; +} + +export class KQLSyntaxError extends Error { + shortMessage: string; - const translatedExpectations = error.expected.map((expected) => { + constructor(error: KQLSyntaxErrorData, expression: any) { + const translatedExpectations = error.expected.map(expected => { return grammarRuleTranslations[expected.description] || expected.description; }); const translatedExpectationText = translatedExpectations.join(', '); - const message = i18n.translate('kbnESQuery.kql.errors.syntaxError', { + const message = i18n.translate('data.common.esQuery.kql.errors.syntaxError', { defaultMessage: 'Expected {expectedList} but {foundInput} found.', values: { expectedList: translatedExpectationText, @@ -56,11 +67,9 @@ export class KQLSyntaxError extends Error { }, }); - const fullMessage = [ - message, - expression, - repeat('-', error.location.start.offset) + '^', - ].join('\n'); + const fullMessage = [message, expression, repeat('-', error.location.start.offset) + '^'].join( + '\n' + ); super(fullMessage); this.name = 'KQLSyntaxError'; diff --git a/packages/kbn-es-query/src/kuery/node_types/function.js b/src/plugins/data/common/es_query/kuery/node_types/function.js similarity index 100% rename from packages/kbn-es-query/src/kuery/node_types/function.js rename to src/plugins/data/common/es_query/kuery/node_types/function.js diff --git a/src/plugins/data/common/es_query/kuery/node_types/function.test.ts b/src/plugins/data/common/es_query/kuery/node_types/function.test.ts new file mode 100644 index 00000000000000..ca9798eb6e74f5 --- /dev/null +++ b/src/plugins/data/common/es_query/kuery/node_types/function.test.ts @@ -0,0 +1,75 @@ +/* + * 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 { fields } from '../../../index_patterns/mocks'; + +import { nodeTypes } from './index'; +import { IIndexPattern } from '../../../index_patterns'; + +// @ts-ignore +import { buildNode, buildNodeWithArgumentNodes, toElasticsearchQuery } from './function'; +// @ts-ignore +import { toElasticsearchQuery as isFunctionToElasticsearchQuery } from '../functions/is'; + +describe('kuery node types', () => { + describe('function', () => { + let indexPattern: IIndexPattern; + + beforeEach(() => { + indexPattern = ({ + fields, + } as unknown) as IIndexPattern; + }); + + describe('buildNode', () => { + test('should return a node representing the given kuery function', () => { + const result = buildNode('is', 'extension', 'jpg'); + + expect(result).toHaveProperty('type', 'function'); + expect(result).toHaveProperty('function', 'is'); + expect(result).toHaveProperty('arguments'); + }); + }); + + describe('buildNodeWithArgumentNodes', () => { + test('should return a function node with the given argument list untouched', () => { + const fieldNameLiteral = nodeTypes.literal.buildNode('extension'); + const valueLiteral = nodeTypes.literal.buildNode('jpg'); + const argumentNodes = [fieldNameLiteral, valueLiteral]; + const result = buildNodeWithArgumentNodes('is', argumentNodes); + + expect(result).toHaveProperty('type', 'function'); + expect(result).toHaveProperty('function', 'is'); + expect(result).toHaveProperty('arguments'); + expect(result.arguments).toBe(argumentNodes); + expect(result.arguments).toEqual(argumentNodes); + }); + }); + + describe('toElasticsearchQuery', () => { + test("should return the given function type's ES query representation", () => { + const node = buildNode('is', 'extension', 'jpg'); + const expected = isFunctionToElasticsearchQuery(node, indexPattern); + const result = toElasticsearchQuery(node, indexPattern); + + expect(expected).toEqual(result); + }); + }); + }); +}); diff --git a/packages/kbn-es-query/src/kuery/node_types/index.d.ts b/src/plugins/data/common/es_query/kuery/node_types/index.d.ts similarity index 72% rename from packages/kbn-es-query/src/kuery/node_types/index.d.ts rename to src/plugins/data/common/es_query/kuery/node_types/index.d.ts index daf8032f9fe0ef..720d64e11a0f89 100644 --- a/packages/kbn-es-query/src/kuery/node_types/index.d.ts +++ b/src/plugins/data/common/es_query/kuery/node_types/index.d.ts @@ -21,7 +21,8 @@ * WARNING: these typings are incomplete */ -import { JsonObject, JsonValue } from '..'; +import { IIndexPattern } from '../../../index_patterns'; +import { KueryNode, JsonValue } from '..'; type FunctionName = | 'is' @@ -34,6 +35,17 @@ type FunctionName = | 'geoPolygon' | 'nested'; +interface FunctionType { + buildNode: (functionName: FunctionName, ...args: any[]) => FunctionTypeBuildNode; + buildNodeWithArgumentNodes: (functionName: FunctionName, ...args: any[]) => FunctionTypeBuildNode; + toElasticsearchQuery: ( + node: any, + indexPattern?: IIndexPattern, + config?: Record, + context?: Record + ) => JsonValue; +} + interface FunctionTypeBuildNode { type: 'function'; function: FunctionName; @@ -41,32 +53,40 @@ interface FunctionTypeBuildNode { arguments: any[]; } -interface FunctionType { - buildNode: (functionName: FunctionName, ...args: any[]) => FunctionTypeBuildNode; - buildNodeWithArgumentNodes: (functionName: FunctionName, ...args: any[]) => FunctionTypeBuildNode; - toElasticsearchQuery: (node: any, indexPattern: any, config: JsonObject) => JsonValue; -} - interface LiteralType { - buildNode: ( - value: null | boolean | number | string - ) => { type: 'literal'; value: null | boolean | number | string }; + buildNode: (value: null | boolean | number | string) => LiteralTypeBuildNode; toElasticsearchQuery: (node: any) => null | boolean | number | string; } +interface LiteralTypeBuildNode { + type: 'literal'; + value: null | boolean | number | string; +} + interface NamedArgType { - buildNode: (name: string, value: any) => { type: 'namedArg'; name: string; value: any }; + buildNode: (name: string, value: any) => NamedArgTypeBuildNode; toElasticsearchQuery: (node: any) => string; } +interface NamedArgTypeBuildNode { + type: 'namedArg'; + name: string; + value: any; +} + interface WildcardType { - buildNode: (value: string) => { type: 'wildcard'; value: string }; + buildNode: (value: string) => WildcardTypeBuildNode; test: (node: any, string: string) => boolean; toElasticsearchQuery: (node: any) => string; toQueryStringQuery: (node: any) => string; hasLeadingWildcard: (node: any) => boolean; } +interface WildcardTypeBuildNode { + type: 'wildcard'; + value: string; +} + interface NodeTypes { function: FunctionType; literal: LiteralType; diff --git a/packages/kbn-es-query/src/kuery/node_types/index.js b/src/plugins/data/common/es_query/kuery/node_types/index.js similarity index 100% rename from packages/kbn-es-query/src/kuery/node_types/index.js rename to src/plugins/data/common/es_query/kuery/node_types/index.js diff --git a/packages/kbn-es-query/src/kuery/node_types/literal.js b/src/plugins/data/common/es_query/kuery/node_types/literal.js similarity index 100% rename from packages/kbn-es-query/src/kuery/node_types/literal.js rename to src/plugins/data/common/es_query/kuery/node_types/literal.js diff --git a/packages/kbn-es-query/src/kuery/node_types/__tests__/literal.js b/src/plugins/data/common/es_query/kuery/node_types/literal.test.ts similarity index 54% rename from packages/kbn-es-query/src/kuery/node_types/__tests__/literal.js rename to src/plugins/data/common/es_query/kuery/node_types/literal.test.ts index 25fe2bcc45a453..60fe2d6d1013c6 100644 --- a/packages/kbn-es-query/src/kuery/node_types/__tests__/literal.js +++ b/src/plugins/data/common/es_query/kuery/node_types/literal.test.ts @@ -17,34 +17,27 @@ * under the License. */ -import expect from '@kbn/expect'; -import * as literal from '../literal'; +// @ts-ignore +import { buildNode, toElasticsearchQuery } from './literal'; -describe('kuery node types', function () { +describe('kuery node types', () => { + describe('literal', () => { + describe('buildNode', () => { + test('should return a node representing the given value', () => { + const result = buildNode('foo'); - describe('literal', function () { - - describe('buildNode', function () { - - it('should return a node representing the given value', function () { - const result = literal.buildNode('foo'); - expect(result).to.have.property('type', 'literal'); - expect(result).to.have.property('value', 'foo'); + expect(result).toHaveProperty('type', 'literal'); + expect(result).toHaveProperty('value', 'foo'); }); - }); - describe('toElasticsearchQuery', function () { + describe('toElasticsearchQuery', () => { + test('should return the literal value represented by the given node', () => { + const node = buildNode('foo'); + const result = toElasticsearchQuery(node); - it('should return the literal value represented by the given node', function () { - const node = literal.buildNode('foo'); - const result = literal.toElasticsearchQuery(node); - expect(result).to.be('foo'); + expect(result).toBe('foo'); }); - }); - - }); - }); diff --git a/packages/kbn-es-query/src/kuery/node_types/named_arg.js b/src/plugins/data/common/es_query/kuery/node_types/named_arg.js similarity index 100% rename from packages/kbn-es-query/src/kuery/node_types/named_arg.js rename to src/plugins/data/common/es_query/kuery/node_types/named_arg.js diff --git a/src/plugins/data/common/es_query/kuery/node_types/named_arg.test.ts b/src/plugins/data/common/es_query/kuery/node_types/named_arg.test.ts new file mode 100644 index 00000000000000..36c40d28e55c22 --- /dev/null +++ b/src/plugins/data/common/es_query/kuery/node_types/named_arg.test.ts @@ -0,0 +1,57 @@ +/* + * 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 { nodeTypes } from './index'; + +// @ts-ignore +import { buildNode, toElasticsearchQuery } from './named_arg'; + +describe('kuery node types', () => { + describe('named arg', () => { + describe('buildNode', () => { + test('should return a node representing a named argument with the given value', () => { + const result = buildNode('fieldName', 'foo'); + expect(result).toHaveProperty('type', 'namedArg'); + expect(result).toHaveProperty('name', 'fieldName'); + expect(result).toHaveProperty('value'); + + const literalValue = result.value; + expect(literalValue).toHaveProperty('type', 'literal'); + expect(literalValue).toHaveProperty('value', 'foo'); + }); + + test('should support literal nodes as values', () => { + const value = nodeTypes.literal.buildNode('foo'); + const result = buildNode('fieldName', value); + + expect(result.value).toBe(value); + expect(result.value).toEqual(value); + }); + }); + + describe('toElasticsearchQuery', () => { + test('should return the argument value represented by the given node', () => { + const node = buildNode('fieldName', 'foo'); + const result = toElasticsearchQuery(node); + + expect(result).toBe('foo'); + }); + }); + }); +}); diff --git a/packages/kbn-es-query/src/kuery/node_types/wildcard.js b/src/plugins/data/common/es_query/kuery/node_types/wildcard.js similarity index 100% rename from packages/kbn-es-query/src/kuery/node_types/wildcard.js rename to src/plugins/data/common/es_query/kuery/node_types/wildcard.js diff --git a/src/plugins/data/common/es_query/kuery/node_types/wildcard.test.ts b/src/plugins/data/common/es_query/kuery/node_types/wildcard.test.ts new file mode 100644 index 00000000000000..7e221d96b49e91 --- /dev/null +++ b/src/plugins/data/common/es_query/kuery/node_types/wildcard.test.ts @@ -0,0 +1,110 @@ +/* + * 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 { + buildNode, + wildcardSymbol, + hasLeadingWildcard, + toElasticsearchQuery, + test as testNode, + toQueryStringQuery, + // @ts-ignore +} from './wildcard'; + +describe('kuery node types', () => { + describe('wildcard', () => { + describe('buildNode', () => { + test('should accept a string argument representing a wildcard string', () => { + const wildcardValue = `foo${wildcardSymbol}bar`; + const result = buildNode(wildcardValue); + + expect(result).toHaveProperty('type', 'wildcard'); + expect(result).toHaveProperty('value', wildcardValue); + }); + + test('should accept and parse a wildcard string', () => { + const result = buildNode('foo*bar'); + + expect(result).toHaveProperty('type', 'wildcard'); + expect(result.value).toBe(`foo${wildcardSymbol}bar`); + }); + }); + + describe('toElasticsearchQuery', () => { + test('should return the string representation of the wildcard literal', () => { + const node = buildNode('foo*bar'); + const result = toElasticsearchQuery(node); + + expect(result).toBe('foo*bar'); + }); + }); + + describe('toQueryStringQuery', () => { + test('should return the string representation of the wildcard literal', () => { + const node = buildNode('foo*bar'); + const result = toQueryStringQuery(node); + + expect(result).toBe('foo*bar'); + }); + + test('should escape query_string query special characters other than wildcard', () => { + const node = buildNode('+foo*bar'); + const result = toQueryStringQuery(node); + + expect(result).toBe('\\+foo*bar'); + }); + }); + + describe('test', () => { + test('should return a boolean indicating whether the string matches the given wildcard node', () => { + const node = buildNode('foo*bar'); + + expect(testNode(node, 'foobar')).toBe(true); + expect(testNode(node, 'foobazbar')).toBe(true); + expect(testNode(node, 'foobar')).toBe(true); + expect(testNode(node, 'fooqux')).toBe(false); + expect(testNode(node, 'bazbar')).toBe(false); + }); + + test('should return a true even when the string has newlines or tabs', () => { + const node = buildNode('foo*bar'); + + expect(testNode(node, 'foo\nbar')).toBe(true); + expect(testNode(node, 'foo\tbar')).toBe(true); + }); + }); + + describe('hasLeadingWildcard', () => { + test('should determine whether a wildcard node contains a leading wildcard', () => { + const node = buildNode('foo*bar'); + expect(hasLeadingWildcard(node)).toBe(false); + + const leadingWildcardNode = buildNode('*foobar'); + expect(hasLeadingWildcard(leadingWildcardNode)).toBe(true); + }); + + // Lone wildcards become exists queries, so we aren't worried about their performance + test('should not consider a lone wildcard to be a leading wildcard', () => { + const leadingWildcardNode = buildNode('*'); + + expect(hasLeadingWildcard(leadingWildcardNode)).toBe(false); + }); + }); + }); +}); diff --git a/packages/kbn-es-query/src/kuery/index.d.ts b/src/plugins/data/common/es_query/kuery/types.ts similarity index 73% rename from packages/kbn-es-query/src/kuery/index.d.ts rename to src/plugins/data/common/es_query/kuery/types.ts index b01a8914f68ef3..86cb7e08a767c1 100644 --- a/packages/kbn-es-query/src/kuery/index.d.ts +++ b/src/plugins/data/common/es_query/kuery/types.ts @@ -17,14 +17,29 @@ * under the License. */ -export * from './ast'; +import { NodeTypes } from './node_types'; + +export interface KueryNode { + type: keyof NodeTypes; + [key: string]: any; +} + +export type DslQuery = any; + +export interface KueryParseOptions { + helpers: { + [key: string]: any; + }; + startRule: string; + allowLeadingWildcards: boolean; + errorOnLuceneSyntax: boolean; +} + export { nodeTypes } from './node_types'; +export type JsonArray = JsonValue[]; export type JsonValue = null | boolean | number | string | JsonObject | JsonArray; export interface JsonObject { [key: string]: JsonValue; } - -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface JsonArray extends Array {} diff --git a/src/plugins/data/common/field_formats/converters/custom.ts b/src/plugins/data/common/field_formats/converters/custom.ts index 687870306c8731..1c17e231cace83 100644 --- a/src/plugins/data/common/field_formats/converters/custom.ts +++ b/src/plugins/data/common/field_formats/converters/custom.ts @@ -17,10 +17,10 @@ * under the License. */ -import { FieldFormat } from '../field_format'; +import { FieldFormat, IFieldFormatType } from '../field_format'; import { TextContextTypeConvert, FIELD_FORMAT_IDS } from '../types'; -export const createCustomFieldFormat = (convert: TextContextTypeConvert) => +export const createCustomFieldFormat = (convert: TextContextTypeConvert): IFieldFormatType => class CustomFieldFormat extends FieldFormat { static id = FIELD_FORMAT_IDS.CUSTOM; diff --git a/src/plugins/data/common/field_formats/field_format.ts b/src/plugins/data/common/field_formats/field_format.ts index 6b5f665c6e20e5..dd445a33f21c5f 100644 --- a/src/plugins/data/common/field_formats/field_format.ts +++ b/src/plugins/data/common/field_formats/field_format.ts @@ -73,7 +73,7 @@ export abstract class FieldFormat { */ public type: any = this.constructor; - private readonly _params: any; + protected readonly _params: any; protected getConfig: Function | undefined; constructor(_params: any = {}, getConfig?: Function) { diff --git a/tasks/config/peg.js b/tasks/config/peg.js index 7c3e597ae12d2c..a9d066f3cd49fa 100644 --- a/tasks/config/peg.js +++ b/tasks/config/peg.js @@ -19,8 +19,8 @@ module.exports = { kuery: { - src: 'packages/kbn-es-query/src/kuery/ast/kuery.peg', - dest: 'packages/kbn-es-query/src/kuery/ast/kuery.js', + src: 'src/plugins/data/common/es_query/kuery/ast/kuery.peg', + dest: 'src/plugins/data/common/es_query/kuery/ast/_generated_/kuery.js', options: { allowedStartRules: ['start', 'Literal'] } diff --git a/x-pack/legacy/plugins/apm/public/components/shared/KueryBar/index.tsx b/x-pack/legacy/plugins/apm/public/components/shared/KueryBar/index.tsx index 52be4d4fba7748..32fbe46ac560c4 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/KueryBar/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/KueryBar/index.tsx @@ -7,7 +7,6 @@ import React, { useState } from 'react'; import { uniqueId, startsWith } from 'lodash'; import styled from 'styled-components'; -import { fromKueryExpression, toElasticsearchQuery } from '@kbn/es-query'; import { i18n } from '@kbn/i18n'; import { fromQuery, toQuery } from '../Links/url_helpers'; // @ts-ignore @@ -16,13 +15,14 @@ import { getBoolFilter } from './get_bool_filter'; import { useLocation } from '../../../hooks/useLocation'; import { useUrlParams } from '../../../hooks/useUrlParams'; import { history } from '../../../utils/history'; +import { usePlugins } from '../../../new-platform/plugin'; +import { useDynamicIndexPattern } from '../../../hooks/useDynamicIndexPattern'; import { - AutocompleteSuggestion, AutocompleteProvider, + AutocompleteSuggestion, + esKuery, IIndexPattern } from '../../../../../../../../src/plugins/data/public'; -import { useDynamicIndexPattern } from '../../../hooks/useDynamicIndexPattern'; -import { usePlugins } from '../../../new-platform/plugin'; const Container = styled.div` margin-bottom: 10px; @@ -34,8 +34,8 @@ interface State { } function convertKueryToEsQuery(kuery: string, indexPattern: IIndexPattern) { - const ast = fromKueryExpression(kuery); - return toElasticsearchQuery(ast, indexPattern); + const ast = esKuery.fromKueryExpression(kuery); + return esKuery.toElasticsearchQuery(ast, indexPattern); } function getSuggestions( diff --git a/x-pack/legacy/plugins/apm/server/lib/helpers/convert_ui_filters/get_ui_filters_es.ts b/x-pack/legacy/plugins/apm/server/lib/helpers/convert_ui_filters/get_ui_filters_es.ts index cee097d010212b..a6f6d36ecfc81a 100644 --- a/x-pack/legacy/plugins/apm/server/lib/helpers/convert_ui_filters/get_ui_filters_es.ts +++ b/x-pack/legacy/plugins/apm/server/lib/helpers/convert_ui_filters/get_ui_filters_es.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { toElasticsearchQuery, fromKueryExpression } from '@kbn/es-query'; import { ESFilter } from '../../../../typings/elasticsearch'; import { UIFilters } from '../../../../typings/ui-filters'; import { getEnvironmentUiFilterES } from './get_environment_ui_filter_es'; @@ -12,10 +11,13 @@ import { localUIFilters, localUIFilterNames } from '../../ui_filters/local_ui_filters/config'; -import { StaticIndexPattern } from '../../../../../../../../src/legacy/core_plugins/data/public'; +import { + esKuery, + IIndexPattern +} from '../../../../../../../../src/plugins/data/server'; export function getUiFiltersES( - indexPattern: StaticIndexPattern | undefined, + indexPattern: IIndexPattern | undefined, uiFilters: UIFilters ) { const { kuery, environment, ...localFilterValues } = uiFilters; @@ -43,13 +45,13 @@ export function getUiFiltersES( } function getKueryUiFilterES( - indexPattern: StaticIndexPattern | undefined, + indexPattern: IIndexPattern | undefined, kuery?: string ) { if (!kuery || !indexPattern) { return; } - const ast = fromKueryExpression(kuery); - return toElasticsearchQuery(ast, indexPattern) as ESFilter; + const ast = esKuery.fromKueryExpression(kuery); + return esKuery.toElasticsearchQuery(ast, indexPattern) as ESFilter; } diff --git a/x-pack/legacy/plugins/beats_management/public/lib/adapters/elasticsearch/rest.ts b/x-pack/legacy/plugins/beats_management/public/lib/adapters/elasticsearch/rest.ts index 526728bd77cac6..83c610800b89ba 100644 --- a/x-pack/legacy/plugins/beats_management/public/lib/adapters/elasticsearch/rest.ts +++ b/x-pack/legacy/plugins/beats_management/public/lib/adapters/elasticsearch/rest.ts @@ -4,11 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { fromKueryExpression, toElasticsearchQuery } from '@kbn/es-query'; import { isEmpty } from 'lodash'; import { npStart } from 'ui/new_platform'; import { ElasticsearchAdapter } from './adapter_types'; -import { AutocompleteSuggestion } from '../../../../../../../../src/plugins/data/public'; +import { AutocompleteSuggestion, esKuery } from '../../../../../../../../src/plugins/data/public'; import { setup as data } from '../../../../../../../../src/legacy/core_plugins/data/public/legacy'; const getAutocompleteProvider = (language: string) => @@ -20,7 +19,7 @@ export class RestElasticsearchAdapter implements ElasticsearchAdapter { public isKueryValid(kuery: string): boolean { try { - fromKueryExpression(kuery); + esKuery.fromKueryExpression(kuery); } catch (err) { return false; } @@ -31,9 +30,9 @@ export class RestElasticsearchAdapter implements ElasticsearchAdapter { if (!this.isKueryValid(kuery)) { return ''; } - const ast = fromKueryExpression(kuery); + const ast = esKuery.fromKueryExpression(kuery); const indexPattern = await this.getIndexPattern(); - return JSON.stringify(toElasticsearchQuery(ast, indexPattern)); + return JSON.stringify(esKuery.toElasticsearchQuery(ast, indexPattern)); } public async getSuggestions( kuery: string, diff --git a/x-pack/legacy/plugins/graph/public/components/search_bar.tsx b/x-pack/legacy/plugins/graph/public/components/search_bar.tsx index 82e50c702997f5..56458e5de273fe 100644 --- a/x-pack/legacy/plugins/graph/public/components/search_bar.tsx +++ b/x-pack/legacy/plugins/graph/public/components/search_bar.tsx @@ -9,12 +9,9 @@ import React, { useState, useEffect } from 'react'; import { i18n } from '@kbn/i18n'; import { connect } from 'react-redux'; -import { fromKueryExpression, toElasticsearchQuery } from '@kbn/es-query'; -import { IDataPluginServices, Query } from 'src/plugins/data/public'; import { IndexPatternSavedObject, IndexPatternProvider } from '../types'; import { QueryBarInput, IndexPattern } from '../../../../../../src/legacy/core_plugins/data/public'; import { openSourceModal } from '../services/source_modal'; - import { GraphState, datasourceSelector, @@ -23,6 +20,7 @@ import { } from '../state_management'; import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; +import { IDataPluginServices, Query, esKuery } from '../../../../../../src/plugins/data/public'; export interface OuterSearchBarProps { isLoading: boolean; @@ -44,7 +42,10 @@ export interface SearchBarProps extends OuterSearchBarProps { function queryToString(query: Query, indexPattern: IndexPattern) { if (query.language === 'kuery' && typeof query.query === 'string') { - const dsl = toElasticsearchQuery(fromKueryExpression(query.query as string), indexPattern); + const dsl = esKuery.toElasticsearchQuery( + esKuery.fromKueryExpression(query.query as string), + indexPattern + ); // JSON representation of query will be handled by existing logic. // TODO clean this up and handle it in the data fetch layer once // it moved to typescript. diff --git a/x-pack/legacy/plugins/infra/public/components/metrics_explorer/kuery_bar.tsx b/x-pack/legacy/plugins/infra/public/components/metrics_explorer/kuery_bar.tsx index 1353b065bc4449..a851f8380b9156 100644 --- a/x-pack/legacy/plugins/infra/public/components/metrics_explorer/kuery_bar.tsx +++ b/x-pack/legacy/plugins/infra/public/components/metrics_explorer/kuery_bar.tsx @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { fromKueryExpression } from '@kbn/es-query'; import { i18n } from '@kbn/i18n'; import React, { useEffect, useState } from 'react'; @@ -12,6 +11,7 @@ import { StaticIndexPattern } from 'ui/index_patterns'; import { WithKueryAutocompletion } from '../../containers/with_kuery_autocompletion'; import { AutocompleteField } from '../autocomplete_field'; import { isDisplayable } from '../../utils/is_displayable'; +import { esKuery } from '../../../../../../../src/plugins/data/public'; interface Props { derivedIndexPattern: StaticIndexPattern; @@ -21,7 +21,7 @@ interface Props { function validateQuery(query: string) { try { - fromKueryExpression(query); + esKuery.fromKueryExpression(query); } catch (err) { return false; } diff --git a/x-pack/legacy/plugins/infra/public/store/local/log_filter/selectors.ts b/x-pack/legacy/plugins/infra/public/store/local/log_filter/selectors.ts index 069f631b9c0264..f17f7be4defe98 100644 --- a/x-pack/legacy/plugins/infra/public/store/local/log_filter/selectors.ts +++ b/x-pack/legacy/plugins/infra/public/store/local/log_filter/selectors.ts @@ -5,10 +5,8 @@ */ import { createSelector } from 'reselect'; - -import { fromKueryExpression } from '@kbn/es-query'; - import { LogFilterState } from './reducer'; +import { esKuery } from '../../../../../../../../src/plugins/data/public'; export const selectLogFilterQuery = (state: LogFilterState) => state.filterQuery ? state.filterQuery.query : null; @@ -23,7 +21,7 @@ export const selectIsLogFilterQueryDraftValid = createSelector( filterQueryDraft => { if (filterQueryDraft && filterQueryDraft.kind === 'kuery') { try { - fromKueryExpression(filterQueryDraft.expression); + esKuery.fromKueryExpression(filterQueryDraft.expression); } catch (err) { return false; } diff --git a/x-pack/legacy/plugins/infra/public/store/local/waffle_filter/selectors.ts b/x-pack/legacy/plugins/infra/public/store/local/waffle_filter/selectors.ts index 7d518b5e20f2dd..0acce82950f779 100644 --- a/x-pack/legacy/plugins/infra/public/store/local/waffle_filter/selectors.ts +++ b/x-pack/legacy/plugins/infra/public/store/local/waffle_filter/selectors.ts @@ -6,8 +6,7 @@ import { createSelector } from 'reselect'; -import { fromKueryExpression } from '@kbn/es-query'; - +import { esKuery } from '../../../../../../../../src/plugins/data/public'; import { WaffleFilterState } from './reducer'; export const selectWaffleFilterQuery = (state: WaffleFilterState) => @@ -23,7 +22,7 @@ export const selectIsWaffleFilterQueryDraftValid = createSelector( filterQueryDraft => { if (filterQueryDraft && filterQueryDraft.kind === 'kuery') { try { - fromKueryExpression(filterQueryDraft.expression); + esKuery.fromKueryExpression(filterQueryDraft.expression); } catch (err) { return false; } diff --git a/x-pack/legacy/plugins/infra/public/utils/kuery.ts b/x-pack/legacy/plugins/infra/public/utils/kuery.ts index 4a767f2777512f..2e793d53b46226 100644 --- a/x-pack/legacy/plugins/infra/public/utils/kuery.ts +++ b/x-pack/legacy/plugins/infra/public/utils/kuery.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { fromKueryExpression, toElasticsearchQuery } from '@kbn/es-query'; import { StaticIndexPattern } from 'ui/index_patterns'; +import { esKuery } from '../../../../../../src/plugins/data/public'; export const convertKueryToElasticSearchQuery = ( kueryExpression: string, @@ -13,7 +13,9 @@ export const convertKueryToElasticSearchQuery = ( ) => { try { return kueryExpression - ? JSON.stringify(toElasticsearchQuery(fromKueryExpression(kueryExpression), indexPattern)) + ? JSON.stringify( + esKuery.toElasticsearchQuery(esKuery.fromKueryExpression(kueryExpression), indexPattern) + ) : ''; } catch (err) { return ''; diff --git a/x-pack/legacy/plugins/kuery_autocomplete/public/autocomplete_providers/index.js b/x-pack/legacy/plugins/kuery_autocomplete/public/autocomplete_providers/index.js index 8a82194470ace4..7b0e42283d5f59 100644 --- a/x-pack/legacy/plugins/kuery_autocomplete/public/autocomplete_providers/index.js +++ b/x-pack/legacy/plugins/kuery_autocomplete/public/autocomplete_providers/index.js @@ -5,11 +5,11 @@ */ import { flatten, mapValues, uniq } from 'lodash'; -import { fromKueryExpression } from '@kbn/es-query'; import { getSuggestionsProvider as field } from './field'; import { getSuggestionsProvider as value } from './value'; import { getSuggestionsProvider as operator } from './operator'; import { getSuggestionsProvider as conjunction } from './conjunction'; +import { esKuery } from '../../../../../../src/plugins/data/public'; const cursorSymbol = '@kuery-cursor@'; @@ -27,7 +27,7 @@ export const kueryProvider = ({ config, indexPatterns, boolFilter }) => { let cursorNode; try { - cursorNode = fromKueryExpression(cursoredQuery, { cursorSymbol, parseCursor: true }); + cursorNode = esKuery.fromKueryExpression(cursoredQuery, { cursorSymbol, parseCursor: true }); } catch (e) { cursorNode = {}; } diff --git a/x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/utils.js b/x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/utils.js index 16e4a563c33ae8..18b3382175fdd7 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/utils.js +++ b/x-pack/legacy/plugins/ml/public/application/components/kql_filter_bar/utils.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { npStart } from 'ui/new_platform'; -import { fromKueryExpression, toElasticsearchQuery } from '@kbn/es-query'; +import { esKuery } from '../../../../../../../../src/plugins/data/public'; const getAutocompleteProvider = language => npStart.plugins.data.autocomplete.getProvider(language); @@ -35,8 +35,8 @@ export async function getSuggestions( } function convertKueryToEsQuery(kuery, indexPattern) { - const ast = fromKueryExpression(kuery); - return toElasticsearchQuery(ast, indexPattern); + const ast = esKuery.fromKueryExpression(kuery); + return esKuery.toElasticsearchQuery(ast, indexPattern); } // Recommended by MDN for escaping user input to be treated as a literal string within a regular expression // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions @@ -53,7 +53,7 @@ export function escapeDoubleQuotes(string) { } export function getKqlQueryValues(inputValue, indexPattern) { - const ast = fromKueryExpression(inputValue); + const ast = esKuery.fromKueryExpression(inputValue); const isAndOperator = (ast.function === 'and'); const query = convertKueryToEsQuery(inputValue, indexPattern); const filteredFields = []; diff --git a/x-pack/legacy/plugins/siem/public/lib/keury/index.ts b/x-pack/legacy/plugins/siem/public/lib/keury/index.ts index b6819e54575d60..d82079dd05d31e 100644 --- a/x-pack/legacy/plugins/siem/public/lib/keury/index.ts +++ b/x-pack/legacy/plugins/siem/public/lib/keury/index.ts @@ -4,12 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { fromKueryExpression, toElasticsearchQuery, JsonObject } from '@kbn/es-query'; import { isEmpty, isString, flow } from 'lodash/fp'; import { Query, esFilters, esQuery, + esKuery, IIndexPattern, } from '../../../../../../../src/plugins/data/public'; @@ -21,7 +21,9 @@ export const convertKueryToElasticSearchQuery = ( ) => { try { return kueryExpression - ? JSON.stringify(toElasticsearchQuery(fromKueryExpression(kueryExpression), indexPattern)) + ? JSON.stringify( + esKuery.toElasticsearchQuery(esKuery.fromKueryExpression(kueryExpression), indexPattern) + ) : ''; } catch (err) { return ''; @@ -31,10 +33,10 @@ export const convertKueryToElasticSearchQuery = ( export const convertKueryToDslFilter = ( kueryExpression: string, indexPattern: IIndexPattern -): JsonObject => { +): esKuery.JsonObject => { try { return kueryExpression - ? toElasticsearchQuery(fromKueryExpression(kueryExpression), indexPattern) + ? esKuery.toElasticsearchQuery(esKuery.fromKueryExpression(kueryExpression), indexPattern) : {}; } catch (err) { return {}; @@ -55,7 +57,7 @@ export const escapeQueryValue = (val: number | string = ''): string | number => export const isFromKueryExpressionValid = (kqlFilterQuery: KueryFilterQuery | null): boolean => { if (kqlFilterQuery && kqlFilterQuery.kind === 'kuery') { try { - fromKueryExpression(kqlFilterQuery.expression); + esKuery.fromKueryExpression(kqlFilterQuery.expression); } catch (err) { return false; } diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_define_rule/schema.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_define_rule/schema.tsx index 500557a2c2a96f..58a9e57b32ce60 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_define_rule/schema.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_define_rule/schema.tsx @@ -6,7 +6,6 @@ import { i18n } from '@kbn/i18n'; import { EuiText } from '@elastic/eui'; -import { fromKueryExpression } from '@kbn/es-query'; import { isEmpty } from 'lodash/fp'; import React from 'react'; @@ -17,6 +16,7 @@ import { } from '../../../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib'; import { fieldValidators } from '../../../../../../../../../../src/plugins/es_ui_shared/static/forms/helpers'; import { ERROR_CODE } from '../../../../../../../../../../src/plugins/es_ui_shared/static/forms/helpers/field_validators/types'; +import { esKuery } from '../../../../../../../../../../src/plugins/data/public'; import * as CreateRuleI18n from '../../translations'; @@ -106,7 +106,7 @@ export const schema: FormSchema = { const { query } = value as FieldValueQueryBar; if (!isEmpty(query.query as string) && query.language === 'kuery') { try { - fromKueryExpression(query.query); + esKuery.fromKueryExpression(query.query); } catch (err) { return { code: 'ERR_FIELD_FORMAT', diff --git a/x-pack/legacy/plugins/transform/public/app/lib/kibana/common.ts b/x-pack/legacy/plugins/transform/public/app/lib/kibana/common.ts index 16100089a9e56f..b465392a50ae14 100644 --- a/x-pack/legacy/plugins/transform/public/app/lib/kibana/common.ts +++ b/x-pack/legacy/plugins/transform/public/app/lib/kibana/common.ts @@ -104,7 +104,7 @@ export function createSearchItems( const filters = fs.length ? fs : []; const esQueryConfigs = esQuery.getEsQueryConfig(config); - combinedQuery = esQuery.buildEsQuery(indexPattern || null, [query], filters, esQueryConfigs); + combinedQuery = esQuery.buildEsQuery(indexPattern, [query], filters, esQueryConfigs); } return { diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/kuery_bar/index.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/kuery_bar/index.tsx index f529c9cd9d53fc..da392660eb70e8 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/kuery_bar/index.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/kuery_bar/index.tsx @@ -9,14 +9,17 @@ import { uniqueId, startsWith } from 'lodash'; import { EuiCallOut } from '@elastic/eui'; import styled from 'styled-components'; import { FormattedMessage } from '@kbn/i18n/react'; -import { fromKueryExpression, toElasticsearchQuery } from '@kbn/es-query'; -import { AutocompleteProviderRegister, AutocompleteSuggestion } from 'src/plugins/data/public'; -import { StaticIndexPattern } from 'src/legacy/core_plugins/data/public/index_patterns/index_patterns'; import { Typeahead } from './typeahead'; import { getIndexPattern } from '../../../lib/adapters/index_pattern'; import { UptimeSettingsContext } from '../../../contexts'; import { useUrlParams } from '../../../hooks'; import { toStaticIndexPattern } from '../../../lib/helper'; +import { + AutocompleteProviderRegister, + AutocompleteSuggestion, + esKuery, + IIndexPattern, +} from '../../../../../../../../src/plugins/data/public'; const Container = styled.div` margin-bottom: 10px; @@ -27,15 +30,15 @@ interface State { isLoadingIndexPattern: boolean; } -function convertKueryToEsQuery(kuery: string, indexPattern: unknown) { - const ast = fromKueryExpression(kuery); - return toElasticsearchQuery(ast, indexPattern); +function convertKueryToEsQuery(kuery: string, indexPattern: IIndexPattern) { + const ast = esKuery.fromKueryExpression(kuery); + return esKuery.toElasticsearchQuery(ast, indexPattern); } function getSuggestions( query: string, selectionStart: number, - apmIndexPattern: StaticIndexPattern, + apmIndexPattern: IIndexPattern, autocomplete: Pick ) { const autocompleteProvider = autocomplete.getProvider('kuery'); diff --git a/x-pack/legacy/plugins/uptime/public/pages/overview.tsx b/x-pack/legacy/plugins/uptime/public/pages/overview.tsx index ded16c3f8eb2f0..09d40d32b696c1 100644 --- a/x-pack/legacy/plugins/uptime/public/pages/overview.tsx +++ b/x-pack/legacy/plugins/uptime/public/pages/overview.tsx @@ -6,10 +6,8 @@ import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { fromKueryExpression, toElasticsearchQuery } from '@kbn/es-query'; import React, { Fragment, useContext, useEffect, useState } from 'react'; import styled from 'styled-components'; -import { AutocompleteProviderRegister } from 'src/plugins/data/public'; import { getOverviewPageBreadcrumbs } from '../breadcrumbs'; import { EmptyState, @@ -26,6 +24,7 @@ import { stringifyUrlParams } from '../lib/helper/stringify_url_params'; import { useTrackPageview } from '../../../infra/public'; import { getIndexPattern } from '../lib/adapters/index_pattern'; import { combineFiltersAndUserSearch, stringifyKueries, toStaticIndexPattern } from '../lib/helper'; +import { AutocompleteProviderRegister, esKuery } from '../../../../../../src/plugins/data/public'; interface OverviewPageProps { basePath: string; @@ -109,8 +108,8 @@ export const OverviewPage = ({ if (indexPattern) { const staticIndexPattern = toStaticIndexPattern(indexPattern); const combinedFilterString = combineFiltersAndUserSearch(filterQueryString, kueryString); - const ast = fromKueryExpression(combinedFilterString); - const elasticsearchQuery = toElasticsearchQuery(ast, staticIndexPattern); + const ast = esKuery.fromKueryExpression(combinedFilterString); + const elasticsearchQuery = esKuery.toElasticsearchQuery(ast, staticIndexPattern); filters = JSON.stringify(elasticsearchQuery); } } diff --git a/x-pack/package.json b/x-pack/package.json index eccc5918e6d506..d97fd38676bde7 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -185,7 +185,6 @@ "@kbn/babel-preset": "1.0.0", "@kbn/config-schema": "1.0.0", "@kbn/elastic-idx": "1.0.0", - "@kbn/es-query": "1.0.0", "@kbn/i18n": "1.0.0", "@kbn/interpreter": "1.0.0", "@kbn/ui-framework": "1.0.0", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index f5fc453557122e..217b20797492a6 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -2527,12 +2527,12 @@ "kbnDocViews.table.fieldNamesBeginningWithUnderscoreUnsupportedAriaLabel": "警告", "kbnDocViews.table.noCachedMappingForThisFieldAriaLabel": "警告", "kbnDocViews.table.toggleFieldDetails": "フィールド詳細を切り替える", - "kbnESQuery.kql.errors.endOfInputText": "インプットの終わり", - "kbnESQuery.kql.errors.fieldNameText": "フィールド名", - "kbnESQuery.kql.errors.literalText": "文字通り", - "kbnESQuery.kql.errors.syntaxError": "{expectedList} が予測されましたが {foundInput} が検出されました。", - "kbnESQuery.kql.errors.valueText": "値", - "kbnESQuery.kql.errors.whitespaceText": "ホワイトスペース", + "data.common.esQuery.kql.errors.endOfInputText": "インプットの終わり", + "data.common.esQuery.kql.errors.fieldNameText": "フィールド名", + "data.common.esQuery.kql.errors.literalText": "文字通り", + "data.common.esQuery.kql.errors.syntaxError": "{expectedList} が予測されましたが {foundInput} が検出されました。", + "data.common.esQuery.kql.errors.valueText": "値", + "data.common.esQuery.kql.errors.whitespaceText": "ホワイトスペース", "kbnVislibVisTypes.area.areaDescription": "折れ線グラフの下の数量を強調します。", "kbnVislibVisTypes.area.areaTitle": "エリア", "kbnVislibVisTypes.area.groupTitle": "系列を分割", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 288fc92be3cbd8..6a2ba20af7714b 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -2528,12 +2528,12 @@ "kbnDocViews.table.fieldNamesBeginningWithUnderscoreUnsupportedAriaLabel": "警告", "kbnDocViews.table.noCachedMappingForThisFieldAriaLabel": "警告", "kbnDocViews.table.toggleFieldDetails": "切换字段详细信息", - "kbnESQuery.kql.errors.endOfInputText": "输入结束", - "kbnESQuery.kql.errors.fieldNameText": "字段名称", - "kbnESQuery.kql.errors.literalText": "文本", - "kbnESQuery.kql.errors.syntaxError": "应为 {expectedList},但却找到了 {foundInput}。", - "kbnESQuery.kql.errors.valueText": "值", - "kbnESQuery.kql.errors.whitespaceText": "空白", + "data.common.esQuery.kql.errors.endOfInputText": "输入结束", + "data.common.esQuery.kql.errors.fieldNameText": "字段名称", + "data.common.esQuery.kql.errors.literalText": "文本", + "data.common.esQuery.kql.errors.syntaxError": "应为 {expectedList},但却找到了 {foundInput}。", + "data.common.esQuery.kql.errors.valueText": "值", + "data.common.esQuery.kql.errors.whitespaceText": "空白", "kbnVislibVisTypes.area.areaDescription": "突出折线图下方的数量", "kbnVislibVisTypes.area.areaTitle": "面积图", "kbnVislibVisTypes.area.groupTitle": "拆分序列", From a234e8b836007754240b4e215b865df1ad0e5fee Mon Sep 17 00:00:00 2001 From: Lisa Cawley Date: Tue, 26 Nov 2019 12:36:35 -0800 Subject: [PATCH 12/12] [DOCS] Fixes broken links (#51634) --- docs/api/role-management/put.asciidoc | 4 ++- docs/developer/security/rbac.asciidoc | 9 ++++++- .../tutorial-full-experience.asciidoc | 2 +- .../tutorial-sample-data.asciidoc | 4 +-- docs/management/managing-indices.asciidoc | 2 +- docs/setup/install.asciidoc | 4 +-- .../security/authentication/index.asciidoc | 25 ++++++++++--------- docs/user/security/reporting.asciidoc | 8 +++--- docs/user/security/securing-kibana.asciidoc | 2 +- 9 files changed, 35 insertions(+), 25 deletions(-) diff --git a/docs/api/role-management/put.asciidoc b/docs/api/role-management/put.asciidoc index 67ec15892afe42..a00fedf7e7ac4c 100644 --- a/docs/api/role-management/put.asciidoc +++ b/docs/api/role-management/put.asciidoc @@ -26,7 +26,9 @@ To use the create or update role API, you must have the `manage_security` cluste (Optional, object) In the `metadata` object, keys that begin with `_` are reserved for system usage. `elasticsearch`:: - (Optional, object) {es} cluster and index privileges. Valid keys include `cluster`, `indices`, and `run_as`. For more information, see {xpack-ref}/defining-roles.html[Defining Roles]. + (Optional, object) {es} cluster and index privileges. Valid keys include + `cluster`, `indices`, and `run_as`. For more information, see + {ref}/defining-roles.html[Defining roles]. `kibana`:: (list) Objects that specify the <> for the role: diff --git a/docs/developer/security/rbac.asciidoc b/docs/developer/security/rbac.asciidoc index b967dabf0684f6..02b8233a9a3df2 100644 --- a/docs/developer/security/rbac.asciidoc +++ b/docs/developer/security/rbac.asciidoc @@ -1,7 +1,14 @@ [[development-security-rbac]] === Role-based access control -Role-based access control (RBAC) in {kib} relies upon the {xpack-ref}/security-privileges.html#application-privileges[application privileges] that Elasticsearch exposes. This allows {kib} to define the privileges that {kib} wishes to grant to users, assign them to the relevant users using roles, and then authorize the user to perform a specific action. This is handled within a secured instance of the `SavedObjectsClient` and available transparently to consumers when using `request.getSavedObjectsClient()` or `savedObjects.getScopedSavedObjectsClient()`. +Role-based access control (RBAC) in {kib} relies upon the +{ref}/security-privileges.html#application-privileges[application privileges] +that Elasticsearch exposes. This allows {kib} to define the privileges that +{kib} wishes to grant to users, assign them to the relevant users using roles, +and then authorize the user to perform a specific action. This is handled within +a secured instance of the `SavedObjectsClient` and available transparently to +consumers when using `request.getSavedObjectsClient()` or +`savedObjects.getScopedSavedObjectsClient()`. [[development-rbac-privileges]] ==== {kib} Privileges diff --git a/docs/getting-started/tutorial-full-experience.asciidoc b/docs/getting-started/tutorial-full-experience.asciidoc index eafbb7d8f7c918..a05205fceab4ad 100644 --- a/docs/getting-started/tutorial-full-experience.asciidoc +++ b/docs/getting-started/tutorial-full-experience.asciidoc @@ -91,7 +91,7 @@ and whether it's _tokenized_, or broken up into separate words. NOTE: If security is enabled, you must have the `all` Kibana privilege to run this tutorial. You must also have the `create`, `manage` `read`, `write,` and `delete` -index privileges. See {xpack-ref}/security-privileges.html[Security Privileges] +index privileges. See {ref}/security-privileges.html[Security privileges] for more information. In Kibana *Dev Tools > Console*, set up a mapping for the Shakespeare data set: diff --git a/docs/getting-started/tutorial-sample-data.asciidoc b/docs/getting-started/tutorial-sample-data.asciidoc index 24cc176d5daf9f..f41c648a3d492d 100644 --- a/docs/getting-started/tutorial-sample-data.asciidoc +++ b/docs/getting-started/tutorial-sample-data.asciidoc @@ -12,8 +12,8 @@ with Kibana sample data and learn to: NOTE: If security is enabled, you must have `read`, `write`, and `manage` privileges -on the `kibana_sample_data_*` indices. See {xpack-ref}/security-privileges.html[Security Privileges] -for more information. +on the `kibana_sample_data_*` indices. See +{ref}/security-privileges.html[Security privileges] for more information. [float] diff --git a/docs/management/managing-indices.asciidoc b/docs/management/managing-indices.asciidoc index 4a736e3ddab59d..4c7f6c2aee6e6f 100644 --- a/docs/management/managing-indices.asciidoc +++ b/docs/management/managing-indices.asciidoc @@ -22,7 +22,7 @@ If security is enabled, you must have the `monitor` cluster privilege and the `view_index_metadata` and `manage` index privileges to view the data. For index templates, you must have the `manage_index_templates` cluster privilege. -See {xpack-ref}/security-privileges.html[Security Privileges] for more +See {ref}/security-privileges.html[Security privileges] for more information. Before using this feature, you should be familiar with index management diff --git a/docs/setup/install.asciidoc b/docs/setup/install.asciidoc index b0893a6e789454..286fed34f64c56 100644 --- a/docs/setup/install.asciidoc +++ b/docs/setup/install.asciidoc @@ -54,8 +54,8 @@ Formulae are available from the Elastic Homebrew tap for installing {kib} on mac <> IMPORTANT: If your Elasticsearch installation is protected by -{xpack-ref}/elasticsearch-security.html[{security}] see -{kibana-ref}/using-kibana-with-security.html[Configuring Security in Kibana] for +{ref}/elasticsearch-security.html[{security}] see +{kibana-ref}/using-kibana-with-security.html[Configuring security in Kibana] for additional setup instructions. include::install/targz.asciidoc[] diff --git a/docs/user/security/authentication/index.asciidoc b/docs/user/security/authentication/index.asciidoc index e6b70fa059fc28..32f341a9c1b7cc 100644 --- a/docs/user/security/authentication/index.asciidoc +++ b/docs/user/security/authentication/index.asciidoc @@ -14,16 +14,17 @@ - <> [[basic-authentication]] -==== Basic Authentication +==== Basic authentication Basic authentication requires a username and password to successfully log in to {kib}. It is enabled by default and based on the Native security realm provided by {es}. The basic authentication provider uses a Kibana provided login form, and supports authentication using the `Authorization` request header's `Basic` scheme. The session cookies that are issued by the basic authentication provider are stateless. Therefore, logging out of Kibana when using the basic authentication provider clears the session cookies from the browser but does not invalidate the session cookie for reuse. -For more information about basic authentication and built-in users, see {xpack-ref}/setting-up-authentication.html[Setting Up User Authentication]. +For more information about basic authentication and built-in users, see +{ref}/setting-up-authentication.html[User authentication]. [[token-authentication]] -==== Token Authentication +==== Token authentication Token authentication allows users to login using the same Kibana provided login form as basic authentication. The token authentication provider is built on {es}'s token APIs. The bearer tokens returned by {es}'s {ref}/security-api-get-token.html[get token API] can be used directly with Kibana using the `Authorization` request header with the `Bearer` scheme. @@ -46,7 +47,7 @@ xpack.security.authc.providers: [token, basic] -------------------------------------------------------------------------------- [[pki-authentication]] -==== Public Key Infrastructure (PKI) Authentication +==== Public key infrastructure (PKI) authentication [IMPORTANT] ============================================================================ @@ -76,9 +77,9 @@ xpack.security.authc.providers: [pki, basic] Note that with `server.ssl.clientAuthentication` set to `required`, users are asked to provide a valid client certificate, even if they want to authenticate with username and password. Depending on the security policies, it may or may not be desired. If not, `server.ssl.clientAuthentication` can be set to `optional`. In this case, {kib} still requests a client certificate, but the client won't be required to present one. The `optional` client authentication mode might also be needed in other cases, for example, when PKI authentication is used in conjunction with Reporting. [[saml]] -==== SAML Single Sign-On +==== SAML single sign-on -SAML authentication allows users to log in to {kib} with an external Identity Provider, such as Okta or Auth0. Make sure that SAML is enabled and configured in {es} before setting it up in {kib}. See {xpack-ref}/saml-guide.html[Configuring SAML Single-Sign-On on the Elastic Stack]. +SAML authentication allows users to log in to {kib} with an external Identity Provider, such as Okta or Auth0. Make sure that SAML is enabled and configured in {es} before setting it up in {kib}. See {ref}/saml-guide.html[Configuring SAML single sign-on on the Elastic Stack]. Set the configuration values in `kibana.yml` as follows: @@ -106,7 +107,7 @@ server.xsrf.whitelist: [/api/security/saml/callback] Users will be able to log in to {kib} via SAML Single Sign-On by navigating directly to the {kib} URL. Users who aren't authenticated are redirected to the Identity Provider for login. Most Identity Providers maintain a long-lived session—users who logged in to a different application using the same Identity Provider in the same browser are automatically authenticated. An exception is if {es} or the Identity Provider is configured to force user to re-authenticate. This login scenario is called _Service Provider initiated login_. [float] -===== SAML and Basic Authentication +===== SAML and basic authentication SAML support in {kib} is designed to be the primary (or sole) authentication method for users of that {kib} instance. However, you can configure both SAML and Basic authentication for the same {kib} instance: @@ -135,7 +136,7 @@ xpack.security.authc.saml.maxRedirectURLSize: 1kb -------------------------------------------------------------------------------- [[oidc]] -==== OpenID Connect Single Sign-On +==== OpenID Connect single sign-on Similar to SAML, authentication with OpenID Connect allows users to log in to {kib} using an OpenID Connect Provider such as Google, or Okta. OpenID Connect should also be configured in {es}. For more details, see {ref}/oidc-guide.html[Configuring single sign-on to the {stack} using OpenID Connect]. @@ -166,7 +167,7 @@ server.xsrf.whitelist: [/api/security/v1/oidc] -------------------------------------------------------------------------------- [float] -===== OpenID Connect and Basic Authentication +===== OpenID Connect and basic authentication Similar to SAML, OpenID Connect support in {kib} is designed to be the primary (or sole) authentication method for users of that {kib} instance. However, you can configure both OpenID Connect and Basic authentication for the same {kib} instance: @@ -179,12 +180,12 @@ xpack.security.authc.providers: [oidc, basic] Users will be able to access the login page and use Basic authentication by navigating to the `/login` URL. [float] -==== Single Sign-On provider details +==== Single sign-on provider details The following sections apply both to <> and <> [float] -===== Access and Refresh Tokens +===== Access and refresh tokens Once the user logs in to {kib} Single Sign-On, either using SAML or OpenID Connect, {es} issues access and refresh tokens that {kib} encrypts and stores them in its own session cookie. This way, the user isn't redirected to the Identity Provider @@ -202,7 +203,7 @@ If {kib} can't redirect the user to the external authentication provider (for ex indicates that both access and refresh tokens are expired. Reloading the current {kib} page fixes the error. [float] -===== Local and Global Logout +===== Local and global logout During logout, both the {kib} session cookie and access/refresh token pair are invalidated. Even if the cookie has been leaked, it can't be re-used after logout. This is known as "local" logout. diff --git a/docs/user/security/reporting.asciidoc b/docs/user/security/reporting.asciidoc index fb40dc17c0abd9..aaba60ca4b3cac 100644 --- a/docs/user/security/reporting.asciidoc +++ b/docs/user/security/reporting.asciidoc @@ -8,7 +8,7 @@ user actions in {kib}. To use {reporting} with {security} enabled, you need to <>. If you are automatically generating reports with -{xpack-ref}/xpack-alerting.html[{watcher}], you also need to configure {watcher} +{ref}/xpack-alerting.html[{watcher}], you also need to configure {watcher} to trust the {kib} server's certificate. For more information, see <>. @@ -35,7 +35,7 @@ POST /_security/user/reporter * If you are using an LDAP or Active Directory realm, you can either assign roles on a per user basis, or assign roles to groups of users. By default, role mappings are configured in -{xpack-ref}/mapping-roles.html[`config/shield/role_mapping.yml`]. +{ref}/mapping-roles.html[`config/shield/role_mapping.yml`]. For example, the following snippet assigns the user named Bill Murray the `kibana_user` and `reporting_user` roles: + @@ -55,7 +55,7 @@ In a production environment, you should restrict access to the {reporting} endpoints to authorized users. This requires that you: . Enable {security} on your {es} cluster. For more information, -see {xpack-ref}/security-getting-started.html[Getting Started with Security]. +see {ref}/security-getting-started.html[Getting Started with Security]. . Configure an SSL certificate for Kibana. For more information, see <>. . Configure {watcher} to trust the Kibana server's certificate by adding it to @@ -83,4 +83,4 @@ includes a watch that submits requests as the built-in `elastic` user: <>. For more information about configuring watches, see -{xpack-ref}/how-watcher-works.html[How Watcher Works]. +{ref}/how-watcher-works.html[How Watcher works]. diff --git a/docs/user/security/securing-kibana.asciidoc b/docs/user/security/securing-kibana.asciidoc index 2fbc6ba4f1ee64..60f5473f43b9df 100644 --- a/docs/user/security/securing-kibana.asciidoc +++ b/docs/user/security/securing-kibana.asciidoc @@ -121,7 +121,7 @@ TIP: You can define as many different roles for your {kib} users as you need. For example, create roles that have `read` and `view_index_metadata` privileges on specific index patterns. For more information, see -{xpack-ref}/authorization.html[Configuring Role-based Access Control]. +{ref}/authorization.html[User authorization]. --