diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsservicesetup.md b/docs/development/core/server/kibana-plugin-server.savedobjectsservicesetup.md index b6f2e7320c48a..963c4bbeb5515 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsservicesetup.md +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsservicesetup.md @@ -16,6 +16,8 @@ export interface SavedObjectsServiceSetup When plugins access the Saved Objects client, a new client is created using the factory provided to `setClientFactory` and wrapped by all wrappers registered through `addClientWrapper`. +All the setup APIs will throw if called after the service has started, and therefor cannot be used from legacy plugin code. Legacy plugins should use the legacy savedObject service until migrated. + ## Example 1 diff --git a/docs/images/visualize_coordinate_map_example.png b/docs/images/visualize_coordinate_map_example.png new file mode 100644 index 0000000000000..24f03376adade Binary files /dev/null and b/docs/images/visualize_coordinate_map_example.png differ diff --git a/docs/images/visualize_heat_map_example.png b/docs/images/visualize_heat_map_example.png new file mode 100644 index 0000000000000..2522e91f4dbc7 Binary files /dev/null and b/docs/images/visualize_heat_map_example.png differ diff --git a/docs/images/visualize_region_map_example.png b/docs/images/visualize_region_map_example.png new file mode 100644 index 0000000000000..cf89e92625ece Binary files /dev/null and b/docs/images/visualize_region_map_example.png differ diff --git a/docs/redirects.asciidoc b/docs/redirects.asciidoc index 65de5c4f93d12..3843cc27defd5 100644 --- a/docs/redirects.asciidoc +++ b/docs/redirects.asciidoc @@ -43,7 +43,7 @@ This page was deleted. See <> and <>. [role="exclude",id="xpack-dashboard-only-mode"] == Dashboard-only mode -Using the `kibana_dashboard_only_user` role is deprecated. +Using the `kibana_dashboard_only_user` role is deprecated. Use <> instead. [role="exclude",id="pdf-layout-modes"] @@ -56,4 +56,7 @@ This page has moved. Please see <>. This page has moved. Please see <>. +[role="exclude",id="tilemap"] +== Coordinate map +This page has moved. Please see <>. diff --git a/docs/setup/settings.asciidoc b/docs/setup/settings.asciidoc index 2d4d00b730109..7d0adb9b0e7ef 100644 --- a/docs/setup/settings.asciidoc +++ b/docs/setup/settings.asciidoc @@ -221,14 +221,14 @@ requests. Supported on Elastic Cloud Enterprise. Set to false to disable connections to Elastic Maps Service. When `includeElasticMapsService` is turned off, only the vector layers configured by `map.regionmap` and the tile layer configured by `map.tilemap.url` will be available in the <>, -<>, and <>. +<>, and <>. `map.proxyElasticMapsServiceInMaps:`:: *Default: false* Set to true to proxy all <> Elastic Maps Service requests through the Kibana server. -This setting does not impact <> and <>. +This setting does not impact <> and <>. [[regionmap-settings]] `map.regionmap:`:: Specifies additional vector layers for -use in <> visualizations. Supported on {ece}. Each layer +use in <> visualizations. Supported on {ece}. Each layer object points to an external vector file that contains a geojson FeatureCollection. The file must use the https://en.wikipedia.org/wiki/World_Geodetic_System[WGS84 coordinate reference system (ESPG:4326)] @@ -254,7 +254,7 @@ The following example shows a valid regionmap configuration. [[regionmap-ES-map]]`map.includeElasticMapsService:`:: Turns on or off whether layers from the Elastic Maps Service should be included in the vector layer option list. Supported on Elastic Cloud Enterprise. By turning this off, -only the layers that are configured here will be included. The default is `true`. +only the layers that are configured here will be included. The default is `true`. This also affects whether tile-service from the Elastic Maps Service will be available. [[regionmap-attribution]]`map.regionmap.layers[].attribution:`:: Optional. diff --git a/docs/user/monitoring/elasticsearch-details.asciidoc b/docs/user/monitoring/elasticsearch-details.asciidoc index 2990e965be03c..c0e804672d298 100644 --- a/docs/user/monitoring/elasticsearch-details.asciidoc +++ b/docs/user/monitoring/elasticsearch-details.asciidoc @@ -14,7 +14,7 @@ the <>, <>, [role="screenshot"] image::user/monitoring/images/monitoring-elasticsearch.jpg["Monitoring clusters"] -See also {ref}/es-monitoring.html[Monitoring {es}]. +See also {ref}/monitor-elasticsearch-cluster.html[Monitor a cluster]. [float] [[cluster-overview-page]] diff --git a/docs/user/visualize.asciidoc b/docs/user/visualize.asciidoc index 1bcbd51a9629a..a78b4604ed1e6 100644 --- a/docs/user/visualize.asciidoc +++ b/docs/user/visualize.asciidoc @@ -45,11 +45,11 @@ data sets. [horizontal] <>:: The most powerful way of visualizing map data in {kib}. -<>:: Displays points on a map using a geohash aggregation. +<>:: Displays points on a map using a geohash aggregation. -<>:: Merge any structured map data onto a shape. +<>:: Merge any structured map data onto a shape. -<>:: Display shaded cells within a matrix. +<>:: Display shaded cells within a matrix. * *<>* [horizontal] @@ -136,8 +136,6 @@ include::{kib-repo-dir}/visualize/tsvb.asciidoc[] include::{kib-repo-dir}/visualize/timelion.asciidoc[] include::{kib-repo-dir}/visualize/tilemap.asciidoc[] -include::{kib-repo-dir}/visualize/regionmap.asciidoc[] -include::{kib-repo-dir}/visualize/heatmap.asciidoc[] include::{kib-repo-dir}/visualize/for-dashboard.asciidoc[] diff --git a/docs/visualize/heatmap.asciidoc b/docs/visualize/heatmap.asciidoc deleted file mode 100644 index 18c4018213390..0000000000000 --- a/docs/visualize/heatmap.asciidoc +++ /dev/null @@ -1,40 +0,0 @@ -[[heatmap]] -== Heat map - -Heat maps are graphical representations of data where the individual values are represented as colors. - -NOTE: Heat map has been replaced with <>, which offers more functionality and is easier to use. - -[float] -[[heatmap-aggregation]] -=== Supported aggregations - -Heat maps support the following aggregations: - -* <> - -* <> - -* <> - -* <> - -[float] -[[navigate-heatmap]] -=== Change the color ranges - -When only one color displays on the heat map, you might need to change the color ranges. - -To specify the number of color ranges: - -. Click *Options*. - -. Enter the *Number of colors* to display. - -To specify custom ranges: - -. Click *Options*. - -. Select *Use custom ranges*. - -. Enter the ranges to display. diff --git a/docs/visualize/metric.asciidoc b/docs/visualize/metric.asciidoc deleted file mode 100644 index ddcf5fe3b73bd..0000000000000 --- a/docs/visualize/metric.asciidoc +++ /dev/null @@ -1,16 +0,0 @@ -[[metric-chart]] -=== Metric - -Click the *Options* tab to display the font size slider. - -[float] -[[metric-aggregation]] -==== Supported aggregations - -Metric support the following aggregations: - -* <> - -* <> - -* <> diff --git a/docs/visualize/regionmap.asciidoc b/docs/visualize/regionmap.asciidoc deleted file mode 100644 index accabd16e5fcd..0000000000000 --- a/docs/visualize/regionmap.asciidoc +++ /dev/null @@ -1,53 +0,0 @@ -[[regionmap]] -== Region Maps - -Region maps are thematic maps in which boundary vector shapes are colored using a gradient: -higher intensity colors indicate larger values, and lower intensity colors indicate smaller values. -These are also known as choropleth maps. - -Kibana’s out-of-the-box settings do not show a region map in the New Visualization menu. Use <> instead, which offers more functionality and is easier to use. -If you want to create new region map visualizations, set `xpack.maps.showMapVisualizationTypes` to `true`. - -image::images/regionmap.png[] - -[float] -[[regionmap-configuration]] -=== Configuration - -To create a region map, you configure an inner join that joins the result of an Elasticsearch terms aggregation -and a reference vector file based on a shared key. - -[float] -[[region-map-aggregation]] -=== Supported aggregations - -Region maps support the following aggregations: - -* <> - -* <> - -* <> - -Use the _key_ term to join the results to the vector data on the map. - -[float] -==== Options - -[float] -===== Layer Settings -- *Vector map*: select from a list of vector maps. This list includes the maps that are hosted by the © https://www.elastic.co/elastic-maps-service[Elastic Maps Service], -as well as your self-hosted layers that are configured in the *config/kibana.yml* file. To learn more about how to configure Kibana -to make self-hosted layers available, see the <> documentation. You can also explore and preview vector layers available in Elastic Maps Service at https://maps.elastic.co[https://maps.elastic.co]. -- *Join field*: this is the property from the selected vector map that will be used to join on the terms in your terms-aggregation. -When terms cannot be joined to any of the shapes in the vector layer because there is no exact match in the vector layer, Kibana will display a warning. -To turn of these warnings, go to *Management/Kibana/Advanced Settings* and set `visualization:regionmap:showWarnings` to `false`. - -[float] -===== Style Settings -- *Color Schema*: the color range used to color the shapes. - -[float] -===== Basic Settings -- *Legend Position*: the location on the screen where the legend should be rendered. -- *Show Tooltip*: indicates whether a tooltip should be displayed when hovering over a shape.. diff --git a/docs/visualize/tilemap.asciidoc b/docs/visualize/tilemap.asciidoc index 08cf666345e34..349fa681a9777 100644 --- a/docs/visualize/tilemap.asciidoc +++ b/docs/visualize/tilemap.asciidoc @@ -1,47 +1,142 @@ -[[tilemap]] -== Coordinate map +[[visualize-maps]] +== Maps -Coordinate maps display geographic areas overlaid with circles keyed to the data determined by the buckets you specify. To use coordinate maps, you plot latitude and longitude coordinates. +To tell a story and answer questions about your geographical data, you can create several types of interactive maps with Visualize. -NOTE: Coordinate maps have been replaced with <>, which offers more functionality and is easier to use. +Visualize supports the following maps: -To create coordinate maps in Visualize: +* *Coordinate* — Display latitude and longitude coordinates that are associated to the specified bucket aggregation. -* Set `xpack.maps.showMapVisualizationTypes` to `true`. +* *Region* — Display colored boundary vector shapes using a gradient. Darker colors indicate larger values, and lighter colors indicate smaller values. + +* *Heat* — Display graphical representations of data where the individual values are represented by colors. -* To display map tiles, {kib} uses the https://www.elastic.co/elastic-maps-service[Elastic Maps Service]. -To use other tile service providers, configure the <> -in `kibana.yml`. +NOTE: The maps in Visualize have been replaced with <>, which offers more functionality. [float] -[[coordinate-map-aggregation]] -=== Supported aggregations +[[coordinate-map]] +=== Coordinate map + +Use a coordinate map when your data set includes latitude and longitude values. For example, use a coordinate map to see the varying popularity of destination airports using the sample flight data. + +[role="screenshot"] +image::images/visualize_coordinate_map_example.png[] + +[float] +[[build-coordinate-map]] +==== Build a coordinate map + +Configure the `kibana.yml` settings and add the aggregations. -Coordinate maps support the following aggregations: +. Configure the following `kibana.yml` settings: + +* Set `xpack.maps.showMapVisualizationTypes` to `true`. + +* To use a tile service provider for coordinate maps other than https://www.elastic.co/elastic-maps-service[Elastic Maps Service], configure the <>. + +. To display your data on the coordinate map, use the following aggregations: * <> -* <> +* <> -When you deselect *Change precision on map zoom*, the *Precision* slider appears. The *Precision* slider determines the granularity of the results displayed on the map. For details on the area specified by each precision level, refer to {ref}/search-aggregations-bucket-geohashgrid-aggregation.html#_cell_dimensions_at_the_equator[geohash grid]. +. Specify the geohash bucket aggregation options: +* *Precision* slider — Determines the granularity of the results displayed on the map. To show the *Precision* slider, deselect *Change precision on map zoom*. For information on the area specified by each precision level, refer to {ref}/search-aggregations-bucket-geohashgrid-aggregation.html#_cell_dimensions_at_the_equator[geohash grid]. ++ NOTE: Higher precisions increase memory usage for the browser that displays {kib} and the underlying {es} cluster. -When you select *Place markers off grid (use {ref}/search-aggregations-metrics-geocentroid-aggregation.html[geocentroid])*, the markers are -placed in the center of all documents in the bucket, and a more accurate visualization is created. -NOTE: When you have multiple values in the geo_point, the coordinate map is unable to accurately calculate the geo_centroid. - -When you deselect *Place markers off grid (use {ref}/search-aggregations-metrics-geocentroid-aggregation.html[geocentroid])*, the markers are placed in the center +* *Place markers off grid (use {ref}/search-aggregations-metrics-geocentroid-aggregation.html[geocentroid])* — When you selected, the markers are +placed in the center of all documents in the bucket, and a more accurate visualization is created. When deselected, the markers are placed in the center of the geohash grid cell. ++ +NOTE: When you have multiple values in the geo_point, the coordinate map is unable to accurately calculate the geo_centroid. [float] -[[navigate-map]] -=== Navigate the coordinate map +[[navigate-coordinate-map]] +==== Navigate the coordinate map -Use the following navigation options: +To navigate the coordinate map, use the navigation options. * To move the map center, click and hold anywhere on the map and move the cursor. + * To change the zoom level, click *Zoom In* or *Zoom out* image:images/viz-zoom.png[]. + * To automatically crop the map boundaries to the geohash buckets that have at least one result, click *Fit Data Bounds* image:images/viz-fit-bounds.png[]. + +[float] +[[region-map]] +=== Region map + +Use region maps when you want to show statistical data on a geographic area, such as a county, country, province, or state. For example, use a region map if you want to see the average sales for each country with the sample eCommerce order data. + +[role="screenshot"] +image::images/visualize_region_map_example.png[] + +[float] +[[build-region-maps]] +==== Build a region map + +Configure the `kibana.yml` settings and add the aggregations. + +. In `kibana.yml`, set `xpack.maps.showMapVisualizationTypes` to `true`. + +. To display your data on the region map, use the following aggregations: + +* <> +* <> +* <> + +[float] +[[navigate-region-map]] +==== Navigate the region map + +To navigate the region map, use the navigation options. + +* To change the zoom level, click *Zoom In* or *Zoom out* image:images/viz-zoom.png[]. + +* To automatically crop the map boundaries, click *Fit Data Bounds* image:images/viz-fit-bounds.png[]. + +[float] +[[heat-map]] +=== Heat map + +Use heat maps when your data set includes categorical data. For example, use a heat map to see the flights of origin countries compared to destination countries using the sample flight data. + +[role="screenshot"] +image::images/visualize_heat_map_example.png[] + +[float] +[[build-heat-map]] +==== Build a heat map + +To display your data on the heat map, use the supported aggregations. + +Heat maps support the following aggregations: + +* <> +* <> +* <> +* <> + +[float] +[[navigate-heatmap]] +==== Change the color ranges + +When only one color displays on the heat map, you might need to change the color ranges. + +To specify the number of color ranges: + +. Click *Options*. + +. Enter the *Number of colors* to display. + +To specify custom ranges: + +. Click *Options*. + +. Select *Use custom ranges*. + +. Enter the ranges to display. diff --git a/src/cli/serve/integration_tests/reload_logging_config.test.ts b/src/cli/serve/integration_tests/reload_logging_config.test.ts index 2def3569828d3..9ad8438c312a1 100644 --- a/src/cli/serve/integration_tests/reload_logging_config.test.ts +++ b/src/cli/serve/integration_tests/reload_logging_config.test.ts @@ -84,180 +84,174 @@ function createConfigManager(configPath: string) { } describe('Server logging configuration', function() { - let child: Child.ChildProcess; + let child: undefined | Child.ChildProcess; + beforeEach(() => { Fs.mkdirSync(tempDir, { recursive: true }); }); afterEach(async () => { if (child !== undefined) { - child.kill(); - // wait for child to be killed otherwise jest complains that process not finished - await new Promise(res => setTimeout(res, 1000)); + const exitPromise = new Promise(resolve => child?.once('exit', resolve)); + child.kill('SIGKILL'); + await exitPromise; } + Del.sync(tempDir, { force: true }); }); - const isWindows = /^win/.test(process.platform); - if (isWindows) { + if (process.platform.startsWith('win')) { it('SIGHUP is not a feature of Windows.', () => { // nothing to do for Windows }); - } else { - describe('legacy logging', () => { - it( - 'should be reloadable via SIGHUP process signaling', - async function() { - const configFilePath = Path.resolve(tempDir, 'kibana.yml'); - Fs.copyFileSync(legacyConfig, configFilePath); - - child = Child.spawn(process.execPath, [ - kibanaPath, - '--oss', - '--config', - configFilePath, - '--verbose', - ]); - - const message$ = Rx.fromEvent(child.stdout, 'data').pipe( - map(messages => - String(messages) - .split('\n') - .filter(Boolean) - ) - ); - - await message$ - .pipe( - // We know the sighup handler will be registered before this message logged - filter(messages => messages.some(m => m.includes('setting up root'))), - take(1) - ) - .toPromise(); - - const lastMessage = await message$.pipe(take(1)).toPromise(); - expect(containsJsonOnly(lastMessage)).toBe(true); - - createConfigManager(configFilePath).modify(oldConfig => { - oldConfig.logging.json = false; - return oldConfig; - }); - - child.kill('SIGHUP'); - - await message$ - .pipe( - filter(messages => !containsJsonOnly(messages)), - take(1) - ) - .toPromise(); - }, - minute - ); - - it( - 'should recreate file handle on SIGHUP', - async function() { - const logPath = Path.resolve(tempDir, 'kibana.log'); - const logPathArchived = Path.resolve(tempDir, 'kibana_archive.log'); - - child = Child.spawn(process.execPath, [ - kibanaPath, - '--oss', - '--config', - legacyConfig, - '--logging.dest', - logPath, - '--verbose', - ]); - - await watchFileUntil(logPath, /setting up root/, 30 * second); - // once the server is running, archive the log file and issue SIGHUP - Fs.renameSync(logPath, logPathArchived); - child.kill('SIGHUP'); - - await watchFileUntil( - logPath, - /Reloaded logging configuration due to SIGHUP/, - 30 * second - ); - }, - minute - ); - }); - - describe('platform logging', () => { - it( - 'should be reloadable via SIGHUP process signaling', - async function() { - const configFilePath = Path.resolve(tempDir, 'kibana.yml'); - Fs.copyFileSync(configFileLogConsole, configFilePath); - - child = Child.spawn(process.execPath, [kibanaPath, '--oss', '--config', configFilePath]); - - const message$ = Rx.fromEvent(child.stdout, 'data').pipe( - map(messages => - String(messages) - .split('\n') - .filter(Boolean) - ) - ); - - await message$ - .pipe( - // We know the sighup handler will be registered before this message logged - filter(messages => messages.some(m => m.includes('setting up root'))), - take(1) - ) - .toPromise(); - - const lastMessage = await message$.pipe(take(1)).toPromise(); - expect(containsJsonOnly(lastMessage)).toBe(true); - - createConfigManager(configFilePath).modify(oldConfig => { - oldConfig.logging.appenders.console.layout.kind = 'pattern'; - return oldConfig; - }); - child.kill('SIGHUP'); - - await message$ - .pipe( - filter(messages => !containsJsonOnly(messages)), - take(1) - ) - .toPromise(); - }, - 30 * second - ); - it( - 'should recreate file handle on SIGHUP', - async function() { - const configFilePath = Path.resolve(tempDir, 'kibana.yml'); - Fs.copyFileSync(configFileLogFile, configFilePath); - - const logPath = Path.resolve(tempDir, 'kibana.log'); - const logPathArchived = Path.resolve(tempDir, 'kibana_archive.log'); - - createConfigManager(configFilePath).modify(oldConfig => { - oldConfig.logging.appenders.file.path = logPath; - return oldConfig; - }); - - child = Child.spawn(process.execPath, [kibanaPath, '--oss', '--config', configFilePath]); - - await watchFileUntil(logPath, /setting up root/, 30 * second); - // once the server is running, archive the log file and issue SIGHUP - Fs.renameSync(logPath, logPathArchived); - child.kill('SIGHUP'); - - await watchFileUntil( - logPath, - /Reloaded logging configuration due to SIGHUP/, - 30 * second - ); - }, - minute - ); - }); + return; } + + describe('legacy logging', () => { + it( + 'should be reloadable via SIGHUP process signaling', + async function() { + const configFilePath = Path.resolve(tempDir, 'kibana.yml'); + Fs.copyFileSync(legacyConfig, configFilePath); + + child = Child.spawn(process.execPath, [ + kibanaPath, + '--oss', + '--config', + configFilePath, + '--verbose', + ]); + + const message$ = Rx.fromEvent(child.stdout, 'data').pipe( + map(messages => + String(messages) + .split('\n') + .filter(Boolean) + ) + ); + + await message$ + .pipe( + // We know the sighup handler will be registered before this message logged + filter(messages => messages.some(m => m.includes('setting up root'))), + take(1) + ) + .toPromise(); + + const lastMessage = await message$.pipe(take(1)).toPromise(); + expect(containsJsonOnly(lastMessage)).toBe(true); + + createConfigManager(configFilePath).modify(oldConfig => { + oldConfig.logging.json = false; + return oldConfig; + }); + + child.kill('SIGHUP'); + + await message$ + .pipe( + filter(messages => !containsJsonOnly(messages)), + take(1) + ) + .toPromise(); + }, + minute + ); + + it( + 'should recreate file handle on SIGHUP', + async function() { + const logPath = Path.resolve(tempDir, 'kibana.log'); + const logPathArchived = Path.resolve(tempDir, 'kibana_archive.log'); + + child = Child.spawn(process.execPath, [ + kibanaPath, + '--oss', + '--config', + legacyConfig, + '--logging.dest', + logPath, + '--verbose', + ]); + + await watchFileUntil(logPath, /setting up root/, 30 * second); + // once the server is running, archive the log file and issue SIGHUP + Fs.renameSync(logPath, logPathArchived); + child.kill('SIGHUP'); + + await watchFileUntil(logPath, /Reloaded logging configuration due to SIGHUP/, 30 * second); + }, + minute + ); + }); + + describe('platform logging', () => { + it( + 'should be reloadable via SIGHUP process signaling', + async function() { + const configFilePath = Path.resolve(tempDir, 'kibana.yml'); + Fs.copyFileSync(configFileLogConsole, configFilePath); + + child = Child.spawn(process.execPath, [kibanaPath, '--oss', '--config', configFilePath]); + + const message$ = Rx.fromEvent(child.stdout, 'data').pipe( + map(messages => + String(messages) + .split('\n') + .filter(Boolean) + ) + ); + + await message$ + .pipe( + // We know the sighup handler will be registered before this message logged + filter(messages => messages.some(m => m.includes('setting up root'))), + take(1) + ) + .toPromise(); + + const lastMessage = await message$.pipe(take(1)).toPromise(); + expect(containsJsonOnly(lastMessage)).toBe(true); + + createConfigManager(configFilePath).modify(oldConfig => { + oldConfig.logging.appenders.console.layout.kind = 'pattern'; + return oldConfig; + }); + child.kill('SIGHUP'); + + await message$ + .pipe( + filter(messages => !containsJsonOnly(messages)), + take(1) + ) + .toPromise(); + }, + 30 * second + ); + it( + 'should recreate file handle on SIGHUP', + async function() { + const configFilePath = Path.resolve(tempDir, 'kibana.yml'); + Fs.copyFileSync(configFileLogFile, configFilePath); + + const logPath = Path.resolve(tempDir, 'kibana.log'); + const logPathArchived = Path.resolve(tempDir, 'kibana_archive.log'); + + createConfigManager(configFilePath).modify(oldConfig => { + oldConfig.logging.appenders.file.path = logPath; + return oldConfig; + }); + + child = Child.spawn(process.execPath, [kibanaPath, '--oss', '--config', configFilePath]); + + await watchFileUntil(logPath, /setting up root/, 30 * second); + // once the server is running, archive the log file and issue SIGHUP + Fs.renameSync(logPath, logPathArchived); + child.kill('SIGHUP'); + + await watchFileUntil(logPath, /Reloaded logging configuration due to SIGHUP/, 30 * second); + }, + minute + ); + }); }); diff --git a/src/core/MIGRATION_EXAMPLES.md b/src/core/MIGRATION_EXAMPLES.md index def83ba177fc9..2953edb535f47 100644 --- a/src/core/MIGRATION_EXAMPLES.md +++ b/src/core/MIGRATION_EXAMPLES.md @@ -917,4 +917,10 @@ Would be converted to: ```typescript const migration: SavedObjectMigrationFn = (doc, { log }) => {...} -``` \ No newline at end of file +``` + +### Remarks + +The `registerType` API will throw if called after the service has started, and therefor cannot be used from +legacy plugin code. Legacy plugins should use the legacy savedObjects service and the legacy way to register +saved object types until migrated. \ No newline at end of file diff --git a/src/core/server/http/http_server.mocks.ts b/src/core/server/http/http_server.mocks.ts index c586cf6a9825f..0a9541393284e 100644 --- a/src/core/server/http/http_server.mocks.ts +++ b/src/core/server/http/http_server.mocks.ts @@ -43,6 +43,7 @@ interface RequestFixtureOptions

{ method?: RouteMethod; socket?: Socket; routeTags?: string[]; + routeAuthRequired?: false; validation?: { params?: RouteValidationSpec

; query?: RouteValidationSpec; @@ -59,6 +60,7 @@ function createKibanaRequestMock

({ method = 'get', socket = new Socket(), routeTags, + routeAuthRequired, validation = {}, }: RequestFixtureOptions = {}) { const queryString = stringify(query, { sort: false }); @@ -77,7 +79,9 @@ function createKibanaRequestMock

({ query: queryString, search: queryString ? `?${queryString}` : queryString, }, - route: { settings: { tags: routeTags } }, + route: { + settings: { tags: routeTags, auth: routeAuthRequired }, + }, raw: { req: { socket }, }, diff --git a/src/core/server/saved_objects/saved_objects_service.test.ts b/src/core/server/saved_objects/saved_objects_service.test.ts index a1e2c1e8dbf26..554acf8d43dcb 100644 --- a/src/core/server/saved_objects/saved_objects_service.test.ts +++ b/src/core/server/saved_objects/saved_objects_service.test.ts @@ -232,6 +232,36 @@ describe('SavedObjectsService', () => { expect(migratorInstanceMock.runMigrations).toHaveBeenCalledTimes(1); }); + it('throws when calling setup APIs once started', async () => { + const coreContext = createCoreContext({ skipMigration: false }); + const soService = new SavedObjectsService(coreContext); + const setup = await soService.setup(createSetupDeps()); + await soService.start({}); + + expect(() => { + setup.setClientFactoryProvider(jest.fn()); + }).toThrowErrorMatchingInlineSnapshot( + `"cannot call \`setClientFactoryProvider\` after service startup."` + ); + + expect(() => { + setup.addClientWrapper(0, 'dummy', jest.fn()); + }).toThrowErrorMatchingInlineSnapshot( + `"cannot call \`addClientWrapper\` after service startup."` + ); + + expect(() => { + setup.registerType({ + name: 'someType', + hidden: false, + namespaceAgnostic: false, + mappings: { properties: {} }, + }); + }).toThrowErrorMatchingInlineSnapshot( + `"cannot call \`registerType\` after service startup."` + ); + }); + describe('#getTypeRegistry', () => { it('returns the internal type registry of the service', async () => { const coreContext = createCoreContext({ skipMigration: false }); diff --git a/src/core/server/saved_objects/saved_objects_service.ts b/src/core/server/saved_objects/saved_objects_service.ts index da8f7ab96d689..d5a9f47420971 100644 --- a/src/core/server/saved_objects/saved_objects_service.ts +++ b/src/core/server/saved_objects/saved_objects_service.ts @@ -61,6 +61,9 @@ import { registerRoutes } from './routes'; * the factory provided to `setClientFactory` and wrapped by all wrappers * registered through `addClientWrapper`. * + * All the setup APIs will throw if called after the service has started, and therefor cannot be used + * from legacy plugin code. Legacy plugins should use the legacy savedObject service until migrated. + * * @example * ```ts * import { SavedObjectsClient, CoreSetup } from 'src/core/server'; @@ -275,6 +278,7 @@ export class SavedObjectsService private migrator$ = new Subject(); private typeRegistry = new SavedObjectTypeRegistry(); private validations: PropertyValidators = {}; + private started = false; constructor(private readonly coreContext: CoreContext) { this.logger = coreContext.logger.get('savedobjects-service'); @@ -316,12 +320,18 @@ export class SavedObjectsService return { setClientFactoryProvider: provider => { + if (this.started) { + throw new Error('cannot call `setClientFactoryProvider` after service startup.'); + } if (this.clientFactoryProvider) { throw new Error('custom client factory is already set, and can only be set once'); } this.clientFactoryProvider = provider; }, addClientWrapper: (priority, id, factory) => { + if (this.started) { + throw new Error('cannot call `addClientWrapper` after service startup.'); + } this.clientFactoryWrappers.push({ priority, id, @@ -329,6 +339,9 @@ export class SavedObjectsService }); }, registerType: type => { + if (this.started) { + throw new Error('cannot call `registerType` after service startup.'); + } this.typeRegistry.registerType(type); }, }; @@ -415,6 +428,8 @@ export class SavedObjectsService clientProvider.addClientWrapperFactory(priority, id, factory); }); + this.started = true; + return { migrator, clientProvider, diff --git a/src/dev/npm/integration_tests/installed_packages.test.ts b/src/dev/npm/integration_tests/installed_packages.test.ts index 5c942005d2eee..75cd0e5607698 100644 --- a/src/dev/npm/integration_tests/installed_packages.test.ts +++ b/src/dev/npm/integration_tests/installed_packages.test.ts @@ -41,7 +41,7 @@ describe('src/dev/npm/installed_packages', () => { includeDev: true, }), ]); - }, 60 * 1000); + }, 360 * 1000); it('reads all installed packages of a module', () => { expect(fixture1Packages).toEqual([ diff --git a/src/legacy/core_plugins/data/public/search/expressions/esaggs.ts b/src/legacy/core_plugins/data/public/search/expressions/esaggs.ts index 302527e4ed549..7a5d927d0f219 100644 --- a/src/legacy/core_plugins/data/public/search/expressions/esaggs.ts +++ b/src/legacy/core_plugins/data/public/search/expressions/esaggs.ts @@ -40,7 +40,7 @@ import { import { buildTabularInspectorData } from './build_tabular_inspector_data'; import { calculateObjectHash } from '../../../../visualizations/public'; import { tabifyAggResponse } from '../../../../../core_plugins/data/public'; -import { PersistedState } from '../../../../../ui/public/persisted_state'; +import { PersistedState } from '../../../../../../plugins/visualizations/public'; import { Adapters } from '../../../../../../plugins/inspector/public'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { getQueryService, getIndexPatterns } from '../../../../../../plugins/data/public/services'; diff --git a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/application.ts b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/application.ts index 257ba8a4711b0..9ca84735cac16 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/application.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/application.ts @@ -59,6 +59,10 @@ export interface RenderDeps { savedDashboards: SavedObjectLoader; dashboardConfig: KibanaLegacyStart['dashboardConfig']; dashboardCapabilities: any; + embeddableCapabilities: { + visualizeCapabilities: any; + mapsCapabilities: any; + }; uiSettings: IUiSettingsClient; chrome: ChromeStart; addBasePath: (path: string) => string; diff --git a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_app_controller.tsx b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_app_controller.tsx index 84dd73882d134..af3347afa9c5f 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_app_controller.tsx +++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_app_controller.tsx @@ -108,6 +108,7 @@ export class DashboardAppController { embeddable, share, dashboardCapabilities, + embeddableCapabilities: { visualizeCapabilities, mapsCapabilities }, data: { query: queryService }, core: { notifications, @@ -134,7 +135,6 @@ export class DashboardAppController { } = syncQueryStateWithUrl(queryService, kbnUrlStateStorage); let lastReloadRequestTime = 0; - const dash = ($scope.dash = $route.current.locals.dash); if (dash.id) { chrome.docTitle.change(dash.title); @@ -180,11 +180,18 @@ export class DashboardAppController { dashboardStateManager.getIsViewMode() && !dashboardConfig.getHideWriteControls(); - const getIsEmptyInReadonlyMode = () => - !dashboardStateManager.getPanels().length && - !getShouldShowEditHelp() && - !getShouldShowViewHelp() && - dashboardConfig.getHideWriteControls(); + const shouldShowUnauthorizedEmptyState = () => { + const readonlyMode = + !dashboardStateManager.getPanels().length && + !getShouldShowEditHelp() && + !getShouldShowViewHelp() && + dashboardConfig.getHideWriteControls(); + const userHasNoPermissions = + !dashboardStateManager.getPanels().length && + !visualizeCapabilities.save && + !mapsCapabilities.save; + return readonlyMode || userHasNoPermissions; + }; const addVisualization = () => { navActions[TopNavIds.VISUALIZE](); @@ -250,7 +257,7 @@ export class DashboardAppController { } const shouldShowEditHelp = getShouldShowEditHelp(); const shouldShowViewHelp = getShouldShowViewHelp(); - const isEmptyInReadonlyMode = getIsEmptyInReadonlyMode(); + const isEmptyInReadonlyMode = shouldShowUnauthorizedEmptyState(); return { id: dashboardStateManager.savedDashboard.id || '', filters: queryFilter.getFilters(), @@ -307,7 +314,7 @@ export class DashboardAppController { dashboardContainer.renderEmpty = () => { const shouldShowEditHelp = getShouldShowEditHelp(); const shouldShowViewHelp = getShouldShowViewHelp(); - const isEmptyInReadOnlyMode = getIsEmptyInReadonlyMode(); + const isEmptyInReadOnlyMode = shouldShowUnauthorizedEmptyState(); const isEmptyState = shouldShowEditHelp || shouldShowViewHelp || isEmptyInReadOnlyMode; return isEmptyState ? ( diff --git a/src/legacy/core_plugins/kibana/public/home/np_ready/components/synopsis.js b/src/legacy/core_plugins/kibana/public/home/np_ready/components/synopsis.js index 968b8eb64def5..f43c377b4e5b9 100644 --- a/src/legacy/core_plugins/kibana/public/home/np_ready/components/synopsis.js +++ b/src/legacy/core_plugins/kibana/public/home/np_ready/components/synopsis.js @@ -37,13 +37,7 @@ export function Synopsis({ if (iconUrl) { optionalImg = ; } else if (iconType) { - optionalImg = ( - - ); + optionalImg = ; } const classes = classNames('homSynopsis__card', { diff --git a/src/legacy/core_plugins/kibana/public/visualize/legacy_imports.ts b/src/legacy/core_plugins/kibana/public/visualize/legacy_imports.ts index d52d31c2dd79e..b8ee7cd378750 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/legacy_imports.ts +++ b/src/legacy/core_plugins/kibana/public/visualize/legacy_imports.ts @@ -29,7 +29,6 @@ export { State } from 'ui/state_management/state'; export { GlobalStateProvider } from 'ui/state_management/global_state'; // @ts-ignore export { StateManagementConfigProvider } from 'ui/state_management/config_provider'; -export { PersistedState } from 'ui/persisted_state'; export { subscribeWithScope } from 'ui/utils/subscribe_with_scope'; // @ts-ignore diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/make_stateful.ts b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/make_stateful.ts index 137d4de1fe9a8..8384585108a59 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/make_stateful.ts +++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/make_stateful.ts @@ -17,7 +17,7 @@ * under the License. */ -import { PersistedState } from '../../../legacy_imports'; +import { PersistedState } from '../../../../../../../../plugins/visualizations/public'; import { ReduxLikeStateContainer } from '../../../../../../../../plugins/kibana_utils/public'; import { VisualizeAppState, VisualizeAppStateTransitions } from '../../types'; diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/types.d.ts b/src/legacy/core_plugins/kibana/public/visualize/np_ready/types.d.ts index 3b6ecb45b83b3..d95939170419b 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/types.d.ts +++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/types.d.ts @@ -19,9 +19,10 @@ import { TimeRange, Query, Filter, DataPublicPluginStart } from 'src/plugins/data/public'; import { IEmbeddableStart } from 'src/plugins/embeddable/public'; +import { PersistedState } from 'src/plugins/visualizations/public'; import { LegacyCoreStart } from 'kibana/public'; import { Vis } from 'src/legacy/core_plugins/visualizations/public'; -import { VisSavedObject, PersistedState } from '../legacy_imports'; +import { VisSavedObject } from '../legacy_imports'; export type PureVisState = ReturnType; diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/sidebar/sidebar.tsx b/src/legacy/core_plugins/vis_default_editor/public/components/sidebar/sidebar.tsx index 8615bcdd1bfbd..d3b843eaaec9f 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/sidebar/sidebar.tsx +++ b/src/legacy/core_plugins/vis_default_editor/public/components/sidebar/sidebar.tsx @@ -21,13 +21,13 @@ import React, { useMemo, useState, useCallback, KeyboardEventHandler, useEffect import { get, isEqual } from 'lodash'; import { i18n } from '@kbn/i18n'; import { keyCodes, EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiTitle } from '@elastic/eui'; - import { Vis } from 'src/legacy/core_plugins/visualizations/public'; -import { PersistedState, AggGroupNames } from '../../legacy_imports'; +import { AggGroupNames } from '../../legacy_imports'; import { DefaultEditorNavBar, OptionTab } from './navbar'; import { DefaultEditorControls } from './controls'; import { setStateParamValue, useEditorReducer, useEditorFormState, discardChanges } from './state'; import { DefaultEditorAggCommonProps } from '../agg_common_props'; +import { PersistedState } from '../../../../../../plugins/visualizations/public'; interface DefaultEditorSideBarProps { isCollapsed: boolean; diff --git a/src/legacy/core_plugins/vis_default_editor/public/legacy_imports.ts b/src/legacy/core_plugins/vis_default_editor/public/legacy_imports.ts index 5e547eed1c957..832f73752a99b 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/legacy_imports.ts +++ b/src/legacy/core_plugins/vis_default_editor/public/legacy_imports.ts @@ -48,5 +48,4 @@ export { isValidJson, isValidInterval } from 'ui/agg_types'; export { AggParamOption } from 'ui/agg_types'; export { CidrMask } from 'ui/agg_types'; -export { PersistedState } from 'ui/persisted_state'; export * from 'ui/vis/lib'; diff --git a/src/legacy/core_plugins/vis_default_editor/public/vis_options_props.tsx b/src/legacy/core_plugins/vis_default_editor/public/vis_options_props.tsx index babcb59c6582e..18fbba1b039b5 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/vis_options_props.tsx +++ b/src/legacy/core_plugins/vis_default_editor/public/vis_options_props.tsx @@ -17,7 +17,8 @@ * under the License. */ -import { IAggConfigs, PersistedState } from './legacy_imports'; +import { PersistedState } from '../../../../plugins/visualizations/public'; +import { IAggConfigs } from './legacy_imports'; import { Vis } from '../../visualizations/public'; export interface VisOptionsProps { diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/legacy_imports.ts b/src/legacy/core_plugins/vis_type_timeseries/public/legacy_imports.ts index 401acfc8df766..a2952b2c83afd 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/public/legacy_imports.ts +++ b/src/legacy/core_plugins/vis_type_timeseries/public/legacy_imports.ts @@ -17,7 +17,6 @@ * under the License. */ -export { PersistedState } from 'ui/persisted_state'; // @ts-ignore export { defaultFeedbackMessage } from 'ui/vis/default_feedback_message'; // @ts-ignore diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/metrics_fn.ts b/src/legacy/core_plugins/vis_type_timeseries/public/metrics_fn.ts index 576723bad1e43..1f9cbecc2a354 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/public/metrics_fn.ts +++ b/src/legacy/core_plugins/vis_type_timeseries/public/metrics_fn.ts @@ -24,10 +24,10 @@ import { KibanaContext, Render, } from '../../../../plugins/expressions/public'; +import { PersistedState } from '../../../../plugins/visualizations/public'; // @ts-ignore import { metricsRequestHandler } from './request_handler'; -import { PersistedState } from './legacy_imports'; type Input = KibanaContext | null; type Output = Promise>; diff --git a/src/legacy/core_plugins/visualizations/public/legacy_imports.ts b/src/legacy/core_plugins/visualizations/public/legacy_imports.ts index 385117be8ae2e..fb7a157b53a9a 100644 --- a/src/legacy/core_plugins/visualizations/public/legacy_imports.ts +++ b/src/legacy/core_plugins/visualizations/public/legacy_imports.ts @@ -17,7 +17,6 @@ * under the License. */ -export { PersistedState } from '../../../ui/public/persisted_state'; export { AggConfigs, IAggConfig, diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/components/visualization.tsx b/src/legacy/core_plugins/visualizations/public/np_ready/public/components/visualization.tsx index 5a9a1830ebdf3..33830c45848e4 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/components/visualization.tsx +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/components/visualization.tsx @@ -19,8 +19,7 @@ import { get } from 'lodash'; import React from 'react'; - -import { PersistedState } from '../../../legacy_imports'; +import { PersistedState } from '../../../../../../../plugins/visualizations/public'; import { memoizeLast } from '../legacy/memoize'; import { VisualizationChart } from './visualization_chart'; import { VisualizationNoResults } from './visualization_noresults'; diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/components/visualization_chart.tsx b/src/legacy/core_plugins/visualizations/public/np_ready/public/components/visualization_chart.tsx index 95fd31049d233..7b1a18e8066a7 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/components/visualization_chart.tsx +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/components/visualization_chart.tsx @@ -20,8 +20,7 @@ import React from 'react'; import * as Rx from 'rxjs'; import { debounceTime, filter, share, switchMap } from 'rxjs/operators'; - -import { PersistedState } from '../../../legacy_imports'; +import { PersistedState } from '../../../../../../../plugins/visualizations/public'; import { Vis, VisualizationController } from '../vis'; import { getUpdateStatus } from '../legacy/update_status'; import { ResizeChecker } from '../../../../../../../plugins/kibana_utils/public'; diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/embeddable/visualize_embeddable.ts b/src/legacy/core_plugins/visualizations/public/np_ready/public/embeddable/visualize_embeddable.ts index 2537caa01cd46..97e2b8f88172e 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/embeddable/visualize_embeddable.ts +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/embeddable/visualize_embeddable.ts @@ -43,10 +43,10 @@ import { IExpressionLoaderParams, ExpressionsStart, } from '../../../../../../../plugins/expressions/public'; +import { PersistedState } from '../../../../../../../plugins/visualizations/public'; import { buildPipeline } from '../legacy/build_pipeline'; import { Vis } from '../vis'; import { getExpressions, getUiActions } from '../services'; -import { PersistedState } from '../../../legacy_imports'; import { VisSavedObject } from '../types'; const getKeys = (o: T): Array => Object.keys(o) as Array; diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/expressions/vis.js b/src/legacy/core_plugins/visualizations/public/np_ready/public/expressions/vis.js index 81224b65f7786..a891140677d60 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/expressions/vis.js +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/expressions/vis.js @@ -29,7 +29,7 @@ import { EventEmitter } from 'events'; import _ from 'lodash'; -import { PersistedState } from '../../../legacy_imports'; +import { PersistedState } from '../../../../../../../plugins/visualizations/public'; import { getTypes } from '../services'; diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/expressions/visualization_function.ts b/src/legacy/core_plugins/visualizations/public/np_ready/public/expressions/visualization_function.ts index 4ac0931c5d865..d98eda4c50ef9 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/expressions/visualization_function.ts +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/expressions/visualization_function.ts @@ -19,12 +19,14 @@ import { get } from 'lodash'; import { i18n } from '@kbn/i18n'; -import { VisResponseValue } from '../../../../../../../plugins/visualizations/public'; +import { + VisResponseValue, + PersistedState, +} from '../../../../../../../plugins/visualizations/public'; import { ExpressionFunctionDefinition, Render, } from '../../../../../../../plugins/expressions/public'; -import { PersistedState } from '../../../legacy_imports'; import { getTypes, getIndexPatterns, getFilterManager } from '../services'; interface Arguments { diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/legacy/update_status.ts b/src/legacy/core_plugins/visualizations/public/np_ready/public/legacy/update_status.ts index 6d32a6df5f1ec..d9af5122eadec 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/legacy/update_status.ts +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/legacy/update_status.ts @@ -17,7 +17,7 @@ * under the License. */ -import { PersistedState } from '../../../legacy_imports'; +import { PersistedState } from '../../../../../../../plugins/visualizations/public'; import { calculateObjectHash } from './calculate_object_hash'; import { Vis } from '../vis'; diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/vis_impl.js b/src/legacy/core_plugins/visualizations/public/np_ready/public/vis_impl.js index 6f4ab6d708184..2f36322c67256 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/vis_impl.js +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/vis_impl.js @@ -29,7 +29,8 @@ import { EventEmitter } from 'events'; import _ from 'lodash'; -import { AggConfigs, PersistedState } from '../../legacy_imports'; +import { PersistedState } from '../../../../../../../src/plugins/visualizations/public'; +import { AggConfigs } from '../../legacy_imports'; import { updateVisualizationConfig } from './legacy/vis_update'; import { getTypes } from './services'; diff --git a/src/legacy/ui/public/events.js b/src/legacy/ui/public/events.js index f0a21a1abd012..00c92038e7c9f 100644 --- a/src/legacy/ui/public/events.js +++ b/src/legacy/ui/public/events.js @@ -20,19 +20,19 @@ /** * @name Events * - * @extends SimpleEmitter + * @extends EventEmitter */ import _ from 'lodash'; +import { EventEmitter } from 'events'; import { fatalError } from './notify'; -import { SimpleEmitter } from './utils/simple_emitter'; import { createLegacyClass } from './utils/legacy_class'; import { createDefer } from 'ui/promises'; const location = 'EventEmitter'; export function EventsProvider(Promise) { - createLegacyClass(Events).inherits(SimpleEmitter); + createLegacyClass(Events).inherits(EventEmitter); function Events() { Events.Super.call(this); this._listeners = {}; @@ -79,6 +79,7 @@ export function EventsProvider(Promise) { */ Events.prototype.off = function(name, handler) { if (!name && !handler) { + this._listeners = {}; return this.removeAllListeners(); } diff --git a/src/legacy/ui/public/persisted_state/index.js b/src/legacy/ui/public/persisted_state/index.js deleted file mode 100644 index ab5a3e7be7d28..0000000000000 --- a/src/legacy/ui/public/persisted_state/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 { PersistedState } from './persisted_state'; diff --git a/src/legacy/ui/public/persisted_state/persisted_state.d.ts b/src/legacy/ui/public/persisted_state/persisted_state.d.ts deleted file mode 100644 index b5d7513172e76..0000000000000 --- a/src/legacy/ui/public/persisted_state/persisted_state.d.ts +++ /dev/null @@ -1,29 +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. - */ - -// It's currenty hard to properly type PersistedState, since it dynamically -// inherits the class passed into the constructor. These typings are really pretty bad -// but needed in the short term to make incremental progress elsewhere. Can't even -// just use `any` since then typescript complains about using PersistedState as a -// constructor. -export class PersistedState { - constructor(value?: any, path?: any, EmitterClass?: any); - // method you want typed so far - [prop: string]: any; -} diff --git a/src/legacy/ui/public/persisted_state/persisted_state.js b/src/legacy/ui/public/persisted_state/persisted_state.js deleted file mode 100644 index 071a39ab2b4f8..0000000000000 --- a/src/legacy/ui/public/persisted_state/persisted_state.js +++ /dev/null @@ -1,253 +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. - */ - -/** - * @name PersistedState - * - * @extends Events - */ - -import _ from 'lodash'; -import toPath from 'lodash/internal/toPath'; -import { PersistedStateError } from './errors'; -import { SimpleEmitter } from '../utils/simple_emitter'; - -function prepSetParams(key, value, path) { - // key must be the value, set the entire state using it - if (_.isUndefined(value) && (_.isPlainObject(key) || path.length > 0)) { - // setting entire tree, swap the key and value to write to the state - value = key; - key = undefined; - } - - // ensure the value being passed in is never mutated - return { - value: _.cloneDeep(value), - key: key, - }; -} - -export class PersistedState { - /** - * - * @param value - * @param path - * @param EmitterClass {SimpleEmitter} - a SimpleEmitter class that this class will extend. Can be used to - * inherit a custom event emitter. For example, the EventEmitter is an "angular-ized" version - * for angular components which automatically triggers a digest loop for every registered - * handler. TODO: replace angularized SimpleEmitter and force angular callers to handle digest loops manually ala - * https://github.com/elastic/kibana/issues/13855 - */ - constructor(value, path, EmitterClass = SimpleEmitter) { - EmitterClass.call(this); - - this._path = this._setPath(path); - - _.forOwn(EmitterClass.prototype, (method, methodName) => { - this[methodName] = function() { - return EmitterClass.prototype[methodName].apply(this, arguments); - }; - }); - - // Some validations - if (!this._path.length && value && !_.isPlainObject(value)) { - throw new PersistedStateError('State value must be a plain object'); - } - - value = value || this._getDefault(); - - // copy passed state values and create internal trackers - this.set(value); - this._initialized = true; // used to track state changes - } - - get(key, def) { - return _.cloneDeep(this._get(key, def)); - } - - set(key, value) { - const params = prepSetParams(key, value, this._path); - const val = this._set(params.key, params.value); - this.emit('set'); - return val; - } - - setSilent(key, value) { - const params = prepSetParams(key, value, this._path); - return this._set(params.key, params.value, true); - } - - clearAllKeys() { - Object.getOwnPropertyNames(this._changedState).forEach(key => { - this.set(key, null); - }); - } - - reset(path) { - const keyPath = this._getIndex(path); - const origValue = _.get(this._defaultState, keyPath); - const currentValue = _.get(this._mergedState, keyPath); - - if (_.isUndefined(origValue)) { - this._cleanPath(path, this._mergedState); - } else { - _.set(this._mergedState, keyPath, origValue); - } - - // clean up the changedState tree - this._cleanPath(path, this._changedState); - - if (!_.isEqual(currentValue, origValue)) this.emit('change'); - } - - getChanges() { - return _.cloneDeep(this._changedState); - } - - toJSON() { - return this.get(); - } - - toString() { - return JSON.stringify(this.toJSON()); - } - - fromString(input) { - return this.set(JSON.parse(input)); - } - - _getIndex(key) { - if (_.isUndefined(key)) return this._path; - return (this._path || []).concat(toPath(key)); - } - - _getPartialIndex(key) { - const keyPath = this._getIndex(key); - return keyPath.slice(this._path.length); - } - - _cleanPath(path, stateTree) { - const partialPath = this._getPartialIndex(path); - let remove = true; - - // recursively delete value tree, when no other keys exist - while (partialPath.length > 0) { - const lastKey = partialPath.splice(partialPath.length - 1, 1)[0]; - const statePath = this._path.concat(partialPath); - const stateVal = statePath.length > 0 ? _.get(stateTree, statePath) : stateTree; - - // if stateVal isn't an object, do nothing - if (!_.isPlainObject(stateVal)) return; - - if (remove) delete stateVal[lastKey]; - if (Object.keys(stateVal).length > 0) remove = false; - } - } - - _getDefault() { - return this._hasPath() ? undefined : {}; - } - - _setPath(path) { - const isString = _.isString(path); - const isArray = Array.isArray(path); - - if (!isString && !isArray) return []; - return isString ? [this._getIndex(path)] : path; - } - - _hasPath() { - return this._path.length > 0; - } - - _get(key, def) { - // no path and no key, get the whole state - if (!this._hasPath() && _.isUndefined(key)) { - return this._mergedState; - } - - return _.get(this._mergedState, this._getIndex(key), def); - } - - _set(key, value, silent) { - const self = this; - let stateChanged = false; - const initialState = !this._initialized; - const keyPath = this._getIndex(key); - const hasKeyPath = keyPath.length > 0; - - // if this is the initial state value, save value as the default - if (initialState) { - this._changedState = {}; - if (!this._hasPath() && _.isUndefined(key)) this._defaultState = value; - else this._defaultState = _.set({}, keyPath, value); - } - - if (!initialState) { - // no path and no key, set the whole state - if (!this._hasPath() && _.isUndefined(key)) { - // compare changedState and new state, emit an event when different - stateChanged = !_.isEqual(this._changedState, value); - this._changedState = value; - this._mergedState = _.cloneDeep(value); - } else { - // check for changes at path, emit an event when different - const curVal = hasKeyPath ? this.get(keyPath) : this._mergedState; - stateChanged = !_.isEqual(curVal, value); - - // arrays are merge by index, not desired - ensure they are replaced - if (Array.isArray(_.get(this._mergedState, keyPath))) { - if (hasKeyPath) _.set(this._mergedState, keyPath, undefined); - else this._mergedState = undefined; - } - - if (hasKeyPath) { - _.set(this._changedState, keyPath, value); - } else { - this._changedState = _.isPlainObject(value) ? value : {}; - } - } - } - - // update the merged state value - const targetObj = this._mergedState || _.cloneDeep(this._defaultState); - const sourceObj = _.merge({}, this._changedState); - - // handler arguments are (targetValue, sourceValue, key, target, source) - const mergeMethod = function(targetValue, sourceValue, mergeKey) { - // if not initial state, skip default merge method (ie. return value, see note below) - if (!initialState && _.isEqual(keyPath, self._getIndex(mergeKey))) { - // use the sourceValue or fall back to targetValue - return !_.isUndefined(sourceValue) ? sourceValue : targetValue; - } - }; - - // If `mergeMethod` is provided it is invoked to produce the merged values of the - // destination and source properties. - // If `mergeMethod` returns `undefined` the default merging method is used - this._mergedState = _.merge(targetObj, sourceObj, mergeMethod); - - // sanity check; verify that there are actually changes - if (_.isEqual(this._mergedState, this._defaultState)) this._changedState = {}; - - if (!silent && stateChanged) this.emit('change', key); - - return this; - } -} diff --git a/src/legacy/ui/public/state_management/__tests__/state_monitor_factory.js b/src/legacy/ui/public/state_management/__tests__/state_monitor_factory.js index 1ba0500de7f03..601212d2da1a5 100644 --- a/src/legacy/ui/public/state_management/__tests__/state_monitor_factory.js +++ b/src/legacy/ui/public/state_management/__tests__/state_monitor_factory.js @@ -19,9 +19,9 @@ import expect from '@kbn/expect'; import sinon from 'sinon'; +import { EventEmitter } from 'events'; import { cloneDeep } from 'lodash'; import { stateMonitorFactory } from '../state_monitor_factory'; -import { SimpleEmitter } from '../../utils/simple_emitter'; describe('stateMonitorFactory', function() { const noop = () => {}; @@ -35,7 +35,7 @@ describe('stateMonitorFactory', function() { } function createMockState(state = {}) { - const mockState = new SimpleEmitter(); + const mockState = new EventEmitter(); setState(mockState, state, false); return mockState; } diff --git a/src/legacy/ui/public/state_management/app_state.js b/src/legacy/ui/public/state_management/app_state.js index 279aff62f55af..76675b05e0fe5 100644 --- a/src/legacy/ui/public/state_management/app_state.js +++ b/src/legacy/ui/public/state_management/app_state.js @@ -29,7 +29,7 @@ import { uiModules } from '../modules'; import { StateProvider } from './state'; -import { PersistedState } from '../persisted_state'; +import { PersistedState } from '../../../../plugins/visualizations/public'; import { createLegacyClass } from '../utils/legacy_class'; const urlParam = '_a'; diff --git a/src/legacy/ui/public/utils/simple_emitter.js b/src/legacy/ui/public/utils/simple_emitter.js deleted file mode 100644 index 6bdaae2237d55..0000000000000 --- a/src/legacy/ui/public/utils/simple_emitter.js +++ /dev/null @@ -1,141 +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 _ from 'lodash'; - -/** - * Simple event emitter class used in the vislib. Calls - * handlers synchronously and implements a chainable api - * - * @class - */ -export function SimpleEmitter() { - this._listeners = {}; -} - -/** - * Add an event handler - * - * @param {string} name - * @param {function} handler - * @return {SimpleEmitter} - this, for chaining - */ -SimpleEmitter.prototype.on = function(name, handler) { - let handlers = this._listeners[name]; - if (!handlers) handlers = this._listeners[name] = []; - - handlers.push(handler); - - return this; -}; - -/** - * Remove an event handler - * - * @param {string} name - * @param {function} [handler] - optional handler to remove, if no handler is - * passed then all are removed - * @return {SimpleEmitter} - this, for chaining - */ -SimpleEmitter.prototype.off = function(name, handler) { - if (!this._listeners[name]) { - return this; - } - - // remove a specific handler - if (handler) _.pull(this._listeners[name], handler); - // or remove all listeners - else this._listeners[name] = null; - - return this; -}; - -/** - * Remove all event listeners bound to this emitter. - * - * @return {SimpleEmitter} - this, for chaining - */ -SimpleEmitter.prototype.removeAllListeners = function() { - this._listeners = {}; - return this; -}; - -/** - * Emit an event and all arguments to all listeners for an event name - * - * @param {string} name - * @param {*} [arg...] - any number of arguments that will be applied to each handler - * @return {SimpleEmitter} - this, for chaining - */ -SimpleEmitter.prototype.emit = _.restParam(function(name, args) { - if (!this._listeners[name]) return this; - const listeners = this.listeners(name); - let i = -1; - - while (++i < listeners.length) { - listeners[i].apply(this, args); - } - - return this; -}); - -/** - * Get a list of the event names that currently have listeners - * - * @return {array[string]} - */ -SimpleEmitter.prototype.activeEvents = function() { - return _.reduce( - this._listeners, - function(active, listeners, name) { - return active.concat(_.size(listeners) ? name : []); - }, - [] - ); -}; - -/** - * Get a list of the handler functions for a specific event - * - * @param {string} name - * @return {array[function]} - */ -SimpleEmitter.prototype.listeners = function(name) { - return this._listeners[name] ? this._listeners[name].slice(0) : []; -}; - -/** - * Get the count of handlers for a specific event - * - * @param {string} [name] - optional event name to filter by - * @return {number} - */ -SimpleEmitter.prototype.listenerCount = function(name) { - if (name) { - return _.size(this._listeners[name]); - } - - return _.reduce( - this._listeners, - function(count, handlers) { - return count + _.size(handlers); - }, - 0 - ); -}; diff --git a/src/legacy/ui/public/utils/simple_emitter.test.js b/src/legacy/ui/public/utils/simple_emitter.test.js deleted file mode 100644 index 723a59ccba1c6..0000000000000 --- a/src/legacy/ui/public/utils/simple_emitter.test.js +++ /dev/null @@ -1,176 +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 { SimpleEmitter } from './simple_emitter'; -import sinon from 'sinon'; - -describe('SimpleEmitter class', () => { - let emitter; - - beforeEach(() => { - emitter = new SimpleEmitter(); - }); - - it('constructs an event emitter', () => { - expect(emitter).toHaveProperty('on'); - expect(emitter).toHaveProperty('off'); - expect(emitter).toHaveProperty('emit'); - expect(emitter).toHaveProperty('listenerCount'); - expect(emitter).toHaveProperty('removeAllListeners'); - }); - - describe('#listenerCount', () => { - it('counts all event listeners without any arg', () => { - expect(emitter.listenerCount()).toBe(0); - emitter.on('a', () => {}); - expect(emitter.listenerCount()).toBe(1); - emitter.on('b', () => {}); - expect(emitter.listenerCount()).toBe(2); - }); - - it('limits to the event that is passed in', () => { - expect(emitter.listenerCount()).toBe(0); - emitter.on('a', () => {}); - expect(emitter.listenerCount('a')).toBe(1); - emitter.on('a', () => {}); - expect(emitter.listenerCount('a')).toBe(2); - emitter.on('b', () => {}); - expect(emitter.listenerCount('a')).toBe(2); - expect(emitter.listenerCount('b')).toBe(1); - expect(emitter.listenerCount()).toBe(3); - }); - }); - - describe('#on', () => { - it('registers a handler', () => { - const handler = sinon.stub(); - emitter.on('a', handler); - expect(emitter.listenerCount('a')).toBe(1); - - expect(handler.callCount).toBe(0); - emitter.emit('a'); - expect(handler.callCount).toBe(1); - }); - - it('allows multiple event handlers for the same event', () => { - emitter.on('a', () => {}); - emitter.on('a', () => {}); - expect(emitter.listenerCount('a')).toBe(2); - }); - - it('allows the same function to be registered multiple times', () => { - const handler = () => {}; - emitter.on('a', handler); - expect(emitter.listenerCount()).toBe(1); - emitter.on('a', handler); - expect(emitter.listenerCount()).toBe(2); - }); - }); - - describe('#off', () => { - it('removes a listener if it was registered', () => { - const handler = sinon.stub(); - expect(emitter.listenerCount()).toBe(0); - emitter.on('a', handler); - expect(emitter.listenerCount('a')).toBe(1); - emitter.off('a', handler); - expect(emitter.listenerCount('a')).toBe(0); - }); - - it('clears all listeners if no handler is passed', () => { - emitter.on('a', () => {}); - emitter.on('a', () => {}); - expect(emitter.listenerCount()).toBe(2); - emitter.off('a'); - expect(emitter.listenerCount()).toBe(0); - }); - - it('does not mind if the listener is not registered', () => { - emitter.off('a', () => {}); - }); - - it('does not mind if the event has no listeners', () => { - emitter.off('a'); - }); - }); - - describe('#emit', () => { - it('calls the handlers in the order they were defined', () => { - let i = 0; - const incr = () => ++i; - const one = sinon.spy(incr); - const two = sinon.spy(incr); - const three = sinon.spy(incr); - const four = sinon.spy(incr); - - emitter - .on('a', one) - .on('a', two) - .on('a', three) - .on('a', four) - .emit('a'); - - expect(one).toHaveProperty('callCount', 1); - expect(one.returned(1)).toBeDefined(); - - expect(two).toHaveProperty('callCount', 1); - expect(two.returned(2)).toBeDefined(); - - expect(three).toHaveProperty('callCount', 1); - expect(three.returned(3)).toBeDefined(); - - expect(four).toHaveProperty('callCount', 1); - expect(four.returned(4)).toBeDefined(); - }); - - it('always emits the handlers that were initially registered', () => { - const destructive = sinon.spy(() => { - emitter.removeAllListeners(); - expect(emitter.listenerCount()).toBe(0); - }); - const stub = sinon.stub(); - - emitter - .on('run', destructive) - .on('run', stub) - .emit('run'); - - expect(destructive).toHaveProperty('callCount', 1); - expect(stub).toHaveProperty('callCount', 1); - }); - - it('applies all arguments except the first', () => { - emitter - .on('a', (a, b, c) => { - expect(a).toBe('foo'); - expect(b).toBe('bar'); - expect(c).toBe('baz'); - }) - .emit('a', 'foo', 'bar', 'baz'); - }); - - it('uses the SimpleEmitter as the this context', () => { - emitter - .on('a', function() { - expect(this).toBe(emitter); - }) - .emit('a'); - }); - }); -}); diff --git a/src/plugins/apm_oss/server/index.ts b/src/plugins/apm_oss/server/index.ts index 801140694c139..95a4ae4519bc9 100644 --- a/src/plugins/apm_oss/server/index.ts +++ b/src/plugins/apm_oss/server/index.ts @@ -38,4 +38,4 @@ export function plugin(initializerContext: PluginInitializerContext) { export type APMOSSConfig = TypeOf; -export { APMOSSPlugin as Plugin }; +export { APMOSSPluginSetup } from './plugin'; diff --git a/src/plugins/apm_oss/server/plugin.ts b/src/plugins/apm_oss/server/plugin.ts index 2708f7729482b..9b14d19da90c2 100644 --- a/src/plugins/apm_oss/server/plugin.ts +++ b/src/plugins/apm_oss/server/plugin.ts @@ -20,7 +20,7 @@ import { Plugin, CoreSetup, PluginInitializerContext } from 'src/core/server'; import { Observable } from 'rxjs'; import { APMOSSConfig } from './'; -export class APMOSSPlugin implements Plugin<{ config$: Observable }> { +export class APMOSSPlugin implements Plugin { constructor(private readonly initContext: PluginInitializerContext) { this.initContext = initContext; } @@ -36,3 +36,7 @@ export class APMOSSPlugin implements Plugin<{ config$: Observable start() {} stop() {} } + +export interface APMOSSPluginSetup { + config$: Observable; +} diff --git a/src/plugins/bfetch/common/buffer/tests/timed_item_buffer.test.ts b/src/plugins/bfetch/common/buffer/tests/timed_item_buffer.test.ts index c1c6a8f187a44..e1640927c4ead 100644 --- a/src/plugins/bfetch/common/buffer/tests/timed_item_buffer.test.ts +++ b/src/plugins/bfetch/common/buffer/tests/timed_item_buffer.test.ts @@ -20,7 +20,8 @@ import { TimedItemBuffer } from '../timed_item_buffer'; import { runItemBufferTests } from './run_item_buffer_tests'; -describe('TimedItemBuffer', () => { +// FLAKY: https://github.com/elastic/kibana/issues/58662 +describe.skip('TimedItemBuffer', () => { runItemBufferTests(TimedItemBuffer); test('does not do unnecessary flushes', async () => { diff --git a/src/plugins/data/public/query/filter_manager/lib/generate_filters.ts b/src/plugins/data/public/query/filter_manager/lib/generate_filters.ts index 105e932f696f0..4220df7b1a49b 100644 --- a/src/plugins/data/public/query/filter_manager/lib/generate_filters.ts +++ b/src/plugins/data/public/query/filter_manager/lib/generate_filters.ts @@ -103,11 +103,12 @@ export function generateFilters( filter = existing; } else { const tmpIndexPattern = { id: index } as IIndexPattern; - + // exists filter special case: fieldname = '_exists' and value = fieldname const filterType = fieldName === '_exists_' ? FILTERS.EXISTS : FILTERS.PHRASE; + const actualFieldObj = fieldName === '_exists_' ? ({ name: value } as IFieldType) : fieldObj; filter = buildFilter( tmpIndexPattern, - fieldObj, + actualFieldObj, filterType, negate, false, diff --git a/src/plugins/kibana_legacy/public/utils/register_listen_event_listener.js b/src/plugins/kibana_legacy/public/utils/register_listen_event_listener.js index cce6c98adbbfe..6cc9a5766d3fe 100644 --- a/src/plugins/kibana_legacy/public/utils/register_listen_event_listener.js +++ b/src/plugins/kibana_legacy/public/utils/register_listen_event_listener.js @@ -21,7 +21,7 @@ export function registerListenEventListener($rootScope) { * Helper that registers an event listener, and removes that listener when * the $scope is destroyed. * - * @param {SimpleEmitter} emitter - the event emitter to listen to + * @param {EventEmitter} emitter - the event emitter to listen to * @param {string} eventName - the event name * @param {Function} handler - the event handler * @return {undefined} diff --git a/src/legacy/ui/public/persisted_state/errors.ts b/src/plugins/usage_collection/server/mocks.ts similarity index 58% rename from src/legacy/ui/public/persisted_state/errors.ts rename to src/plugins/usage_collection/server/mocks.ts index 164981107298d..2194b1fb83f6e 100644 --- a/src/legacy/ui/public/persisted_state/errors.ts +++ b/src/plugins/usage_collection/server/mocks.ts @@ -17,10 +17,22 @@ * under the License. */ -import { KbnError } from '../../../../plugins/kibana_utils/public'; +import { loggingServiceMock } from '../../../core/server/mocks'; +import { UsageCollectionSetup } from './plugin'; +import { CollectorSet } from './collector'; -export class PersistedStateError extends KbnError { - constructor() { - super('Error with the persisted state'); - } -} +const createSetupContract = () => { + return { + ...new CollectorSet({ + logger: loggingServiceMock.createLogger(), + maximumWaitTimeForAllCollectorsInS: 1, + }), + registerLegacySavedObjects: jest.fn() as jest.Mocked< + UsageCollectionSetup['registerLegacySavedObjects'] + >, + } as UsageCollectionSetup; +}; + +export const usageCollectionPluginMock = { + createSetupContract, +}; diff --git a/src/plugins/visualizations/public/index.ts b/src/plugins/visualizations/public/index.ts index 2b4b10c8329a3..c08dbf890b8da 100644 --- a/src/plugins/visualizations/public/index.ts +++ b/src/plugins/visualizations/public/index.ts @@ -27,3 +27,5 @@ export function plugin(initializerContext: PluginInitializerContext) { export { VisualizationsPublicPlugin as Plugin }; export * from './plugin'; export * from './types'; + +export { PersistedState } from './persisted_state'; diff --git a/src/legacy/ui/public/persisted_state/index.d.ts b/src/plugins/visualizations/public/persisted_state/index.ts similarity index 100% rename from src/legacy/ui/public/persisted_state/index.d.ts rename to src/plugins/visualizations/public/persisted_state/index.ts diff --git a/src/plugins/visualizations/public/persisted_state/persisted_state.ts b/src/plugins/visualizations/public/persisted_state/persisted_state.ts new file mode 100644 index 0000000000000..d09dcd5381511 --- /dev/null +++ b/src/plugins/visualizations/public/persisted_state/persisted_state.ts @@ -0,0 +1,254 @@ +/* + * 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 { EventEmitter } from 'events'; + +import { isPlainObject, cloneDeep, get, set, isEqual, isString, merge } from 'lodash'; +import toPath from 'lodash/internal/toPath'; + +function prepSetParams(key: PersistedStateKey, value: any, path: PersistedStatePath) { + // key must be the value, set the entire state using it + if (value === undefined && (isPlainObject(key) || path.length > 0)) { + // setting entire tree, swap the key and value to write to the state + return { + value: key, + key: undefined, + }; + } + + // ensure the value being passed in is never mutated + return { + value: cloneDeep(value), + key, + }; +} + +type PersistedStateKey = string | string[] | undefined; +type PersistedStatePath = string | string[]; + +export class PersistedState extends EventEmitter { + private readonly _path: PersistedStatePath; + private readonly _initialized: boolean; + + private _changedState: any; + private _defaultState: any; + private _mergedState: any; + + constructor(value?: any, path?: PersistedStatePath) { + super(); + + this._path = this.setPath(path); + + // Some validations + if (!this._path.length && value && !isPlainObject(value)) { + throw new Error('State value must be a plain object'); + } + + value = value || this.getDefault(); + + // copy passed state values and create internal trackers + this.set(value); + this._initialized = true; // used to track state changes + } + + get(key?: PersistedStateKey, defaultValue?: any) { + // no path and no key, get the whole state + if (!this.hasPath() && key === undefined) { + return this._mergedState; + } + + return cloneDeep(get(this._mergedState, this.getIndex(key || ''), defaultValue)); + } + + set(key: PersistedStateKey | any, value?: any) { + const params = prepSetParams(key, value, this._path); + const val = this.setValue(params.key, params.value); + + this.emit('set'); + return val; + } + + setSilent(key: PersistedStateKey | any, value?: any) { + const params = prepSetParams(key, value, this._path); + + if (params.key) { + return this.setValue(params.key, params.value, true); + } + } + + clearAllKeys() { + Object.getOwnPropertyNames(this._changedState).forEach(key => { + this.set(key, null); + }); + } + + reset(path: PersistedStatePath) { + const keyPath = this.getIndex(path); + const origValue = get(this._defaultState, keyPath); + const currentValue = get(this._mergedState, keyPath); + + if (origValue === undefined) { + this.cleanPath(path, this._mergedState); + } else { + set(this._mergedState, keyPath, origValue); + } + + // clean up the changedState tree + this.cleanPath(path, this._changedState); + + if (!isEqual(currentValue, origValue)) this.emit('change'); + } + + getChanges() { + return cloneDeep(this._changedState); + } + + toJSON() { + return this.get(); + } + + toString() { + return JSON.stringify(this.toJSON()); + } + + fromString(input: string) { + return this.set(JSON.parse(input)); + } + + private getIndex(key: PersistedStateKey) { + if (key === undefined) return this._path; + + return [...(this._path || []), ...toPath(key)]; + } + + private getPartialIndex(key: PersistedStateKey) { + const keyPath = this.getIndex(key); + + return keyPath.slice(this._path.length); + } + + private cleanPath(path: PersistedStatePath, stateTree: any) { + const partialPath = this.getPartialIndex(path); + let remove = true; + + if (Array.isArray(partialPath)) { + // recursively delete value tree, when no other keys exist + while (partialPath.length > 0) { + const lastKey = partialPath.splice(partialPath.length - 1, 1)[0]; + const statePath = [...this._path, partialPath]; + const stateVal = statePath.length > 0 ? get(stateTree, statePath) : stateTree; + + // if stateVal isn't an object, do nothing + if (!isPlainObject(stateVal)) return; + + if (remove) delete stateVal[lastKey]; + if (Object.keys(stateVal).length > 0) remove = false; + } + } + } + + private getDefault() { + return this.hasPath() ? undefined : {}; + } + + private setPath(path?: PersistedStatePath): string[] { + if (Array.isArray(path)) { + return path; + } + + if (isString(path)) { + return [...this.getIndex(path)]; + } + + return []; + } + + private hasPath() { + return this._path.length > 0; + } + + private setValue(key: PersistedStateKey, value: any, silent: boolean = false) { + const self = this; + let stateChanged = false; + const initialState = !this._initialized; + const keyPath = this.getIndex(key); + const hasKeyPath = keyPath.length > 0; + + // if this is the initial state value, save value as the default + if (initialState) { + this._changedState = {}; + if (!this.hasPath() && key === undefined) this._defaultState = value; + else this._defaultState = set({}, keyPath, value); + } + + if (!initialState) { + // no path and no key, set the whole state + if (!this.hasPath() && key === undefined) { + // compare changedState and new state, emit an event when different + stateChanged = !isEqual(this._changedState, value); + this._changedState = value; + this._mergedState = cloneDeep(value); + } else { + // check for changes at path, emit an event when different + const curVal = hasKeyPath ? this.get(keyPath) : this._mergedState; + stateChanged = !isEqual(curVal, value); + + // arrays are merge by index, not desired - ensure they are replaced + if (Array.isArray(get(this._mergedState, keyPath))) { + if (hasKeyPath) { + set(this._mergedState, keyPath, undefined); + } else { + this._mergedState = undefined; + } + } + + if (hasKeyPath) { + set(this._changedState, keyPath, value); + } else { + this._changedState = isPlainObject(value) ? value : {}; + } + } + } + + // update the merged state value + const targetObj = this._mergedState || cloneDeep(this._defaultState); + const sourceObj = merge({}, this._changedState); + + // handler arguments are (targetValue, sourceValue, key, target, source) + const mergeMethod = function(targetValue: any, sourceValue: any, mergeKey: string) { + // if not initial state, skip default merge method (ie. return value, see note below) + if (!initialState && isEqual(keyPath, self.getIndex(mergeKey))) { + // use the sourceValue or fall back to targetValue + return sourceValue === undefined ? targetValue : sourceValue; + } + }; + + // If `mergeMethod` is provided it is invoked to produce the merged values of the + // destination and source properties. + // If `mergeMethod` returns `undefined` the default merging method is used + this._mergedState = merge(targetObj, sourceObj, mergeMethod); + + // sanity check; verify that there are actually changes + if (isEqual(this._mergedState, this._defaultState)) this._changedState = {}; + + if (!silent && stateChanged) this.emit('change', key); + + return this; + } +} diff --git a/src/legacy/ui/public/persisted_state/persisted_state_provider.test.ts b/src/plugins/visualizations/public/persisted_state/persisted_state_provider.test.ts similarity index 96% rename from src/legacy/ui/public/persisted_state/persisted_state_provider.test.ts rename to src/plugins/visualizations/public/persisted_state/persisted_state_provider.test.ts index f14215002cf8f..76446a3f44861 100644 --- a/src/legacy/ui/public/persisted_state/persisted_state_provider.test.ts +++ b/src/plugins/visualizations/public/persisted_state/persisted_state_provider.test.ts @@ -17,7 +17,6 @@ * under the License. */ -import { PersistedStateError } from './errors'; import { PersistedState } from './persisted_state'; describe('Persisted State Provider', () => { @@ -47,7 +46,7 @@ describe('Persisted State Provider', () => { }); test('should throw if given an invalid value', () => { - expect(() => new PersistedState('bananas')).toThrow(PersistedStateError); + expect(() => new PersistedState('bananas')).toThrow(Error); }); }); @@ -224,13 +223,13 @@ describe('Persisted State Provider', () => { describe('internal state tracking', () => { test('should be an empty object', () => { const persistedState = new PersistedState(); - expect(persistedState._defaultState).toEqual({}); + expect(persistedState).toHaveProperty('_defaultState', {}); }); test('should store the default state value', () => { const val = { one: 1, two: 2 }; const persistedState = new PersistedState(val); - expect(persistedState._defaultState).toEqual(val); + expect(persistedState).toHaveProperty('_defaultState', val); }); test('should keep track of changes', () => { @@ -238,8 +237,8 @@ describe('Persisted State Provider', () => { const persistedState = new PersistedState(val); persistedState.set('two', 22); - expect(persistedState._defaultState).toEqual(val); - expect(persistedState._changedState).toEqual({ two: 22 }); + expect(persistedState).toHaveProperty('_defaultState', val); + expect(persistedState).toHaveProperty('_changedState', { two: 22 }); }); }); diff --git a/tasks/test_jest.js b/tasks/test_jest.js index ff1a941610ad9..bcb05a83675e5 100644 --- a/tasks/test_jest.js +++ b/tasks/test_jest.js @@ -33,7 +33,7 @@ module.exports = function(grunt) { function runJest(jestScript) { const serverCmd = { cmd: 'node', - args: [jestScript, '--ci'], + args: [jestScript, '--ci', '--detectOpenHandles'], opts: { stdio: 'inherit' }, }; diff --git a/test/functional/apps/context/_filters.js b/test/functional/apps/context/_filters.js index 4cd3f1a54b771..721f8a50d0e46 100644 --- a/test/functional/apps/context/_filters.js +++ b/test/functional/apps/context/_filters.js @@ -39,14 +39,13 @@ export default function({ getService, getPageObjects }) { }); }); - it('should be addable via expanded doc table rows', async function() { + it('inclusive filter should be addable via expanded doc table rows', async function() { await docTable.toggleRowExpanded({ isAnchorRow: true }); await retry.try(async () => { const anchorDetailsRow = await docTable.getAnchorDetailsRow(); await docTable.addInclusiveFilter(anchorDetailsRow, TEST_ANCHOR_FILTER_FIELD); await PageObjects.context.waitUntilContextLoadingHasFinished(); - // await docTable.toggleRowExpanded({ isAnchorRow: true }); expect( await filterBar.hasFilter(TEST_ANCHOR_FILTER_FIELD, TEST_ANCHOR_FILTER_VALUE, true) ).to.be(true); @@ -58,14 +57,14 @@ export default function({ getService, getPageObjects }) { }); }); - it('should be toggleable via the filter bar', async function() { + it('inclusive filter should be toggleable via the filter bar', async function() { await filterBar.addFilter(TEST_ANCHOR_FILTER_FIELD, 'IS', TEST_ANCHOR_FILTER_VALUE); await PageObjects.context.waitUntilContextLoadingHasFinished(); // disable filter await filterBar.toggleFilterEnabled(TEST_ANCHOR_FILTER_FIELD); await PageObjects.context.waitUntilContextLoadingHasFinished(); - retry.try(async () => { + await retry.try(async () => { expect( await filterBar.hasFilter(TEST_ANCHOR_FILTER_FIELD, TEST_ANCHOR_FILTER_VALUE, false) ).to.be(true); @@ -76,5 +75,16 @@ export default function({ getService, getPageObjects }) { expect(hasOnlyFilteredRows).to.be(false); }); }); + + it('filter for presence should be addable via expanded doc table rows', async function() { + await docTable.toggleRowExpanded({ isAnchorRow: true }); + + await retry.try(async () => { + const anchorDetailsRow = await docTable.getAnchorDetailsRow(); + await docTable.addExistsFilter(anchorDetailsRow, TEST_ANCHOR_FILTER_FIELD); + await PageObjects.context.waitUntilContextLoadingHasFinished(); + expect(await filterBar.hasFilter(TEST_ANCHOR_FILTER_FIELD, 'exists', true)).to.be(true); + }); + }); }); } diff --git a/test/functional/apps/context/_size.js b/test/functional/apps/context/_size.js index 5f3d1ebe40974..ea9b2c8cf1819 100644 --- a/test/functional/apps/context/_size.js +++ b/test/functional/apps/context/_size.js @@ -30,7 +30,8 @@ export default function({ getService, getPageObjects }) { const docTable = getService('docTable'); const PageObjects = getPageObjects(['context']); - describe('context size', function contextSize() { + // FLAKY: https://github.com/elastic/kibana/issues/53888 + describe.skip('context size', function contextSize() { before(async function() { await kibanaServer.uiSettings.update({ 'context:defaultSize': `${TEST_DEFAULT_CONTEXT_SIZE}`, diff --git a/test/functional/apps/dashboard/panel_expand_toggle.js b/test/functional/apps/dashboard/panel_expand_toggle.js index 930445a67aa20..5e7d55706968d 100644 --- a/test/functional/apps/dashboard/panel_expand_toggle.js +++ b/test/functional/apps/dashboard/panel_expand_toggle.js @@ -56,7 +56,7 @@ export default function({ getService, getPageObjects }) { // Add a retry to fix https://github.com/elastic/kibana/issues/14574. Perhaps the recent changes to this // being a CSS update is causing the UI to change slower than grabbing the panels? - retry.try(async () => { + await retry.try(async () => { const panelCountAfterMaxThenMinimize = await PageObjects.dashboard.getPanelCount(); expect(panelCountAfterMaxThenMinimize).to.be(panelCount); }); diff --git a/test/functional/apps/visualize/_tsvb_markdown.ts b/test/functional/apps/visualize/_tsvb_markdown.ts index b7307ac9c6cab..d37404a3d60cb 100644 --- a/test/functional/apps/visualize/_tsvb_markdown.ts +++ b/test/functional/apps/visualize/_tsvb_markdown.ts @@ -121,7 +121,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { await visualBuilder.markdownSwitchSubTab('data'); await visualBuilder.cloneSeries(); - retry.try(async function seriesCountCheck() { + await retry.try(async function seriesCountCheck() { const seriesLength = (await visualBuilder.getSeries()).length; expect(seriesLength).to.be.equal(2); }); @@ -131,7 +131,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { await visualBuilder.markdownSwitchSubTab('data'); await visualBuilder.createNewAgg(); - retry.try(async function aggregationCountCheck() { + await retry.try(async function aggregationCountCheck() { const aggregationLength = await visualBuilder.getAggregationCount(); expect(aggregationLength).to.be.equal(2); }); diff --git a/test/functional/services/doc_table.ts b/test/functional/services/doc_table.ts index 6957b0fa99929..2530831e0f6f9 100644 --- a/test/functional/services/doc_table.ts +++ b/test/functional/services/doc_table.ts @@ -98,13 +98,12 @@ export function DocTableProvider({ getService, getPageObjects }: FtrProviderCont const $ = await table.parseDomContent(); const rowLocator = options.isAnchorRow ? '~docTableAnchorRow' : '~docTableRow'; const rows = $.findTestSubjects(rowLocator).toArray(); - const fields = rows.map((row: any) => + return rows.map((row: any) => $(row) .find('[data-test-subj~="docTableField"]') .toArray() .map((field: any) => $(field).text()) ); - return fields; } public async getHeaderFields(): Promise { @@ -144,6 +143,22 @@ export function DocTableProvider({ getService, getPageObjects }: FtrProviderCont await PageObjects.header.awaitGlobalLoadingIndicatorHidden(); } + public async getAddExistsFilterButton( + tableDocViewRow: WebElementWrapper + ): Promise { + return await tableDocViewRow.findByCssSelector(`[data-test-subj~="addExistsFilterButton"]`); + } + + public async addExistsFilter( + detailsRow: WebElementWrapper, + fieldName: WebElementWrapper + ): Promise { + const tableDocViewRow = await this.getTableDocViewRow(detailsRow, fieldName); + const addInclusiveFilterButton = await this.getAddExistsFilterButton(tableDocViewRow); + await addInclusiveFilterButton.click(); + await PageObjects.header.awaitGlobalLoadingIndicatorHidden(); + } + public async toggleRowExpanded( options: SelectOptions = { isAnchorRow: false, rowIndex: 0 } ): Promise { diff --git a/test/scripts/jenkins_xpack.sh b/test/scripts/jenkins_xpack.sh index 3d30496ecb582..b629e064b39b5 100755 --- a/test/scripts/jenkins_xpack.sh +++ b/test/scripts/jenkins_xpack.sh @@ -11,7 +11,7 @@ if [[ -z "$CODE_COVERAGE" ]] ; then echo " -> Running jest tests" cd "$XPACK_DIR" - checks-reporter-with-killswitch "X-Pack Jest" node scripts/jest --ci --verbose + checks-reporter-with-killswitch "X-Pack Jest" node scripts/jest --ci --verbose --detectOpenHandles echo "" echo "" diff --git a/x-pack/.i18nrc.json b/x-pack/.i18nrc.json index bb084b3bb72a1..66342266f1dbc 100644 --- a/x-pack/.i18nrc.json +++ b/x-pack/.i18nrc.json @@ -26,7 +26,7 @@ "xpack.logstash": "legacy/plugins/logstash", "xpack.main": "legacy/plugins/xpack_main", "xpack.maps": "legacy/plugins/maps", - "xpack.ml": "legacy/plugins/ml", + "xpack.ml": ["plugins/ml", "legacy/plugins/ml"], "xpack.monitoring": "legacy/plugins/monitoring", "xpack.remoteClusters": "plugins/remote_clusters", "xpack.reporting": ["plugins/reporting", "legacy/plugins/reporting"], diff --git a/x-pack/legacy/plugins/apm/readme.md b/x-pack/legacy/plugins/apm/readme.md index a513249c296db..0edcdc279815c 100644 --- a/x-pack/legacy/plugins/apm/readme.md +++ b/x-pack/legacy/plugins/apm/readme.md @@ -74,10 +74,14 @@ node scripts/jest.js plugins/apm --updateSnapshot ### Functional tests **Start server** -`node scripts/functional_tests_server --config x-pack/test/functional/config.js` +``` +node scripts/functional_tests_server --config x-pack/test/functional/config.js +``` **Run tests** -`node scripts/functional_test_runner --config x-pack/test/functional/config.js --grep='APM specs'` +``` +node scripts/functional_test_runner --config x-pack/test/functional/config.js --grep='APM specs' +``` APM tests are located in `x-pack/test/functional/apps/apm`. For debugging access Elasticsearch on http://localhost:9220` (elastic/changeme) @@ -85,10 +89,14 @@ For debugging access Elasticsearch on http://localhost:9220` (elastic/changeme) ### API integration tests **Start server** -`node scripts/functional_tests_server --config x-pack/test/api_integration/config.js` +``` +node scripts/functional_tests_server --config x-pack/test/api_integration/config.js +``` **Run tests** -`node scripts/functional_test_runner --config x-pack/test/api_integration/config.js --grep='APM specs'` +``` +node scripts/functional_test_runner --config x-pack/test/api_integration/config.js --grep='APM specs' +``` APM tests are located in `x-pack/test/api_integration/apis/apm`. For debugging access Elasticsearch on http://localhost:9220` (elastic/changeme) diff --git a/x-pack/legacy/plugins/maps/public/index.ts b/x-pack/legacy/plugins/maps/public/index.ts index f3213a36bb66d..27cd64103eec9 100644 --- a/x-pack/legacy/plugins/maps/public/index.ts +++ b/x-pack/legacy/plugins/maps/public/index.ts @@ -11,7 +11,6 @@ import 'uiExports/inspectorViews'; import 'uiExports/search'; import 'uiExports/embeddableFactories'; import 'uiExports/embeddableActions'; -import 'ui/agg_types'; import 'ui/autoload/all'; import 'react-vis/dist/style.css'; diff --git a/x-pack/legacy/plugins/maps/public/layers/fields/es_agg_field.js b/x-pack/legacy/plugins/maps/public/layers/fields/es_agg_field.js index 28c199b64d3ef..27ab8fc5bfb3a 100644 --- a/x-pack/legacy/plugins/maps/public/layers/fields/es_agg_field.js +++ b/x-pack/legacy/plugins/maps/public/layers/fields/es_agg_field.js @@ -21,13 +21,17 @@ export class ESAggMetricField extends AbstractField { } getName() { - return this._source.formatMetricKey(this.getAggType(), this.getESDocFieldName()); + return this._source.getAggKey(this.getAggType(), this.getRootName()); + } + + getRootName() { + return this._getESDocFieldName(); } async getLabel() { return this._label - ? await this._label - : this._source.formatMetricLabel(this.getAggType(), this.getESDocFieldName()); + ? this._label + : this._source.getAggLabel(this.getAggType(), this.getRootName()); } getAggType() { @@ -42,13 +46,13 @@ export class ESAggMetricField extends AbstractField { return this.getAggType() === AGG_TYPE.TERMS ? 'string' : 'number'; } - getESDocFieldName() { + _getESDocFieldName() { return this._esDocField ? this._esDocField.getName() : ''; } getRequestDescription() { return this.getAggType() !== AGG_TYPE.COUNT - ? `${this.getAggType()} ${this.getESDocFieldName()}` + ? `${this.getAggType()} ${this.getRootName()}` : AGG_TYPE.COUNT; } @@ -64,7 +68,7 @@ export class ESAggMetricField extends AbstractField { } getValueAggDsl(indexPattern) { - const field = getField(indexPattern, this.getESDocFieldName()); + const field = getField(indexPattern, this.getRootName()); const aggType = this.getAggType(); const aggBody = aggType === AGG_TYPE.TERMS ? { size: 1, shard_size: 1 } : {}; return { @@ -77,6 +81,11 @@ export class ESAggMetricField extends AbstractField { return !isMetricCountable(this.getAggType()); } + canValueBeFormatted() { + // Do not use field formatters for counting metrics + return ![AGG_TYPE.COUNT, AGG_TYPE.UNIQUE_COUNT].includes(this.getAggType()); + } + async getOrdinalFieldMetaRequest(config) { return this._esDocField.getOrdinalFieldMetaRequest(config); } diff --git a/x-pack/legacy/plugins/maps/public/layers/fields/field.js b/x-pack/legacy/plugins/maps/public/layers/fields/field.js index b5d157ad1697a..2dd553f66755f 100644 --- a/x-pack/legacy/plugins/maps/public/layers/fields/field.js +++ b/x-pack/legacy/plugins/maps/public/layers/fields/field.js @@ -17,6 +17,14 @@ export class AbstractField { return this._fieldName; } + getRootName() { + return this.getName(); + } + + canValueBeFormatted() { + return true; + } + getSource() { return this._source; } diff --git a/x-pack/legacy/plugins/maps/public/layers/joins/inner_join.test.js b/x-pack/legacy/plugins/maps/public/layers/joins/inner_join.test.js index 05b177b361449..4a91ed3a3eafb 100644 --- a/x-pack/legacy/plugins/maps/public/layers/joins/inner_join.test.js +++ b/x-pack/legacy/plugins/maps/public/layers/joins/inner_join.test.js @@ -7,12 +7,6 @@ import { InnerJoin } from './inner_join'; jest.mock('../../kibana_services', () => {}); -jest.mock('ui/agg_types', () => { - class MockSchemas {} - return { - Schemas: MockSchemas, - }; -}); jest.mock('ui/timefilter', () => {}); jest.mock('../vector_layer', () => {}); diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_agg_source.js b/x-pack/legacy/plugins/maps/public/layers/sources/es_agg_source.js index bee35216f59da..775535d9e2299 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/es_agg_source.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_agg_source.js @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { i18n } from '@kbn/i18n'; import { AbstractESSource } from './es_source'; import { ESAggMetricField } from '../fields/es_agg_field'; import { ESDocField } from '../fields/es_doc_field'; @@ -72,12 +73,22 @@ export class AbstractESAggSource extends AbstractESSource { return metrics; } - formatMetricKey(aggType, fieldName) { + getAggKey(aggType, fieldName) { return aggType !== AGG_TYPE.COUNT ? `${aggType}${AGG_DELIMITER}${fieldName}` : COUNT_PROP_NAME; } - formatMetricLabel(aggType, fieldName) { - return aggType !== AGG_TYPE.COUNT ? `${aggType} of ${fieldName}` : COUNT_PROP_LABEL; + getAggLabel(aggType, fieldName) { + switch (aggType) { + case AGG_TYPE.COUNT: + return COUNT_PROP_LABEL; + case AGG_TYPE.TERMS: + return i18n.translate('xpack.maps.source.esAggSource.topTermLabel', { + defaultMessage: `Top {fieldName}`, + values: { fieldName }, + }); + default: + return `${aggType} ${fieldName}`; + } } getValueAggsDsl(indexPattern) { diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_source.js b/x-pack/legacy/plugins/maps/public/layers/sources/es_source.js index 782f2845ceeff..f575fd05c8061 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/es_source.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_source.js @@ -14,11 +14,10 @@ import { import { createExtentFilter } from '../../elasticsearch_geo_utils'; import { timefilter } from 'ui/timefilter'; import _ from 'lodash'; -import { AggConfigs } from 'ui/agg_types'; import { i18n } from '@kbn/i18n'; import uuid from 'uuid/v4'; import { copyPersistentState } from '../../reducers/util'; -import { ES_GEO_FIELD_TYPE, AGG_TYPE } from '../../../common/constants'; +import { ES_GEO_FIELD_TYPE } from '../../../common/constants'; import { DataRequestAbortError } from '../util/data_request'; import { expandToTileBoundaries } from './es_geo_grid_source/geo_tile_utils'; @@ -73,10 +72,6 @@ export class AbstractESSource extends AbstractVectorSource { return clonedDescriptor; } - getMetricFields() { - return []; - } - async _runEsQuery({ requestId, requestName, @@ -151,27 +146,18 @@ export class AbstractESSource extends AbstractVectorSource { { sourceQuery, query, timeFilters, filters, applyGlobalQuery }, 0 ); - const geoField = await this._getGeoField(); - const indexPattern = await this.getIndexPattern(); - - const geoBoundsAgg = [ - { - type: 'geo_bounds', - enabled: true, - params: { - field: geoField, + searchSource.setField('aggs', { + fitToBounds: { + geo_bounds: { + field: this._descriptor.geoField, }, - schema: 'metric', }, - ]; - - const aggConfigs = new AggConfigs(indexPattern, geoBoundsAgg); - searchSource.setField('aggs', aggConfigs.toDsl()); + }); let esBounds; try { const esResp = await searchSource.fetch(); - esBounds = _.get(esResp, 'aggregations.1.bounds'); + esBounds = _.get(esResp, 'aggregations.fitToBounds.bounds'); } catch (error) { esBounds = { top_left: { @@ -264,23 +250,7 @@ export class AbstractESSource extends AbstractVectorSource { return this._descriptor.id; } - async getFieldFormatter(fieldName) { - const metricField = this.getMetricFields().find(field => field.getName() === fieldName); - - // Do not use field formatters for counting metrics - if ( - metricField && - (metricField.type === AGG_TYPE.COUNT || metricField.type === AGG_TYPE.UNIQUE_COUNT) - ) { - return null; - } - - // fieldName could be an aggregation so it needs to be unpacked to expose raw field. - const realFieldName = metricField ? metricField.getESDocFieldName() : fieldName; - if (!realFieldName) { - return null; - } - + async createFieldFormatter(field) { let indexPattern; try { indexPattern = await this.getIndexPattern(); @@ -288,7 +258,7 @@ export class AbstractESSource extends AbstractVectorSource { return null; } - const fieldFromIndexPattern = indexPattern.fields.getByName(realFieldName); + const fieldFromIndexPattern = indexPattern.fields.getByName(field.getRootName()); if (!fieldFromIndexPattern) { return null; } @@ -346,25 +316,19 @@ export class AbstractESSource extends AbstractVectorSource { return resp.aggregations; } - getValueSuggestions = async (fieldName, query) => { - // fieldName could be an aggregation so it needs to be unpacked to expose raw field. - const metricField = this.getMetricFields().find(field => field.getName() === fieldName); - const realFieldName = metricField ? metricField.getESDocFieldName() : fieldName; - if (!realFieldName) { - return []; - } - + getValueSuggestions = async (field, query) => { try { const indexPattern = await this.getIndexPattern(); - const field = indexPattern.fields.getByName(realFieldName); return await autocompleteService.getValueSuggestions({ indexPattern, - field, + field: indexPattern.fields.getByName(field.getRootName()), query, }); } catch (error) { console.warn( - `Unable to fetch suggestions for field: ${fieldName}, query: ${query}, error: ${error.message}` + `Unable to fetch suggestions for field: ${field.getRootName()}, query: ${query}, error: ${ + error.message + }` ); return []; } diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_term_source.js b/x-pack/legacy/plugins/maps/public/layers/sources/es_term_source.js index 9cc2919404a94..30f60f543d38d 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/es_term_source.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_term_source.js @@ -67,7 +67,7 @@ export class ESTermSource extends AbstractESAggSource { return this._descriptor.whereQuery; } - formatMetricKey(aggType, fieldName) { + getAggKey(aggType, fieldName) { const metricKey = aggType !== AGG_TYPE.COUNT ? `${aggType}${AGG_DELIMITER}${fieldName}` : aggType; return `${FIELD_NAME_PREFIX}${metricKey}${GROUP_BY_DELIMITER}${ @@ -75,21 +75,13 @@ export class ESTermSource extends AbstractESAggSource { }.${this._termField.getName()}`; } - formatMetricLabel(type, fieldName) { - switch (type) { - case AGG_TYPE.COUNT: - return i18n.translate('xpack.maps.source.esJoin.countLabel', { + getAggLabel(aggType, fieldName) { + return aggType === AGG_TYPE.COUNT + ? i18n.translate('xpack.maps.source.esJoin.countLabel', { defaultMessage: `Count of {indexPatternTitle}`, values: { indexPatternTitle: this._descriptor.indexPatternTitle }, - }); - case AGG_TYPE.TERMS: - return i18n.translate('xpack.maps.source.esJoin.topTermLabel', { - defaultMessage: `Top {fieldName}`, - values: { fieldName }, - }); - default: - return `${type} ${fieldName}`; - } + }) + : super.getAggLabel(aggType, fieldName); } async getPropertiesMap(searchFilters, leftSourceName, leftFieldName, registerCancelCallback) { @@ -116,7 +108,7 @@ export class ESTermSource extends AbstractESAggSource { requestDescription: this._getRequestDescription(leftSourceName, leftFieldName), }); - const countPropertyName = this.formatMetricKey(AGG_TYPE.COUNT); + const countPropertyName = this.getAggKey(AGG_TYPE.COUNT); return { propertiesMap: extractPropertiesMap(rawEsData, countPropertyName), }; diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_term_source.test.js b/x-pack/legacy/plugins/maps/public/layers/sources/es_term_source.test.js index 39cc301d458cb..d6f9f6d2911e9 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/es_term_source.test.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_term_source.test.js @@ -54,7 +54,7 @@ describe('getMetricFields', () => { expect(metrics.length).toBe(2); expect(metrics[0].getAggType()).toEqual('sum'); - expect(metrics[0].getESDocFieldName()).toEqual(sumFieldName); + expect(metrics[0].getRootName()).toEqual(sumFieldName); expect(metrics[0].getName()).toEqual( '__kbnjoin__sum_of_myFieldGettingSummed_groupby_myIndex.myTermField' ); diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/source.js b/x-pack/legacy/plugins/maps/public/layers/sources/source.js index 3c6ddb74bedeb..4fef52e731f9b 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/source.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/source.js @@ -132,7 +132,7 @@ export class AbstractSource { } // Returns function used to format value - async getFieldFormatter(/* fieldName */) { + async createFieldFormatter(/* field */) { return null; } @@ -140,7 +140,7 @@ export class AbstractSource { throw new Error(`Source#loadStylePropsMeta not implemented`); } - async getValueSuggestions(/* fieldName, query */) { + async getValueSuggestions(/* field, query */) { return []; } } diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_color_property.test.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_color_property.test.js index 6b08fc2a105c3..8648b073a7b79 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_color_property.test.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_color_property.test.js @@ -25,6 +25,9 @@ const mockField = { getName() { return 'foobar'; }, + getRootName() { + return 'foobar'; + }, supportsFieldMeta() { return true; }, diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_style_property.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_style_property.js index af78c4c0e461e..e40c82e6276c7 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_style_property.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_style_property.js @@ -13,7 +13,6 @@ import React from 'react'; import { OrdinalLegend } from './components/ordinal_legend'; import { CategoricalLegend } from './components/categorical_legend'; import { OrdinalFieldMetaOptionsPopover } from '../components/ordinal_field_meta_options_popover'; -import { ESAggMetricField } from '../../../fields/es_agg_field'; export class DynamicStyleProperty extends AbstractStyleProperty { static type = STYLE_TYPE.DYNAMIC; @@ -26,9 +25,9 @@ export class DynamicStyleProperty extends AbstractStyleProperty { } getValueSuggestions = query => { - const fieldName = this.getFieldName(); + const field = this.getField(); const fieldSource = this.getFieldSource(); - return fieldSource && fieldName ? fieldSource.getValueSuggestions(fieldName, query) : []; + return fieldSource && field ? fieldSource.getValueSuggestions(field, query) : []; }; getFieldMeta() { @@ -185,11 +184,7 @@ export class DynamicStyleProperty extends AbstractStyleProperty { } _pluckOrdinalStyleMetaFromFieldMetaData(fieldMetaData) { - const realFieldName = - this._field instanceof ESAggMetricField - ? this._field.getESDocFieldName() - : this._field.getName(); - const stats = fieldMetaData[realFieldName]; + const stats = fieldMetaData[this._field.getRootName()]; if (!stats) { return null; } @@ -209,15 +204,12 @@ export class DynamicStyleProperty extends AbstractStyleProperty { } _pluckCategoricalStyleMetaFromFieldMetaData(fieldMetaData) { - const realFieldName = - this._field instanceof ESAggMetricField - ? this._field.getESDocFieldName() - : this._field.getName(); - if (!fieldMetaData[realFieldName] || !fieldMetaData[realFieldName].buckets) { + const rootFieldName = this._field.getRootName(); + if (!fieldMetaData[rootFieldName] || !fieldMetaData[rootFieldName].buckets) { return null; } - const ordered = fieldMetaData[realFieldName].buckets.map(bucket => { + const ordered = fieldMetaData[rootFieldName].buckets.map(bucket => { return { key: bucket.key, count: bucket.doc_count, diff --git a/x-pack/legacy/plugins/maps/public/layers/tooltips/es_aggmetric_tooltip_property.js b/x-pack/legacy/plugins/maps/public/layers/tooltips/es_aggmetric_tooltip_property.js index 229c84fe234bd..ea000a78331eb 100644 --- a/x-pack/legacy/plugins/maps/public/layers/tooltips/es_aggmetric_tooltip_property.js +++ b/x-pack/legacy/plugins/maps/public/layers/tooltips/es_aggmetric_tooltip_property.js @@ -27,9 +27,7 @@ export class ESAggMetricTooltipProperty extends ESTooltipProperty { ) { return this._rawValue; } - const indexPatternField = this._indexPattern.fields.getByName( - this._metricField.getESDocFieldName() - ); + const indexPatternField = this._indexPattern.fields.getByName(this._metricField.getRootName()); if (!indexPatternField) { return this._rawValue; } 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 e1a30c8aef1d3..c515feecc1551 100644 --- a/x-pack/legacy/plugins/maps/public/layers/vector_layer.js +++ b/x-pack/legacy/plugins/maps/public/layers/vector_layer.js @@ -561,10 +561,13 @@ export class VectorLayer extends AbstractLayer { startLoading(dataRequestId, requestToken, nextMeta); const formatters = {}; - const promises = fields.map(async field => { - const fieldName = field.getName(); - formatters[fieldName] = await source.getFieldFormatter(fieldName); - }); + const promises = fields + .filter(field => { + return field.canValueBeFormatted(); + }) + .map(async field => { + formatters[field.getName()] = await source.createFieldFormatter(field); + }); await Promise.all(promises); stopLoading(dataRequestId, requestToken, formatters, nextMeta); diff --git a/x-pack/legacy/plugins/ml/common/constants/app.ts b/x-pack/legacy/plugins/ml/common/constants/app.ts index 140a709b0c42b..bbec35a17faa5 100644 --- a/x-pack/legacy/plugins/ml/common/constants/app.ts +++ b/x-pack/legacy/plugins/ml/common/constants/app.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export const API_BASE_PATH = '/api/transform/'; +export const PLUGIN_ID = 'ml'; diff --git a/x-pack/legacy/plugins/ml/common/constants/license.ts b/x-pack/legacy/plugins/ml/common/constants/license.ts index 2027e2c8b1865..183844c9ef980 100644 --- a/x-pack/legacy/plugins/ml/common/constants/license.ts +++ b/x-pack/legacy/plugins/ml/common/constants/license.ts @@ -8,3 +8,5 @@ export enum LICENSE_TYPE { BASIC, FULL, // >= platinum } + +export const VALID_FULL_LICENSE_MODES = ['platinum', 'enterprise', 'trial']; diff --git a/x-pack/legacy/plugins/ml/index.ts b/x-pack/legacy/plugins/ml/index.ts index 09f1b9ccedce4..47df7c8c3e5e6 100755 --- a/x-pack/legacy/plugins/ml/index.ts +++ b/x-pack/legacy/plugins/ml/index.ts @@ -6,23 +6,13 @@ import { resolve } from 'path'; import { i18n } from '@kbn/i18n'; -import KbnServer, { Server } from 'src/legacy/server/kbn_server'; -import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; -import { plugin } from './server/new_platform'; -import { CloudSetup } from '../../../plugins/cloud/server'; +import { Server } from 'src/legacy/server/kbn_server'; import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/utils'; -import { - MlInitializerContext, - MlCoreSetup, - MlHttpServiceSetup, -} from './server/new_platform/plugin'; +// @ts-ignore: could not find declaration file for module +import { mirrorPluginStatus } from '../../server/lib/mirror_plugin_status'; // @ts-ignore: could not find declaration file for module import mappings from './mappings'; -interface MlServer extends Server { - addAppLinksToSampleDataset: () => {}; -} - export const ml = (kibana: any) => { return new kibana.Plugin({ require: ['kibana', 'elasticsearch', 'xpack_main'], @@ -60,43 +50,8 @@ export const ml = (kibana: any) => { }, }, - async init(server: MlServer) { - const kbnServer = (server as unknown) as KbnServer; - - const initializerContext = ({ - legacyConfig: server.config(), - logger: { - get(...contextParts: string[]) { - return kbnServer.newPlatform.coreContext.logger.get('plugins', 'ml', ...contextParts); - }, - }, - } as unknown) as MlInitializerContext; - - const mlHttpService: MlHttpServiceSetup = { - ...kbnServer.newPlatform.setup.core.http, - route: server.route.bind(server), - }; - - const core: MlCoreSetup = { - injectUiAppVars: server.injectUiAppVars, - http: mlHttpService, - savedObjects: server.savedObjects, - coreSavedObjects: kbnServer.newPlatform.start.core.savedObjects, - elasticsearch: kbnServer.newPlatform.setup.core.elasticsearch, - }; - const { usageCollection, cloud, home } = kbnServer.newPlatform.setup.plugins; - const plugins = { - elasticsearch: server.plugins.elasticsearch, // legacy - security: server.newPlatform.setup.plugins.security, - xpackMain: server.plugins.xpack_main, - spaces: server.plugins.spaces, - home, - usageCollection: usageCollection as UsageCollectionSetup, - cloud: cloud as CloudSetup, - ml: this, - }; - - plugin(initializerContext).setup(core, plugins); + async init(server: Server) { + mirrorPluginStatus(server.plugins.xpack_main, this); }, }); }; diff --git a/x-pack/legacy/plugins/ml/kibana.json b/x-pack/legacy/plugins/ml/kibana.json deleted file mode 100644 index f36b484818690..0000000000000 --- a/x-pack/legacy/plugins/ml/kibana.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "id": "ml", - "version": "0.0.1", - "kibanaVersion": "kibana", - "configPath": ["ml"], - "server": true, - "ui": true -} diff --git a/x-pack/legacy/plugins/ml/public/application/license/check_license.tsx b/x-pack/legacy/plugins/ml/public/application/license/check_license.tsx index 96e6aab377962..4af753ddb4d1f 100644 --- a/x-pack/legacy/plugins/ml/public/application/license/check_license.tsx +++ b/x-pack/legacy/plugins/ml/public/application/license/check_license.tsx @@ -82,9 +82,16 @@ function setLicenseExpired(features: any) { } } } - +// Temporary hack for cutting over server to NP function getFeatures() { - return xpackInfo.get('features.ml'); + return { + isAvailable: true, + showLinks: true, + enableLinks: true, + licenseType: 1, + hasExpired: false, + }; + // return xpackInfo.get('features.ml'); } function redirectToKibana() { diff --git a/x-pack/legacy/plugins/ml/server/client/call_with_internal_user_factory.d.ts b/x-pack/legacy/plugins/ml/server/client/call_with_internal_user_factory.d.ts deleted file mode 100644 index bf2e656afff12..0000000000000 --- a/x-pack/legacy/plugins/ml/server/client/call_with_internal_user_factory.d.ts +++ /dev/null @@ -1,9 +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 { ElasticsearchPlugin } from 'src/legacy/core_plugins/elasticsearch'; - -export function callWithInternalUserFactory(elasticsearchPlugin: ElasticsearchPlugin): any; diff --git a/x-pack/legacy/plugins/ml/server/client/call_with_internal_user_factory.js b/x-pack/legacy/plugins/ml/server/client/call_with_internal_user_factory.js deleted file mode 100644 index 2e5431bdd6ce2..0000000000000 --- a/x-pack/legacy/plugins/ml/server/client/call_with_internal_user_factory.js +++ /dev/null @@ -1,18 +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 { once } from 'lodash'; - -const _callWithInternalUser = once(elasticsearchPlugin => { - const { callWithInternalUser } = elasticsearchPlugin.getCluster('admin'); - return callWithInternalUser; -}); - -export const callWithInternalUserFactory = elasticsearchPlugin => { - return (...args) => { - return _callWithInternalUser(elasticsearchPlugin)(...args); - }; -}; diff --git a/x-pack/legacy/plugins/ml/server/client/call_with_internal_user_factory.test.ts b/x-pack/legacy/plugins/ml/server/client/call_with_internal_user_factory.test.ts deleted file mode 100644 index be016cc13ed0f..0000000000000 --- a/x-pack/legacy/plugins/ml/server/client/call_with_internal_user_factory.test.ts +++ /dev/null @@ -1,28 +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 { callWithInternalUserFactory } from './call_with_internal_user_factory'; - -describe('call_with_internal_user_factory', () => { - describe('callWithInternalUserFactory', () => { - let elasticsearchPlugin: any; - let callWithInternalUser: any; - - beforeEach(() => { - callWithInternalUser = jest.fn(); - elasticsearchPlugin = { - getCluster: jest.fn(() => ({ callWithInternalUser })), - }; - }); - - it('should use internal user "admin"', () => { - const callWithInternalUserInstance = callWithInternalUserFactory(elasticsearchPlugin); - callWithInternalUserInstance(); - - expect(elasticsearchPlugin.getCluster).toHaveBeenCalledWith('admin'); - }); - }); -}); diff --git a/x-pack/legacy/plugins/ml/server/client/call_with_request_factory.js b/x-pack/legacy/plugins/ml/server/client/call_with_request_factory.js deleted file mode 100644 index b39a58b317500..0000000000000 --- a/x-pack/legacy/plugins/ml/server/client/call_with_request_factory.js +++ /dev/null @@ -1,21 +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 { once } from 'lodash'; -import { elasticsearchJsPlugin } from './elasticsearch_ml'; - -const callWithRequest = once(elasticsearchPlugin => { - const config = { plugins: [elasticsearchJsPlugin] }; - const cluster = elasticsearchPlugin.createCluster('ml', config); - - return cluster.callWithRequest; -}); - -export const callWithRequestFactory = (elasticsearchPlugin, request) => { - return (...args) => { - return callWithRequest(elasticsearchPlugin)(request, ...args); - }; -}; diff --git a/x-pack/legacy/plugins/ml/server/lib/__tests__/security_utils.js b/x-pack/legacy/plugins/ml/server/lib/__tests__/security_utils.js deleted file mode 100644 index 6e0181f49072e..0000000000000 --- a/x-pack/legacy/plugins/ml/server/lib/__tests__/security_utils.js +++ /dev/null @@ -1,35 +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 expect from '@kbn/expect'; -import { isSecurityDisabled } from '../security_utils'; - -describe('ML - security utils', () => { - function mockXpackMainPluginFactory(isAvailable = true, isEnabled = true) { - return { - info: { - isAvailable: () => isAvailable, - feature: () => ({ - isEnabled: () => isEnabled, - }), - }, - }; - } - - describe('isSecurityDisabled', () => { - it('returns not disabled for given mock server object #1', () => { - expect(isSecurityDisabled(mockXpackMainPluginFactory())).to.be(false); - }); - - it('returns not disabled for given mock server object #2', () => { - expect(isSecurityDisabled(mockXpackMainPluginFactory(false))).to.be(false); - }); - - it('returns disabled for given mock server object #3', () => { - expect(isSecurityDisabled(mockXpackMainPluginFactory(true, false))).to.be(true); - }); - }); -}); diff --git a/x-pack/legacy/plugins/ml/server/lib/check_annotations/index.d.ts b/x-pack/legacy/plugins/ml/server/lib/check_annotations/index.d.ts deleted file mode 100644 index dbd08eacd3ca2..0000000000000 --- a/x-pack/legacy/plugins/ml/server/lib/check_annotations/index.d.ts +++ /dev/null @@ -1,11 +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 { IScopedClusterClient } from 'src/core/server'; - -export function isAnnotationsFeatureAvailable( - callAsCurrentUser: IScopedClusterClient['callAsCurrentUser'] -): boolean; diff --git a/x-pack/legacy/plugins/ml/server/lib/security_utils.js b/x-pack/legacy/plugins/ml/server/lib/security_utils.js deleted file mode 100644 index 27109e645b185..0000000000000 --- a/x-pack/legacy/plugins/ml/server/lib/security_utils.js +++ /dev/null @@ -1,19 +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. - */ - -/* - * Contains utility functions related to x-pack security. - */ - -export function isSecurityDisabled(xpackMainPlugin) { - const xpackInfo = xpackMainPlugin && xpackMainPlugin.info; - // we assume that `xpack.isAvailable()` always returns `true` because we're inside x-pack - // if for whatever reason it returns `false`, `isSecurityDisabled()` would also return `false` - // which would result in follow-up behavior assuming security is enabled. This is intentional, - // because it results in more defensive behavior. - const securityInfo = xpackInfo && xpackInfo.isAvailable() && xpackInfo.feature('security'); - return securityInfo && securityInfo.isEnabled() === false; -} diff --git a/x-pack/legacy/plugins/ml/server/new_platform/plugin.ts b/x-pack/legacy/plugins/ml/server/new_platform/plugin.ts deleted file mode 100644 index 43c276ac63a13..0000000000000 --- a/x-pack/legacy/plugins/ml/server/new_platform/plugin.ts +++ /dev/null @@ -1,238 +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 { i18n } from '@kbn/i18n'; -import { ServerRoute } from 'hapi'; -import { KibanaConfig, SavedObjectsLegacyService } from 'src/legacy/server/kbn_server'; -import { - Logger, - PluginInitializerContext, - CoreSetup, - IRouter, - IScopedClusterClient, - SavedObjectsServiceStart, -} from 'src/core/server'; -import { ElasticsearchPlugin } from 'src/legacy/core_plugins/elasticsearch'; -import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; -import { ElasticsearchServiceSetup } from 'src/core/server'; -import { CloudSetup } from '../../../../../plugins/cloud/server'; -import { XPackMainPlugin } from '../../../xpack_main/server/xpack_main'; -import { addLinksToSampleDatasets } from '../lib/sample_data_sets'; -import { checkLicense } from '../lib/check_license'; -// @ts-ignore: could not find declaration file for module -import { mirrorPluginStatus } from '../../../../server/lib/mirror_plugin_status'; -import { LICENSE_TYPE } from '../../common/constants/license'; -import { annotationRoutes } from '../routes/annotations'; -import { jobRoutes } from '../routes/anomaly_detectors'; -import { dataFeedRoutes } from '../routes/datafeeds'; -import { indicesRoutes } from '../routes/indices'; -import { jobValidationRoutes } from '../routes/job_validation'; -import { makeMlUsageCollector } from '../lib/ml_telemetry'; -import { notificationRoutes } from '../routes/notification_settings'; -import { systemRoutes } from '../routes/system'; -import { dataFrameAnalyticsRoutes } from '../routes/data_frame_analytics'; -import { dataRecognizer } from '../routes/modules'; -import { dataVisualizerRoutes } from '../routes/data_visualizer'; -import { calendars } from '../routes/calendars'; -// @ts-ignore: could not find declaration file for module -import { fieldsService } from '../routes/fields_service'; -import { filtersRoutes } from '../routes/filters'; -import { resultsServiceRoutes } from '../routes/results_service'; -import { jobServiceRoutes } from '../routes/job_service'; -import { jobAuditMessagesRoutes } from '../routes/job_audit_messages'; -import { fileDataVisualizerRoutes } from '../routes/file_data_visualizer'; -import { initMlServerLog, LogInitialization } from '../client/log'; -import { HomeServerPluginSetup } from '../../../../../../src/plugins/home/server'; -// @ts-ignore: could not find declaration file for module -import { elasticsearchJsPlugin } from '../client/elasticsearch_ml'; - -export const PLUGIN_ID = 'ml'; - -type CoreHttpSetup = CoreSetup['http']; -export interface MlHttpServiceSetup extends CoreHttpSetup { - route(route: ServerRoute | ServerRoute[]): void; -} - -export interface MlXpackMainPlugin extends XPackMainPlugin { - status?: any; -} - -export interface MlCoreSetup { - injectUiAppVars: (id: string, callback: () => {}) => any; - http: MlHttpServiceSetup; - savedObjects: SavedObjectsLegacyService; - coreSavedObjects: SavedObjectsServiceStart; - elasticsearch: ElasticsearchServiceSetup; -} -export interface MlInitializerContext extends PluginInitializerContext { - legacyConfig: KibanaConfig; - log: Logger; -} -export interface PluginsSetup { - elasticsearch: ElasticsearchPlugin; - xpackMain: MlXpackMainPlugin; - security: any; - spaces: any; - usageCollection?: UsageCollectionSetup; - cloud?: CloudSetup; - home?: HomeServerPluginSetup; - // TODO: this is temporary for `mirrorPluginStatus` - ml: any; -} - -export interface RouteInitialization { - commonRouteConfig: any; - config?: any; - elasticsearchPlugin: ElasticsearchPlugin; - elasticsearchService: ElasticsearchServiceSetup; - route(route: ServerRoute | ServerRoute[]): void; - router: IRouter; - xpackMainPlugin: MlXpackMainPlugin; - savedObjects?: SavedObjectsServiceStart; - spacesPlugin: any; - securityPlugin: any; - cloud?: CloudSetup; -} - -declare module 'kibana/server' { - interface RequestHandlerContext { - ml?: { - mlClient: IScopedClusterClient; - }; - } -} - -export class Plugin { - private readonly pluginId: string = PLUGIN_ID; - private config: any; - private log: Logger; - - constructor(initializerContext: MlInitializerContext) { - this.config = initializerContext.legacyConfig; - this.log = initializerContext.logger.get(); - } - - public setup(core: MlCoreSetup, plugins: PluginsSetup) { - const xpackMainPlugin: MlXpackMainPlugin = plugins.xpackMain; - const { http, coreSavedObjects } = core; - const pluginId = this.pluginId; - - mirrorPluginStatus(xpackMainPlugin, plugins.ml); - xpackMainPlugin.status.once('green', () => { - // Register a function that is called whenever the xpack info changes, - // to re-compute the license check results for this plugin - const mlFeature = xpackMainPlugin.info.feature(pluginId); - mlFeature.registerLicenseCheckResultsGenerator(checkLicense); - - // Add links to the Kibana sample data sets if ml is enabled - // and there is a full license (trial or platinum). - if (mlFeature.isEnabled() === true && plugins.home) { - const licenseCheckResults = mlFeature.getLicenseCheckResults(); - if (licenseCheckResults.licenseType === LICENSE_TYPE.FULL) { - addLinksToSampleDatasets({ - addAppLinksToSampleDataset: plugins.home.sampleData.addAppLinksToSampleDataset, - }); - } - } - }); - - xpackMainPlugin.registerFeature({ - id: 'ml', - name: i18n.translate('xpack.ml.featureRegistry.mlFeatureName', { - defaultMessage: 'Machine Learning', - }), - icon: 'machineLearningApp', - navLinkId: 'ml', - app: ['ml', 'kibana'], - catalogue: ['ml'], - privileges: {}, - reserved: { - privilege: { - savedObject: { - all: [], - read: [], - }, - ui: [], - }, - description: i18n.translate('xpack.ml.feature.reserved.description', { - defaultMessage: - 'To grant users access, you should also assign either the machine_learning_user or machine_learning_admin role.', - }), - }, - }); - - // Add server routes and initialize the plugin here - const commonRouteConfig = { - pre: [ - function forbidApiAccess() { - const licenseCheckResults = xpackMainPlugin.info - .feature(pluginId) - .getLicenseCheckResults(); - if (licenseCheckResults.isAvailable) { - return null; - } else { - throw Boom.forbidden(licenseCheckResults.message); - } - }, - ], - }; - - // Can access via new platform router's handler function 'context' parameter - context.ml.mlClient - const mlClient = core.elasticsearch.createClient('ml', { plugins: [elasticsearchJsPlugin] }); - http.registerRouteHandlerContext('ml', (context, request) => { - return { - mlClient: mlClient.asScoped(request), - }; - }); - - const routeInitializationDeps: RouteInitialization = { - commonRouteConfig, - route: http.route, - router: http.createRouter(), - elasticsearchPlugin: plugins.elasticsearch, - elasticsearchService: core.elasticsearch, - xpackMainPlugin: plugins.xpackMain, - spacesPlugin: plugins.spaces, - securityPlugin: plugins.security, - }; - - const extendedRouteInitializationDeps: RouteInitialization = { - ...routeInitializationDeps, - config: this.config, - savedObjects: coreSavedObjects, - spacesPlugin: plugins.spaces, - cloud: plugins.cloud, - }; - - const logInitializationDeps: LogInitialization = { - log: this.log, - }; - - annotationRoutes(routeInitializationDeps); - jobRoutes(routeInitializationDeps); - dataFeedRoutes(routeInitializationDeps); - dataFrameAnalyticsRoutes(routeInitializationDeps); - indicesRoutes(routeInitializationDeps); - jobValidationRoutes(extendedRouteInitializationDeps); - notificationRoutes(routeInitializationDeps); - systemRoutes(extendedRouteInitializationDeps); - dataRecognizer(extendedRouteInitializationDeps); - dataVisualizerRoutes(routeInitializationDeps); - calendars(routeInitializationDeps); - fieldsService(routeInitializationDeps); - filtersRoutes(routeInitializationDeps); - resultsServiceRoutes(routeInitializationDeps); - jobServiceRoutes(routeInitializationDeps); - jobAuditMessagesRoutes(routeInitializationDeps); - fileDataVisualizerRoutes(extendedRouteInitializationDeps); - - initMlServerLog(logInitializationDeps); - makeMlUsageCollector(plugins.usageCollection, coreSavedObjects); - } - - public stop() {} -} diff --git a/x-pack/legacy/plugins/monitoring/common/constants.ts b/x-pack/legacy/plugins/monitoring/common/constants.ts index 1fb6acdb915b8..9a4030f3eb214 100644 --- a/x-pack/legacy/plugins/monitoring/common/constants.ts +++ b/x-pack/legacy/plugins/monitoring/common/constants.ts @@ -141,23 +141,12 @@ export const CLUSTER_ALERTS_ADDRESS_CONFIG_KEY = 'cluster_alerts.email_notificat export const STANDALONE_CLUSTER_CLUSTER_UUID = '__standalone_cluster__'; -const INDEX_PATTERN_NEW = ',monitoring-*-7-*,monitoring-*-8-*'; -const INDEX_PATTERN_KIBANA_NEW = ',monitoring-kibana-7-*,monitoring-kibana-8-*'; -const INDEX_PATTERN_LOGSTASH_NEW = ',monitoring-logstash-7-*,monitoring-logstash-8-*'; -const INDEX_PATTERN_BEATS_NEW = ',monitoring-beats-7-*,monitoring-beats-8-*'; -const INDEX_ALERTS_NEW = ',monitoring-alerts-7,monitoring-alerts-8'; -const INDEX_PATTERN_ELASTICSEARCH_NEW = ',monitoring-es-7-*,monitoring-es-8-*'; - -export const INDEX_PATTERN = '.monitoring-*-6-*,.monitoring-*-7-*' + INDEX_PATTERN_NEW; -export const INDEX_PATTERN_KIBANA = - '.monitoring-kibana-6-*,.monitoring-kibana-7-*' + INDEX_PATTERN_KIBANA_NEW; -export const INDEX_PATTERN_LOGSTASH = - '.monitoring-logstash-6-*,.monitoring-logstash-7-*' + INDEX_PATTERN_LOGSTASH_NEW; -export const INDEX_PATTERN_BEATS = - '.monitoring-beats-6-*,.monitoring-beats-7-*' + INDEX_PATTERN_BEATS_NEW; -export const INDEX_ALERTS = '.monitoring-alerts-6,.monitoring-alerts-7' + INDEX_ALERTS_NEW; -export const INDEX_PATTERN_ELASTICSEARCH = - '.monitoring-es-6-*,.monitoring-es-7-*' + INDEX_PATTERN_ELASTICSEARCH_NEW; +export const INDEX_PATTERN = '.monitoring-*-6-*,.monitoring-*-7-*'; +export const INDEX_PATTERN_KIBANA = '.monitoring-kibana-6-*,.monitoring-kibana-7-*'; +export const INDEX_PATTERN_LOGSTASH = '.monitoring-logstash-6-*,.monitoring-logstash-7-*'; +export const INDEX_PATTERN_BEATS = '.monitoring-beats-6-*,.monitoring-beats-7-*'; +export const INDEX_ALERTS = '.monitoring-alerts-6,.monitoring-alerts-7'; +export const INDEX_PATTERN_ELASTICSEARCH = '.monitoring-es-6-*,.monitoring-es-7-*'; // This is the unique token that exists in monitoring indices collected by metricbeat export const METRICBEAT_INDEX_NAME_UNIQUE_TOKEN = '-mb-'; diff --git a/x-pack/legacy/plugins/monitoring/public/np_imports/legacy_imports.ts b/x-pack/legacy/plugins/monitoring/public/np_imports/legacy_imports.ts index ea29ac95eb03f..a2ebe8231456f 100644 --- a/x-pack/legacy/plugins/monitoring/public/np_imports/legacy_imports.ts +++ b/x-pack/legacy/plugins/monitoring/public/np_imports/legacy_imports.ts @@ -17,7 +17,6 @@ export { StateManagementConfigProvider } from 'ui/state_management/config_provid export { AppStateProvider } from 'ui/state_management/app_state'; // @ts-ignore export { EventsProvider } from 'ui/events'; -export { PersistedState } from 'ui/persisted_state'; // @ts-ignore export { KbnUrlProvider, RedirectWhenMissingProvider } from 'ui/url'; export { registerTimefilterWithGlobalStateFactory } from 'ui/timefilter/setup_router'; diff --git a/x-pack/legacy/plugins/monitoring/server/lib/setup/collection/__test__/get_collection_status.js b/x-pack/legacy/plugins/monitoring/server/lib/setup/collection/__test__/get_collection_status.test.js similarity index 85% rename from x-pack/legacy/plugins/monitoring/server/lib/setup/collection/__test__/get_collection_status.js rename to x-pack/legacy/plugins/monitoring/server/lib/setup/collection/__test__/get_collection_status.test.js index 8562bdb2b0029..75ca6434c4e7a 100644 --- a/x-pack/legacy/plugins/monitoring/server/lib/setup/collection/__test__/get_collection_status.js +++ b/x-pack/legacy/plugins/monitoring/server/lib/setup/collection/__test__/get_collection_status.test.js @@ -6,11 +6,11 @@ import expect from '@kbn/expect'; import sinon from 'sinon'; -import { getCollectionStatus } from '../'; +import { getCollectionStatus } from '..'; import { getIndexPatterns } from '../../../cluster/get_index_patterns'; const liveClusterUuid = 'a12'; -const mockReq = (searchResult = {}) => { +const mockReq = (searchResult = {}, securityEnabled = true, userHasPermissions = true) => { return { server: { newPlatform: { @@ -40,6 +40,14 @@ const mockReq = (searchResult = {}) => { }, }, plugins: { + xpack_main: { + info: { + isAvailable: () => true, + feature: () => ({ + isEnabled: () => securityEnabled, + }), + }, + }, elasticsearch: { getCluster() { return { @@ -51,6 +59,13 @@ const mockReq = (searchResult = {}) => { ) { return Promise.resolve({ cluster_uuid: liveClusterUuid }); } + if ( + type === 'transport.request' && + params && + params.path === '/_security/user/_has_privileges' + ) { + return Promise.resolve({ has_all_requested: userHasPermissions }); + } if (type === 'transport.request' && params && params.path === '/_nodes') { return Promise.resolve({ nodes: {} }); } @@ -218,19 +233,7 @@ describe('getCollectionStatus', () => { }); it('should detect products based on other indices', async () => { - const req = mockReq( - {}, - { - responses: [ - { hits: { total: { value: 1 } } }, - { hits: { total: { value: 1 } } }, - { hits: { total: { value: 1 } } }, - { hits: { total: { value: 1 } } }, - { hits: { total: { value: 1 } } }, - ], - } - ); - + const req = mockReq({ hits: { total: { value: 1 } } }); const result = await getCollectionStatus(req, getIndexPatterns(req.server), liveClusterUuid); expect(result.kibana.detected.doesExist).to.be(true); @@ -238,4 +241,16 @@ describe('getCollectionStatus', () => { expect(result.beats.detected.mightExist).to.be(true); expect(result.logstash.detected.mightExist).to.be(true); }); + + it('should work properly when security is disabled', async () => { + const req = mockReq({ hits: { total: { value: 1 } } }, false); + const result = await getCollectionStatus(req, getIndexPatterns(req.server), liveClusterUuid); + expect(result.kibana.detected.doesExist).to.be(true); + }); + + it('should not work if the user does not have the necessary permissions', async () => { + const req = mockReq({ hits: { total: { value: 1 } } }, true, false); + const result = await getCollectionStatus(req, getIndexPatterns(req.server), liveClusterUuid); + expect(result._meta.hasPermissions).to.be(false); + }); }); diff --git a/x-pack/legacy/plugins/monitoring/server/lib/setup/collection/get_collection_status.js b/x-pack/legacy/plugins/monitoring/server/lib/setup/collection/get_collection_status.js index 42d100b8af75e..0029aaa9ce8ee 100644 --- a/x-pack/legacy/plugins/monitoring/server/lib/setup/collection/get_collection_status.js +++ b/x-pack/legacy/plugins/monitoring/server/lib/setup/collection/get_collection_status.js @@ -226,16 +226,25 @@ function isBeatFromAPM(bucket) { } async function hasNecessaryPermissions(req) { - const { callWithRequest } = req.server.plugins.elasticsearch.getCluster('data'); - const response = await callWithRequest(req, 'transport.request', { - method: 'POST', - path: '/_security/user/_has_privileges', - body: { - cluster: ['monitor'], - }, - }); - // If there is some problem, assume they do not have access - return get(response, 'has_all_requested', false); + try { + const { callWithRequest } = req.server.plugins.elasticsearch.getCluster('data'); + const response = await callWithRequest(req, 'transport.request', { + method: 'POST', + path: '/_security/user/_has_privileges', + body: { + cluster: ['monitor'], + }, + }); + // If there is some problem, assume they do not have access + return get(response, 'has_all_requested', false); + } catch (err) { + if ( + err.message === 'no handler found for uri [/_security/user/_has_privileges] and method [POST]' + ) { + return true; + } + return false; + } } /** diff --git a/x-pack/legacy/plugins/siem/public/components/link_to/index.ts b/x-pack/legacy/plugins/siem/public/components/link_to/index.ts index c93b415e017bb..a1c1f78e398e3 100644 --- a/x-pack/legacy/plugins/siem/public/components/link_to/index.ts +++ b/x-pack/legacy/plugins/siem/public/components/link_to/index.ts @@ -17,6 +17,8 @@ export { getCaseDetailsUrl, getCaseUrl, getCreateCaseUrl, + getConfigureCasesUrl, RedirectToCasePage, RedirectToCreatePage, + RedirectToConfigureCasesPage, } from './redirect_to_case'; diff --git a/x-pack/legacy/plugins/siem/public/components/link_to/link_to.tsx b/x-pack/legacy/plugins/siem/public/components/link_to/link_to.tsx index c08b429dc4625..08e4d1a3494e0 100644 --- a/x-pack/legacy/plugins/siem/public/components/link_to/link_to.tsx +++ b/x-pack/legacy/plugins/siem/public/components/link_to/link_to.tsx @@ -20,7 +20,11 @@ import { RedirectToHostsPage, RedirectToHostDetailsPage } from './redirect_to_ho import { RedirectToNetworkPage } from './redirect_to_network'; import { RedirectToOverviewPage } from './redirect_to_overview'; import { RedirectToTimelinesPage } from './redirect_to_timelines'; -import { RedirectToCasePage, RedirectToCreatePage } from './redirect_to_case'; +import { + RedirectToCasePage, + RedirectToCreatePage, + RedirectToConfigureCasesPage, +} from './redirect_to_case'; import { DetectionEngineTab } from '../../pages/detection_engine/types'; interface LinkToPageProps { @@ -43,6 +47,11 @@ export const LinkToPage = React.memo(({ match }) => ( component={RedirectToCreatePage} path={`${match.url}/:pageName(${SiemPageName.case})/create`} /> + ; +export const RedirectToConfigureCasesPage = () => ( + +); const baseCaseUrl = `#/link-to/${SiemPageName.case}`; export const getCaseUrl = () => baseCaseUrl; export const getCaseDetailsUrl = (detailName: string) => `${baseCaseUrl}/${detailName}`; export const getCreateCaseUrl = () => `${baseCaseUrl}/create`; +export const getConfigureCasesUrl = () => `${baseCaseUrl}/configure`; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/__snapshots__/zeek_details.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/__snapshots__/zeek_details.test.tsx.snap index c883983f8cf01..0a60c8facff9c 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/__snapshots__/zeek_details.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/__snapshots__/zeek_details.test.tsx.snap @@ -498,3974 +498,3 @@ exports[`ZeekDetails rendering it renders the default ZeekDetails 1`] = ` timelineId="test" /> `; - -exports[`ZeekDetails rendering it returns zeek.files if the data does contain zeek.files data 1`] = ` -.c3, -.c3::before, -.c3::after { - -webkit-transition: background 150ms ease, color 150ms ease; - transition: background 150ms ease, color 150ms ease; -} - -.c3 { - border-radius: 2px; - padding: 0 4px 0 8px; - position: relative; - z-index: 0 !important; -} - -.c3::before { - background-image: linear-gradient( 135deg, #535966 25%, transparent 25% ), linear-gradient( -135deg, #535966 25%, transparent 25% ), linear-gradient( 135deg, transparent 75%, #535966 75% ), linear-gradient( -135deg, transparent 75%, #535966 75% ); - background-position: 0 0,1px 0,1px -1px,0px 1px; - background-size: 2px 2px; - bottom: 2px; - content: ''; - display: block; - left: 2px; - position: absolute; - top: 2px; - width: 4px; -} - -.c3:hover, -.c3:hover .euiBadge, -.c3:hover .euiBadge__text { - cursor: move; - cursor: -webkit-grab; - cursor: -moz-grab; - cursor: grab; -} - -.event-column-view:hover .c3, -tr:hover .c3 { - background-color: #343741; -} - -.event-column-view:hover .c3::before, -tr:hover .c3::before { - background-image: linear-gradient( 135deg, #98a2b3 25%, transparent 25% ), linear-gradient( -135deg, #98a2b3 25%, transparent 25% ), linear-gradient( 135deg, transparent 75%, #98a2b3 75% ), linear-gradient( -135deg, transparent 75%, #98a2b3 75% ); -} - -.c3:hover, -.c3:focus, -.event-column-view:hover .c3:hover, -.event-column-view:focus .c3:focus, -tr:hover .c3:hover, -tr:hover .c3:focus { - background-color: #1ba9f5; -} - -.c3:hover, -.c3:focus, -.event-column-view:hover .c3:hover, -.event-column-view:focus .c3:focus, -tr:hover .c3:hover, -tr:hover .c3:focus, -.c3:hover a, -.c3:focus a, -.event-column-view:hover .c3:hover a, -.event-column-view:focus .c3:focus a, -tr:hover .c3:hover a, -tr:hover .c3:focus a, -.c3:hover a:hover, -.c3:focus a:hover, -.event-column-view:hover .c3:hover a:hover, -.event-column-view:focus .c3:focus a:hover, -tr:hover .c3:hover a:hover, -tr:hover .c3:focus a:hover { - color: #1d1e24; -} - -.c3:hover::before, -.c3:focus::before, -.event-column-view:hover .c3:hover::before, -.event-column-view:focus .c3:focus::before, -tr:hover .c3:hover::before, -tr:hover .c3:focus::before { - background-image: linear-gradient( 135deg, #1d1e24 25%, transparent 25% ), linear-gradient( -135deg, #1d1e24 25%, transparent 25% ), linear-gradient( 135deg, transparent 75%, #1d1e24 75% ), linear-gradient( -135deg, transparent 75%, #1d1e24 75% ); -} - -.c2 { - display: inline-block; - max-width: 100%; -} - -.c2 [data-rbd-placeholder-context-id] { - display: none !important; -} - -.c4 > span.euiToolTipAnchor { - display: block; -} - -.c8 { - margin: 0 2px; -} - -.c7 { - margin-top: 3px; -} - -.c6 { - margin-right: 10px; -} - -.c1 { - margin-left: 3px; -} - -.c5 { - margin-left: 6px; -} - -.c0 { - margin: 5px 0; -} - - - - - - - - - - - - - - - -

-
- - -
- - - -
- - - -
- - -
- - - - - - -
- - - - - - - - - - - Cu0n232QMyvNtzb75j - - - - - - - - - - - - - - - -
-
-
-
-
-
-
- -
-
-
-
-
-
-
-
-
-
-
- - - -
- - - -
- - -
- - - - - - -
- - - - - - - - - - - files - - - - - - - - - - - - - - - -
-
-
-
-
-
-
- -
-
-
-
-
-
-
-
-
-
-
- - - -
- - - -
- - -
- - - - - - -
- - - - - - - - - - - sha1: fa5195a... - - - - - - - - - - - - - - - -
-
-
-
-
-
-
- -
-
-
-
-
-
-
-
-
-
-
- - - -
- - - -
- - -
- - - - - - -
- - - - - - - - - - - md5: f7653f1... - - - - - - - - - - - - - - - -
-
-
-
-
-
-
- -
-
-
-
-
-
-
-
-
-
-
- - - - - - - - - - -
-
- -
- - - - - - - - -
-
- -
-
-
-
-
-
-
-
- -
- - - - -
- -
- - -
- - -
- - -
- - -
- - - - -
- - -
- - -
- - - -
- - -
- -
- - -
- - -
- - - -
- - -
- -
- -
-
- - - -
- - - - -
- -
-
-
-
- -
- - -
-
- -
-
-
-
- -
-
- -
- - -
- - -
- -
-
- - -
-
- - - - - - - - - - - - - - -`; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_details.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_details.test.tsx index db51ade6df4c5..b45e4c41762bc 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_details.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_details.test.tsx @@ -113,7 +113,6 @@ describe('ZeekDetails', () => { /> ); - expect(wrapper).toMatchSnapshot(); expect(wrapper.text()).toEqual('Cu0n232QMyvNtzb75jfilessha1: fa5195a...md5: f7653f1...'); }); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/case.tsx b/x-pack/legacy/plugins/siem/public/pages/case/case.tsx index 1206ec950deed..15a6d076f1009 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/case.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/case.tsx @@ -6,30 +6,29 @@ import React from 'react'; -import { EuiButton, EuiFlexGroup } from '@elastic/eui'; -import { HeaderPage } from '../../components/header_page'; +import { EuiButton, EuiButtonIcon, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { CaseHeaderPage } from './components/case_header_page'; import { WrapperPage } from '../../components/wrapper_page'; import { AllCases } from './components/all_cases'; import { SpyRoute } from '../../utils/route/spy_routes'; import * as i18n from './translations'; -import { getCreateCaseUrl } from '../../components/link_to'; - -const badgeOptions = { - beta: true, - text: i18n.PAGE_BADGE_LABEL, - tooltip: i18n.PAGE_BADGE_TOOLTIP, -}; +import { getCreateCaseUrl, getConfigureCasesUrl } from '../../components/link_to'; export const CasesPage = React.memo(() => ( <> - + - - {i18n.CREATE_TITLE} - + + + {i18n.CREATE_TITLE} + + + + + - + diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/case_header_page/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/case_header_page/index.tsx new file mode 100644 index 0000000000000..ae2664ca6e839 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/case_header_page/index.tsx @@ -0,0 +1,22 @@ +/* + * 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 { HeaderPage, HeaderPageProps } from '../../../../components/header_page'; +import * as i18n from './translations'; + +const CaseHeaderPageComponent: React.FC = props => ; + +CaseHeaderPageComponent.defaultProps = { + badgeOptions: { + beta: true, + text: i18n.PAGE_BADGE_LABEL, + tooltip: i18n.PAGE_BADGE_TOOLTIP, + }, +}; + +export const CaseHeaderPage = React.memo(CaseHeaderPageComponent); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/case_header_page/translations.ts b/x-pack/legacy/plugins/siem/public/pages/case/components/case_header_page/translations.ts new file mode 100644 index 0000000000000..ceb70b7df3bb1 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/case_header_page/translations.ts @@ -0,0 +1,16 @@ +/* + * 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 { i18n } from '@kbn/i18n'; + +export const PAGE_BADGE_LABEL = i18n.translate('xpack.siem.case.caseView.pageBadgeLabel', { + defaultMessage: 'Beta', +}); + +export const PAGE_BADGE_TOOLTIP = i18n.translate('xpack.siem.case.caseView.pageBadgeTooltip', { + defaultMessage: + 'Cases is still in beta. Please help us improve by reporting issues or bugs in the Kibana repo.', +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx index 6a6d5660de067..bf59487f46fc9 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx @@ -30,6 +30,7 @@ import { UserActionTree } from '../user_action_tree'; import { UserList } from '../user_list'; import { useUpdateCase } from '../../../../containers/case/use_update_case'; import { WrapperPage } from '../../../../components/wrapper_page'; +import { WhitePageWrapper } from '../wrappers'; import { getTypedPayload } from '../../../../containers/case/utils'; interface Props { @@ -53,14 +54,6 @@ const MyEuiFlexGroup = styled(EuiFlexGroup)` height: 100%; `; -const BackgroundWrapper = styled.div` - ${({ theme }) => css` - background-color: ${theme.eui.euiColorEmptyShade}; - border-top: ${theme.eui.euiBorderThin}; - height: 100%; - `} -`; - export interface CaseProps { caseId: string; initialData: Case; @@ -206,7 +199,7 @@ export const CaseComponent = React.memo(({ caseId, initialData }) => - + @@ -231,7 +224,7 @@ export const CaseComponent = React.memo(({ caseId, initialData }) => - + ); }); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/connectors.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/connectors.tsx new file mode 100644 index 0000000000000..561464e44c703 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/connectors.tsx @@ -0,0 +1,52 @@ +/* + * 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 { + EuiDescribedFormGroup, + EuiFormRow, + EuiFlexGroup, + EuiFlexItem, + EuiLink, +} from '@elastic/eui'; + +import styled from 'styled-components'; + +import { ConnectorsDropdown } from './connectors_dropdown'; +import * as i18n from './translations'; + +const EuiFormRowExtended = styled(EuiFormRow)` + .euiFormRow__labelWrapper { + .euiFormRow__label { + width: 100%; + } + } +`; + +const ConnectorsComponent: React.FC = () => { + const dropDownLabel = ( + + {i18n.INCIDENT_MANAGEMENT_SYSTEM_LABEL} + + {i18n.ADD_NEW_CONNECTOR} + + + ); + + return ( + {i18n.INCIDENT_MANAGEMENT_SYSTEM_TITLE}} + description={i18n.INCIDENT_MANAGEMENT_SYSTEM_DESC} + > + + + + + ); +}; + +export const Connectors = React.memo(ConnectorsComponent); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/connectors_dropdown/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/connectors_dropdown/index.tsx new file mode 100644 index 0000000000000..c00baa04d78a0 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/connectors_dropdown/index.tsx @@ -0,0 +1,56 @@ +/* + * 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, { useState, useCallback } from 'react'; +import { EuiSuperSelect, EuiIcon, EuiSuperSelectOption } from '@elastic/eui'; +import styled from 'styled-components'; + +import * as i18n from '../translations'; + +const ICON_SIZE = 'm'; + +const EuiIconExtended = styled(EuiIcon)` + margin-right: 13px; +`; + +const connectors: Array> = [ + { + value: 'no-connector', + inputDisplay: ( + <> + + {i18n.NO_CONNECTOR} + + ), + 'data-test-subj': 'no-connector', + }, + { + value: 'servicenow-connector', + inputDisplay: ( + <> + + {'My ServiceNow connector'} + + ), + 'data-test-subj': 'servicenow-connector', + }, +]; + +const ConnectorsDropdownComponent: React.FC = () => { + const [selectedConnector, selectConnector] = useState(connectors[0].value); + const onChange = useCallback(connector => selectConnector(connector), [selectedConnector]); + + return ( + + ); +}; + +export const ConnectorsDropdown = React.memo(ConnectorsDropdownComponent); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/translations.ts b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/translations.ts new file mode 100644 index 0000000000000..54d256b143f60 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/translations.ts @@ -0,0 +1,37 @@ +/* + * 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 { i18n } from '@kbn/i18n'; + +export const INCIDENT_MANAGEMENT_SYSTEM_TITLE = i18n.translate( + 'xpack.siem.case.configureCases.incidentManagementSystemTitle', + { + defaultMessage: 'Connect to third-party incident management system', + } +); + +export const INCIDENT_MANAGEMENT_SYSTEM_DESC = i18n.translate( + 'xpack.siem.case.configureCases.incidentManagementSystemDesc', + { + defaultMessage: + 'You may optionally connect SIEM cases to a third-party incident management system of your choosing. This will allow you to push case data as an incident in your chosen third-party system.', + } +); + +export const INCIDENT_MANAGEMENT_SYSTEM_LABEL = i18n.translate( + 'xpack.siem.case.configureCases.incidentManagementSystemLabel', + { + defaultMessage: 'Incident management system', + } +); + +export const NO_CONNECTOR = i18n.translate('xpack.siem.case.configureCases.noConnector', { + defaultMessage: 'No connector selected', +}); + +export const ADD_NEW_CONNECTOR = i18n.translate('xpack.siem.case.configureCases.addNewConnector', { + defaultMessage: 'Add new connector option', +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/wrappers/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/wrappers/index.tsx new file mode 100644 index 0000000000000..772d78f948b79 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/wrappers/index.tsx @@ -0,0 +1,22 @@ +/* + * 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 styled, { css } from 'styled-components'; + +export const WhitePageWrapper = styled.div` + ${({ theme }) => css` + background-color: ${theme.eui.euiColorEmptyShade}; + border-top: ${theme.eui.euiBorderThin}; + height: 100%; + min-height: 100vh; + `} +`; + +export const SectionWrapper = styled.div` + box-sizing: content-box; + margin: 0 auto; + max-width: 1175px; +`; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/configure_cases.tsx b/x-pack/legacy/plugins/siem/public/pages/case/configure_cases.tsx new file mode 100644 index 0000000000000..018f9dc9ade52 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/configure_cases.tsx @@ -0,0 +1,54 @@ +/* + * 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 styled, { css } from 'styled-components'; + +import { WrapperPage } from '../../components/wrapper_page'; +import { CaseHeaderPage } from './components/case_header_page'; +import { SpyRoute } from '../../utils/route/spy_routes'; +import { getCaseUrl } from '../../components/link_to'; +import { WhitePageWrapper, SectionWrapper } from './components/wrappers'; +import { Connectors } from './components/configure_cases/connectors'; +import * as i18n from './translations'; + +const backOptions = { + href: getCaseUrl(), + text: i18n.BACK_TO_ALL, +}; + +const wrapperPageStyle: Record = { + paddingLeft: '0', + paddingRight: '0', + paddingBottom: '0', +}; + +export const FormWrapper = styled.div` + ${({ theme }) => css` + padding-top: ${theme.eui.paddingSizes.l}; + padding-bottom: ${theme.eui.paddingSizes.l}; + `} +`; + +const ConfigureCasesPageComponent: React.FC = () => ( + <> + + + + + + + + + + + + + + +); + +export const ConfigureCasesPage = React.memo(ConfigureCasesPageComponent); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/create_case.tsx b/x-pack/legacy/plugins/siem/public/pages/case/create_case.tsx index 9bc356517cc68..2c7525264f71b 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/create_case.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/create_case.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { WrapperPage } from '../../components/wrapper_page'; import { Create } from './components/create'; import { SpyRoute } from '../../utils/route/spy_routes'; -import { HeaderPage } from '../../components/header_page'; +import { CaseHeaderPage } from './components/case_header_page'; import * as i18n from './translations'; import { getCaseUrl } from '../../components/link_to'; @@ -17,15 +17,11 @@ const backOptions = { href: getCaseUrl(), text: i18n.BACK_TO_ALL, }; -const badgeOptions = { - beta: true, - text: i18n.PAGE_BADGE_LABEL, - tooltip: i18n.PAGE_BADGE_TOOLTIP, -}; + export const CreateCasePage = React.memo(() => ( <> - + diff --git a/x-pack/legacy/plugins/siem/public/pages/case/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/index.tsx index 9bd91b1c6d62d..1bde9de1535b5 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/index.tsx @@ -11,10 +11,12 @@ import { SiemPageName } from '../home/types'; import { CaseDetailsPage } from './case_details'; import { CasesPage } from './case'; import { CreateCasePage } from './create_case'; +import { ConfigureCasesPage } from './configure_cases'; const casesPagePath = `/:pageName(${SiemPageName.case})`; const caseDetailsPagePath = `${casesPagePath}/:detailName`; const createCasePagePath = `${casesPagePath}/create`; +const configureCasesPagePath = `${casesPagePath}/configure`; const CaseContainerComponent: React.FC = () => ( @@ -24,6 +26,9 @@ const CaseContainerComponent: React.FC = () => ( + + + diff --git a/x-pack/legacy/plugins/siem/public/pages/case/translations.ts b/x-pack/legacy/plugins/siem/public/pages/case/translations.ts index 54af1a38a3f91..5f0509586fc81 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/translations.ts +++ b/x-pack/legacy/plugins/siem/public/pages/case/translations.ts @@ -64,15 +64,6 @@ export const LAST_UPDATED = i18n.translate('xpack.siem.case.caseView.updatedAt', defaultMessage: 'Last updated', }); -export const PAGE_BADGE_LABEL = i18n.translate('xpack.siem.case.caseView.pageBadgeLabel', { - defaultMessage: 'Beta', -}); - -export const PAGE_BADGE_TOOLTIP = i18n.translate('xpack.siem.case.caseView.pageBadgeTooltip', { - defaultMessage: - 'Cases is still in beta. Please help us improve by reporting issues or bugs in the Kibana repo.', -}); - export const PAGE_SUBTITLE = i18n.translate('xpack.siem.case.caseView.pageSubtitle', { defaultMessage: 'Cases within the Elastic SIEM', }); @@ -110,6 +101,17 @@ export const TITLE_REQUIRED = i18n.translate('xpack.siem.case.createCase.titleFi defaultMessage: 'A title is required.', }); +export const CONFIGURE_CASES_PAGE_TITLE = i18n.translate( + 'xpack.siem.case.configureCases.headerTitle', + { + defaultMessage: 'Configure cases', + } +); + +export const CONFIGURE_CASES_BUTTON = i18n.translate('xpack.siem.case.configureCasesButton', { + defaultMessage: 'Configure cases', +}); + export const ADD_COMMENT = i18n.translate('xpack.siem.case.caseView.comment.addComment', { defaultMessage: 'Add comment', }); diff --git a/x-pack/legacy/plugins/siem/public/pages/home/index.tsx b/x-pack/legacy/plugins/siem/public/pages/home/index.tsx index 806f6c7937077..5d20993144af9 100644 --- a/x-pack/legacy/plugins/siem/public/pages/home/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/home/index.tsx @@ -21,6 +21,7 @@ import { AutoSaveWarningMsg } from '../../components/timeline/auto_save_warning' import { UseUrlState } from '../../components/url_state'; import { useWithSource } from '../../containers/source'; import { SpyRoute } from '../../utils/route/spy_routes'; +import { useShowTimeline } from '../../utils/timeline/use_show_timeline'; import { NotFoundPage } from '../404'; import { DetectionEngineContainer } from '../detection_engine'; import { HostsContainer } from '../hosts'; @@ -74,6 +75,8 @@ export const HomePageComponent = () => { const { browserFields, indexPattern, contentAvailable } = useWithSource(); + const [showTimeline] = useShowTimeline(); + return ( @@ -81,7 +84,7 @@ export const HomePageComponent = () => {
- {contentAvailable && ( + {contentAvailable && showTimeline && ( <> { + const currentLocation = useLocation(); + const [showTimeline, setShowTimeline] = useState( + !hideTimelineForRoutes.includes(currentLocation.pathname) + ); + + useEffect(() => { + if (hideTimelineForRoutes.includes(currentLocation.pathname)) { + if (showTimeline) { + setShowTimeline(false); + } + } else if (!showTimeline) { + setShowTimeline(true); + } + }, [currentLocation.pathname]); + + return [showTimeline]; +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_status_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_status_route.ts index c496c7b7ce59c..5687c5d4095db 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_status_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_status_route.ts @@ -59,6 +59,10 @@ export const createFindRulesStatusRoute = (getClients: GetScopedClients): Hapi.S searchFields: ['alertId'], }); const accumulated = await acc; + + // Array accessors can result in undefined but + // this is not represented in typescript for some reason, + // https://github.com/Microsoft/TypeScript/issues/11122 const currentStatus = convertToSnakeCase( lastFiveErrorsForId.saved_objects[0]?.attributes ); 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 3148083b4db26..a382c4a323671 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,6 +6,8 @@ import Boom from 'boom'; +import { SavedObjectsFindResponse } from 'kibana/server'; +import { IRuleSavedAttributesSavedObjectAttributes, IRuleStatusAttributes } from '../rules/types'; import { transformError, transformBulkError, @@ -323,5 +325,19 @@ describe('utils', () => { const values = {}; expect(convertToSnakeCase(values)).toEqual({}); }); + it('returns null when passed in undefined', () => { + // Array accessors can result in undefined but + // this is not represented in typescript for some reason, + // https://github.com/Microsoft/TypeScript/issues/11122 + const values: SavedObjectsFindResponse = { + page: 0, + per_page: 5, + total: 0, + saved_objects: [], + }; + expect( + convertToSnakeCase(values.saved_objects[0]?.attributes) // this is undefined, but it says it's not + ).toEqual(null); + }); }); }); 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 aaa5db7966b2b..65c9141619cb9 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 @@ -222,8 +222,9 @@ export const getIndex = (getSpaceId: () => string, config: LegacyServices['confi return `${signalsIndex}-${spaceId}`; }; -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export const convertToSnakeCase = >(obj: T): Partial | null => { +export const convertToSnakeCase = >( + obj: T +): Partial | null => { if (!obj) { return null; } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/read_rules.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/read_rules.test.ts index aa1cce6f15238..862ea9d2dcbe5 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/read_rules.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/read_rules.test.ts @@ -39,9 +39,9 @@ describe('read_rules', () => { }); test('should return null if saved object found by alerts client given id is not alert type', async () => { const alertsClient = alertsClientMock.create(); - const { alertTypeId, ...rest } = getResult(); - // @ts-ignore - alertsClient.get.mockImplementation(() => rest); + const result = getResult(); + delete result.alertTypeId; + alertsClient.get.mockResolvedValue(result); const rule = await readRules({ alertsClient, @@ -109,8 +109,7 @@ describe('read_rules', () => { test('should return null if the output from alertsClient with ruleId set is empty', async () => { const alertsClient = alertsClientMock.create(); alertsClient.get.mockResolvedValue(getResult()); - // @ts-ignore - alertsClient.find.mockResolvedValue({ data: [] }); + alertsClient.find.mockResolvedValue({ data: [], page: 0, perPage: 1, total: 0 }); const rule = await readRules({ alertsClient, diff --git a/x-pack/legacy/plugins/spaces/index.ts b/x-pack/legacy/plugins/spaces/index.ts index ab3388ae96475..757c1eb557c54 100644 --- a/x-pack/legacy/plugins/spaces/index.ts +++ b/x-pack/legacy/plugins/spaces/index.ts @@ -34,19 +34,6 @@ export const spaces = (kibana: Record) => publicDir: resolve(__dirname, 'public'), require: ['kibana', 'elasticsearch', 'xpack_main'], - uiCapabilities() { - return { - spaces: { - manage: true, - }, - management: { - kibana: { - spaces: true, - }, - }, - }; - }, - uiExports: { styleSheetPaths: resolve(__dirname, 'public/index.scss'), managementSections: [], @@ -110,14 +97,9 @@ export const spaces = (kibana: Record) => throw new Error('New Platform XPack Spaces plugin is not available.'); } - const config = server.config(); - const { registerLegacyAPI, createDefaultSpace } = spacesPlugin.__legacyCompat; registerLegacyAPI({ - legacyConfig: { - kibanaIndex: config.get('kibana.index'), - }, savedObjects: server.savedObjects, auditLogger: { create: (pluginId: string) => diff --git a/x-pack/legacy/plugins/transform/public/__mocks__/shared_imports.ts b/x-pack/legacy/plugins/transform/public/__mocks__/shared_imports.ts index b55a4cd5c7bd6..aa130b5030fc7 100644 --- a/x-pack/legacy/plugins/transform/public/__mocks__/shared_imports.ts +++ b/x-pack/legacy/plugins/transform/public/__mocks__/shared_imports.ts @@ -4,7 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ +jest.mock('ui/new_platform'); + export function XJsonMode() {} export function setDependencyCache() {} +export const useRequest = () => ({ + isLoading: false, + error: null, + data: undefined, +}); export { mlInMemoryTableBasicFactory } from '../../../ml/public/application/components/ml_in_memory_table'; export const SORT_DIRECTION = { ASC: 'asc' }; diff --git a/x-pack/legacy/plugins/transform/public/app/app.tsx b/x-pack/legacy/plugins/transform/public/app/app.tsx index 0f21afbcccca8..efbaabe447efa 100644 --- a/x-pack/legacy/plugins/transform/public/app/app.tsx +++ b/x-pack/legacy/plugins/transform/public/app/app.tsx @@ -5,8 +5,8 @@ */ import React, { useContext, FC } from 'react'; -import { render } from 'react-dom'; -import { Redirect, Route, Switch } from 'react-router-dom'; +import { render, unmountComponentAtNode } from 'react-dom'; +import { HashRouter, Redirect, Route, Switch } from 'react-router-dom'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -22,8 +22,7 @@ import { TransformManagementSection } from './sections/transform_management'; export const App: FC = () => { const { apiError } = useContext(AuthorizationContext); - - if (apiError) { + if (apiError !== null) { return ( { return (
- - - - - - + + + + + + + +
); }; -export const renderReact = (elem: Element, appDependencies: AppDependencies) => { +export const renderApp = (element: HTMLElement, appDependencies: AppDependencies) => { const Providers = getAppProviders(appDependencies); render( , - elem + element ); + + return () => { + unmountComponentAtNode(element); + }; }; diff --git a/x-pack/legacy/plugins/transform/public/app/app_dependencies.tsx b/x-pack/legacy/plugins/transform/public/app/app_dependencies.tsx index 282d1380b396b..21ffbf5911a21 100644 --- a/x-pack/legacy/plugins/transform/public/app/app_dependencies.tsx +++ b/x-pack/legacy/plugins/transform/public/app/app_dependencies.tsx @@ -7,9 +7,6 @@ import React, { createContext, useContext, ReactNode } from 'react'; import { HashRouter } from 'react-router-dom'; -import chrome from 'ui/chrome'; -import { metadata } from 'ui/metadata'; - import { API_BASE_PATH } from '../../common/constants'; import { setDependencyCache } from '../shared_imports'; @@ -17,24 +14,20 @@ import { AppDependencies } from '../shim'; import { AuthorizationProvider } from './lib/authorization'; -const legacyBasePath = { - prepend: chrome.addBasePath, - get: chrome.getBasePath, - remove: () => {}, -}; -const legacyDocLinks = { - ELASTIC_WEBSITE_URL: 'https://www.elastic.co/', - DOC_LINK_VERSION: metadata.branch, -}; - let DependenciesContext: React.Context; const setAppDependencies = (deps: AppDependencies) => { + const legacyBasePath = { + prepend: deps.core.http.basePath.prepend, + get: deps.core.http.basePath.get, + remove: () => {}, + }; + setDependencyCache({ autocomplete: deps.plugins.data.autocomplete, - docLinks: legacyDocLinks as any, + docLinks: deps.core.docLinks, basePath: legacyBasePath as any, - XSRF: chrome.getXsrfToken(), + XSRF: deps.plugins.xsrfToken, }); DependenciesContext = createContext(deps); return DependenciesContext.Provider; @@ -48,6 +41,22 @@ export const useAppDependencies = () => { return useContext(DependenciesContext); }; +export const useDocumentationLinks = () => { + const { + core: { documentation }, + } = useAppDependencies(); + return documentation; +}; + +export const useToastNotifications = () => { + const { + core: { + notifications: { toasts: toastNotifications }, + }, + } = useAppDependencies(); + return toastNotifications; +}; + export const getAppProviders = (deps: AppDependencies) => { const I18nContext = deps.core.i18n.Context; @@ -55,9 +64,7 @@ export const getAppProviders = (deps: AppDependencies) => { const AppDependenciesProvider = setAppDependencies(deps); return ({ children }: { children: ReactNode }) => ( - + {children} diff --git a/x-pack/legacy/plugins/transform/public/app/common/navigation.tsx b/x-pack/legacy/plugins/transform/public/app/common/navigation.tsx index ac98d92fdba83..15966a93e1f42 100644 --- a/x-pack/legacy/plugins/transform/public/app/common/navigation.tsx +++ b/x-pack/legacy/plugins/transform/public/app/common/navigation.tsx @@ -29,9 +29,9 @@ export function getDiscoverUrl(indexPatternId: string, baseUrl: string): string } export const RedirectToTransformManagement: FC = () => ( - + ); export const RedirectToCreateTransform: FC<{ savedObjectId: string }> = ({ savedObjectId }) => ( - + ); diff --git a/x-pack/legacy/plugins/transform/public/app/components/section_error.tsx b/x-pack/legacy/plugins/transform/public/app/components/section_error.tsx index 2ad6f0870c140..4bef917a91a18 100644 --- a/x-pack/legacy/plugins/transform/public/app/components/section_error.tsx +++ b/x-pack/legacy/plugins/transform/public/app/components/section_error.tsx @@ -4,18 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiCallOut, EuiSpacer } from '@elastic/eui'; -import React, { Fragment } from 'react'; +import { EuiCallOut } from '@elastic/eui'; +import React from 'react'; interface Props { title: React.ReactNode; - error: { - data: { - error: string; - cause?: string[]; - message?: string; - }; - }; + error: Error | null; actions?: JSX.Element; } @@ -25,25 +19,11 @@ export const SectionError: React.FunctionComponent = ({ actions, ...rest }) => { - const { - error: errorString, - cause, // wrapEsError() on the server adds a "cause" array - message, - } = error.data; + const errorMessage = error?.message ?? JSON.stringify(error, null, 2); return ( - {cause ? message || errorString :

{message || errorString}

} - {cause && ( - - -
    - {cause.map((causeMsg, i) => ( -
  • {causeMsg}
  • - ))} -
-
- )} +
{errorMessage}
{actions ? actions : null}
); diff --git a/x-pack/legacy/plugins/transform/public/app/components/toast_notification_text.test.tsx b/x-pack/legacy/plugins/transform/public/app/components/toast_notification_text.test.tsx index 904d788b04e2c..81af5c974fe04 100644 --- a/x-pack/legacy/plugins/transform/public/app/components/toast_notification_text.test.tsx +++ b/x-pack/legacy/plugins/transform/public/app/components/toast_notification_text.test.tsx @@ -6,23 +6,44 @@ import React from 'react'; import { render } from '@testing-library/react'; + +import { KibanaContext } from '../lib/kibana'; +import { createPublicShim } from '../../shim'; +import { getAppProviders } from '../app_dependencies'; + import { ToastNotificationText } from './toast_notification_text'; +jest.mock('../../shared_imports'); + describe('ToastNotificationText', () => { test('should render the text as plain text', () => { + const Providers = getAppProviders(createPublicShim()); const props = { text: 'a short text message', }; - const { container } = render(); + const { container } = render( + + + + + + ); expect(container.textContent).toBe('a short text message'); }); test('should render the text within a modal', () => { + const Providers = getAppProviders(createPublicShim()); const props = { text: 'a text message that is longer than 140 characters. a text message that is longer than 140 characters. a text message that is longer than 140 characters. ', }; - const { container } = render(); + const { container } = render( + + + + + + ); expect(container.textContent).toBe( 'a text message that is longer than 140 characters. a text message that is longer than 140 characters. a text message that is longer than 140 ...View details' ); diff --git a/x-pack/legacy/plugins/transform/public/app/components/toast_notification_text.tsx b/x-pack/legacy/plugins/transform/public/app/components/toast_notification_text.tsx index c79bf52a86642..4e0a0a12558d8 100644 --- a/x-pack/legacy/plugins/transform/public/app/components/toast_notification_text.tsx +++ b/x-pack/legacy/plugins/transform/public/app/components/toast_notification_text.tsx @@ -18,12 +18,17 @@ import { import { i18n } from '@kbn/i18n'; -import { npStart } from 'ui/new_platform'; import { toMountPoint } from '../../../../../../../src/plugins/kibana_react/public'; +import { useAppDependencies } from '../app_dependencies'; + const MAX_SIMPLE_MESSAGE_LENGTH = 140; export const ToastNotificationText: FC<{ text: any }> = ({ text }) => { + const { + core: { overlays }, + } = useAppDependencies(); + if (typeof text === 'string' && text.length <= MAX_SIMPLE_MESSAGE_LENGTH) { return text; } @@ -43,7 +48,7 @@ export const ToastNotificationText: FC<{ text: any }> = ({ text }) => { }`; const openModal = () => { - const modal = npStart.core.overlays.openModal( + const modal = overlays.openModal( toMountPoint( modal.close()}> diff --git a/x-pack/legacy/plugins/transform/public/app/constants/index.ts b/x-pack/legacy/plugins/transform/public/app/constants/index.ts index 78b5f018dd782..5d71980c83714 100644 --- a/x-pack/legacy/plugins/transform/public/app/constants/index.ts +++ b/x-pack/legacy/plugins/transform/public/app/constants/index.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -export const CLIENT_BASE_PATH = '/management/elasticsearch/transform'; +export const CLIENT_BASE_PATH = '/management/elasticsearch/transform/'; export enum SECTION_SLUG { HOME = 'transform_management', diff --git a/x-pack/legacy/plugins/transform/public/app/hooks/index.ts b/x-pack/legacy/plugins/transform/public/app/hooks/index.ts index 7981f560a525f..a36550bcd8e57 100644 --- a/x-pack/legacy/plugins/transform/public/app/hooks/index.ts +++ b/x-pack/legacy/plugins/transform/public/app/hooks/index.ts @@ -9,3 +9,4 @@ export { useGetTransforms } from './use_get_transforms'; export { useDeleteTransforms } from './use_delete_transform'; export { useStartTransforms } from './use_start_transform'; export { useStopTransforms } from './use_stop_transform'; +export { useRequest } from './use_request'; diff --git a/x-pack/legacy/plugins/transform/public/app/hooks/use_api.ts b/x-pack/legacy/plugins/transform/public/app/hooks/use_api.ts index c71299eccb34d..802599aaedd4f 100644 --- a/x-pack/legacy/plugins/transform/public/app/hooks/use_api.ts +++ b/x-pack/legacy/plugins/transform/public/app/hooks/use_api.ts @@ -5,14 +5,12 @@ */ import { useAppDependencies } from '../app_dependencies'; - import { PreviewRequestBody, TransformId } from '../common'; - -import { http } from '../services/http_service'; +import { httpFactory, Http } from '../services/http_service'; import { EsIndex, TransformEndpointRequest, TransformEndpointResult } from './use_api_types'; -const apiFactory = (basePath: string, indicesBasePath: string) => ({ +const apiFactory = (basePath: string, indicesBasePath: string, http: Http) => ({ getTransforms(transformId?: TransformId): Promise { const transformIdString = transformId !== undefined ? `/${transformId}` : ''; return http({ @@ -98,6 +96,8 @@ export const useApi = () => { const basePath = appDeps.core.http.basePath.prepend('/api/transform'); const indicesBasePath = appDeps.core.http.basePath.prepend('/api'); + const xsrfToken = appDeps.plugins.xsrfToken; + const http = httpFactory(xsrfToken); - return apiFactory(basePath, indicesBasePath); + return apiFactory(basePath, indicesBasePath, http); }; diff --git a/x-pack/legacy/plugins/transform/public/app/hooks/use_delete_transform.tsx b/x-pack/legacy/plugins/transform/public/app/hooks/use_delete_transform.tsx index e23151900447c..83f456231cb85 100644 --- a/x-pack/legacy/plugins/transform/public/app/hooks/use_delete_transform.tsx +++ b/x-pack/legacy/plugins/transform/public/app/hooks/use_delete_transform.tsx @@ -7,9 +7,9 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; -import { toastNotifications } from 'ui/notify'; import { toMountPoint } from '../../../../../../../src/plugins/kibana_react/public'; +import { useToastNotifications } from '../app_dependencies'; import { TransformListRow, refreshTransformList$, REFRESH_TRANSFORM_LIST_STATE } from '../common'; import { ToastNotificationText } from '../components'; @@ -17,6 +17,7 @@ import { useApi } from './use_api'; import { TransformEndpointRequest, TransformEndpointResult } from './use_api_types'; export const useDeleteTransforms = () => { + const toastNotifications = useToastNotifications(); const api = useApi(); return async (transforms: TransformListRow[]) => { @@ -54,7 +55,9 @@ export const useDeleteTransforms = () => { title: i18n.translate('xpack.transform.transformList.deleteTransformGenericErrorMessage', { defaultMessage: 'An error occurred calling the API endpoint to delete transforms.', }), - text: toMountPoint(), + text: toMountPoint( + + ), }); } }; diff --git a/x-pack/legacy/plugins/transform/public/app/hooks/use_request.ts b/x-pack/legacy/plugins/transform/public/app/hooks/use_request.ts new file mode 100644 index 0000000000000..8c489048a77ef --- /dev/null +++ b/x-pack/legacy/plugins/transform/public/app/hooks/use_request.ts @@ -0,0 +1,16 @@ +/* + * 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 { UseRequestConfig, useRequest as _useRequest } from '../../shared_imports'; + +import { useAppDependencies } from '../app_dependencies'; + +export const useRequest = (config: UseRequestConfig) => { + const { + core: { http }, + } = useAppDependencies(); + return _useRequest(http, config); +}; diff --git a/x-pack/legacy/plugins/transform/public/app/hooks/use_start_transform.ts b/x-pack/legacy/plugins/transform/public/app/hooks/use_start_transform.ts index d6b216accebd9..f460d8200c6e4 100644 --- a/x-pack/legacy/plugins/transform/public/app/hooks/use_start_transform.ts +++ b/x-pack/legacy/plugins/transform/public/app/hooks/use_start_transform.ts @@ -5,14 +5,15 @@ */ import { i18n } from '@kbn/i18n'; -import { toastNotifications } from 'ui/notify'; +import { useToastNotifications } from '../app_dependencies'; import { TransformListRow, refreshTransformList$, REFRESH_TRANSFORM_LIST_STATE } from '../common'; import { useApi } from './use_api'; import { TransformEndpointRequest, TransformEndpointResult } from './use_api_types'; export const useStartTransforms = () => { + const toastNotifications = useToastNotifications(); const api = useApi(); return async (transforms: TransformListRow[]) => { diff --git a/x-pack/legacy/plugins/transform/public/app/hooks/use_stop_transform.ts b/x-pack/legacy/plugins/transform/public/app/hooks/use_stop_transform.ts index bf6edf995bb1f..758c574a3f7cd 100644 --- a/x-pack/legacy/plugins/transform/public/app/hooks/use_stop_transform.ts +++ b/x-pack/legacy/plugins/transform/public/app/hooks/use_stop_transform.ts @@ -5,14 +5,15 @@ */ import { i18n } from '@kbn/i18n'; -import { toastNotifications } from 'ui/notify'; +import { useToastNotifications } from '../app_dependencies'; import { TransformListRow, refreshTransformList$, REFRESH_TRANSFORM_LIST_STATE } from '../common'; import { useApi } from './use_api'; import { TransformEndpointRequest, TransformEndpointResult } from './use_api_types'; export const useStopTransforms = () => { + const toastNotifications = useToastNotifications(); const api = useApi(); return async (transforms: TransformListRow[]) => { diff --git a/x-pack/legacy/plugins/transform/public/app/lib/authorization/components/authorization_provider.tsx b/x-pack/legacy/plugins/transform/public/app/lib/authorization/components/authorization_provider.tsx index 8060659c78b80..dde63710f56aa 100644 --- a/x-pack/legacy/plugins/transform/public/app/lib/authorization/components/authorization_provider.tsx +++ b/x-pack/legacy/plugins/transform/public/app/lib/authorization/components/authorization_provider.tsx @@ -5,20 +5,12 @@ */ import React, { createContext } from 'react'; -import { useRequest } from '../../../services/http/use_request'; +import { useRequest } from '../../../hooks'; import { hasPrivilegeFactory, Capabilities, Privileges } from './common'; -interface ApiError { - data: { - error: string; - cause?: string[]; - message?: string; - }; -} - interface Authorization { isLoading: boolean; - apiError: ApiError | null; + apiError: Error | null; privileges: Privileges; capabilities: Capabilities; } @@ -58,7 +50,7 @@ export const AuthorizationProvider = ({ privilegesEndpoint, children }: Props) = isLoading, privileges: isLoading ? { ...initialValue.privileges } : privilegesData, capabilities: { ...initialCapabalities }, - apiError: error ? (error as ApiError) : null, + apiError: error ? (error as Error) : null, }; const hasPrivilege = hasPrivilegeFactory(value.privileges); diff --git a/x-pack/legacy/plugins/transform/public/app/lib/kibana/kibana_context.tsx b/x-pack/legacy/plugins/transform/public/app/lib/kibana/kibana_context.tsx index b0a0371d2de86..3acec1ea0e809 100644 --- a/x-pack/legacy/plugins/transform/public/app/lib/kibana/kibana_context.tsx +++ b/x-pack/legacy/plugins/transform/public/app/lib/kibana/kibana_context.tsx @@ -22,7 +22,6 @@ export interface InitializedKibanaContextValue { combinedQuery: any; indexPatterns: IndexPatternsContract; initialized: true; - kbnBaseUrl: string; kibanaConfig: IUiSettingsClient; currentIndexPattern: IndexPattern; currentSavedSearch?: SavedSearch; diff --git a/x-pack/legacy/plugins/transform/public/app/lib/kibana/kibana_provider.tsx b/x-pack/legacy/plugins/transform/public/app/lib/kibana/kibana_provider.tsx index d2cf5f2b32910..f2574a4a85f29 100644 --- a/x-pack/legacy/plugins/transform/public/app/lib/kibana/kibana_provider.tsx +++ b/x-pack/legacy/plugins/transform/public/app/lib/kibana/kibana_provider.tsx @@ -6,8 +6,6 @@ import React, { useEffect, useState, FC } from 'react'; -import { npStart } from 'ui/new_platform'; - import { useAppDependencies } from '../../app_dependencies'; import { @@ -19,15 +17,14 @@ import { import { InitializedKibanaContextValue, KibanaContext, KibanaContextValue } from './kibana_context'; -const indexPatterns = npStart.plugins.data.indexPatterns; -const savedObjectsClient = npStart.core.savedObjects.client; - interface Props { savedObjectId: string; } export const KibanaProvider: FC = ({ savedObjectId, children }) => { const appDeps = useAppDependencies(); + const indexPatterns = appDeps.plugins.data.indexPatterns; + const savedObjectsClient = appDeps.core.savedObjects.client; const savedSearches = appDeps.plugins.savedSearches.getClient(); const [contextValue, setContextValue] = useState({ initialized: false }); @@ -50,7 +47,7 @@ export const KibanaProvider: FC = ({ savedObjectId, children }) => { // Just let fetchedSavedSearch stay undefined in case it doesn't exist. } - const kibanaConfig = npStart.core.uiSettings; + const kibanaConfig = appDeps.core.uiSettings; const { indexPattern: currentIndexPattern, @@ -61,7 +58,6 @@ export const KibanaProvider: FC = ({ savedObjectId, children }) => { const kibanaContext: InitializedKibanaContextValue = { indexPatterns, initialized: true, - kbnBaseUrl: npStart.core.injectedMetadata.getBasePath(), kibanaConfig, combinedQuery, currentIndexPattern, diff --git a/x-pack/legacy/plugins/transform/public/app/sections/clone_transform/clone_transform_section.tsx b/x-pack/legacy/plugins/transform/public/app/sections/clone_transform/clone_transform_section.tsx index de96a4de32962..8f58bc94e7c12 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/clone_transform/clone_transform_section.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/clone_transform/clone_transform_section.tsx @@ -22,14 +22,13 @@ import { EuiTitle, } from '@elastic/eui'; -import { npStart } from 'ui/new_platform'; - import { useApi } from '../../hooks/use_api'; import { APP_CREATE_TRANSFORM_CLUSTER_PRIVILEGES } from '../../../../common/constants'; + +import { useAppDependencies, useDocumentationLinks } from '../../app_dependencies'; import { TransformPivotConfig } from '../../common'; import { breadcrumbService, docTitleService, BREADCRUMB_SECTION } from '../../services/navigation'; -import { documentationLinksService } from '../../services/documentation'; import { PrivilegesWrapper } from '../../lib/authorization'; import { getIndexPatternIdByTitle, @@ -40,9 +39,6 @@ import { import { Wizard } from '../create_transform/components/wizard'; -const indexPatterns = npStart.plugins.data.indexPatterns; -const savedObjectsClient = npStart.core.savedObjects.client; - interface GetTransformsResponseOk { count: number; transforms: TransformPivotConfig[]; @@ -74,6 +70,12 @@ export const CloneTransformSection: FC = ({ match }) => { const api = useApi(); + const appDeps = useAppDependencies(); + const savedObjectsClient = appDeps.core.savedObjects.client; + const indexPatterns = appDeps.plugins.data.indexPatterns; + + const { esTransform } = useDocumentationLinks(); + const transformId = match.params.transformId; const [transformConfig, setTransformConfig] = useState(); @@ -154,7 +156,7 @@ export const CloneTransformSection: FC = ({ match }) => { { const r = jest.requireActual('react'); diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_form.test.tsx b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_form.test.tsx index a0c91c070844b..625c545ee8c46 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_form.test.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_form.test.tsx @@ -11,8 +11,6 @@ import { KibanaContext } from '../../../../lib/kibana'; import { StepCreateForm } from './step_create_form'; -jest.mock('ui/new_platform'); - // workaround to make React.memo() work with enzyme jest.mock('react', () => { const r = jest.requireActual('react'); diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_form.tsx b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_form.tsx index 2ca3253d72b44..312d8a30dab77 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_form.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_form.tsx @@ -6,7 +6,6 @@ import React, { Fragment, FC, useEffect, useState } from 'react'; import { i18n } from '@kbn/i18n'; -import { toastNotifications } from 'ui/notify'; import { EuiButton, @@ -30,13 +29,15 @@ import { } from '@elastic/eui'; import { toMountPoint } from '../../../../../../../../../../src/plugins/kibana_react/public'; -import { ToastNotificationText } from '../../../../components'; -import { useApi } from '../../../../hooks/use_api'; -import { useKibanaContext } from '../../../../lib/kibana'; -import { RedirectToTransformManagement } from '../../../../common/navigation'; + import { PROGRESS_REFRESH_INTERVAL_MS } from '../../../../../../common/constants'; import { getTransformProgress, getDiscoverUrl } from '../../../../common'; +import { useApi } from '../../../../hooks/use_api'; +import { useKibanaContext } from '../../../../lib/kibana'; +import { useAppDependencies, useToastNotifications } from '../../../../app_dependencies'; +import { RedirectToTransformManagement } from '../../../../common/navigation'; +import { ToastNotificationText } from '../../../../components'; export interface StepDetailsExposedState { created: boolean; @@ -73,7 +74,9 @@ export const StepCreateForm: FC = React.memo( undefined ); + const deps = useAppDependencies(); const kibanaContext = useKibanaContext(); + const toastNotifications = useToastNotifications(); useEffect(() => { onChange({ created, started, indexPatternId }); @@ -437,7 +440,7 @@ export const StepCreateForm: FC = React.memo( defaultMessage: 'Use Discover to explore the transform.', } )} - href={getDiscoverUrl(indexPatternId, kibanaContext.kbnBaseUrl)} + href={getDiscoverUrl(indexPatternId, deps.core.http.basePath.get())} data-test-subj="transformWizardCardDiscover" /> diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/pivot_preview.test.tsx b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/pivot_preview.test.tsx index a2aa056c1634d..2ac4295da1eed 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/pivot_preview.test.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/pivot_preview.test.tsx @@ -19,8 +19,6 @@ import { import { PivotPreview } from './pivot_preview'; -jest.mock('ui/new_platform'); - // workaround to make React.memo() work with enzyme jest.mock('react', () => { const r = jest.requireActual('react'); diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.test.tsx b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.test.tsx index 0311b26304c30..44edd1340e8d6 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.test.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.test.tsx @@ -15,9 +15,7 @@ import { PIVOT_SUPPORTED_AGGS, PIVOT_SUPPORTED_GROUP_BY_AGGS, } from '../../../../common'; -import { StepDefineForm, isAggNameConflict } from './step_define_form'; - -jest.mock('ui/new_platform'); +import { StepDefineForm, getAggNameConflictToastMessages } from './step_define_form'; // workaround to make React.memo() work with enzyme jest.mock('react', () => { @@ -76,38 +74,78 @@ describe('Transform: isAggNameConflict()', () => { }; // no conflict, completely different name, no namespacing involved - expect(isAggNameConflict('the-other-agg-name', aggList, groupByList)).toBe(false); + expect( + getAggNameConflictToastMessages('the-other-agg-name', aggList, groupByList) + ).toHaveLength(0); // no conflict, completely different name and no conflicting namespace - expect(isAggNameConflict('the-other-agg-name.namespace', aggList, groupByList)).toBe(false); + expect( + getAggNameConflictToastMessages('the-other-agg-name.namespace', aggList, groupByList) + ).toHaveLength(0); // exact match conflict on aggregation name - expect(isAggNameConflict('the-agg-name', aggList, groupByList)).toBe(true); + expect(getAggNameConflictToastMessages('the-agg-name', aggList, groupByList)).toStrictEqual([ + `An aggregation configuration with the name 'the-agg-name' already exists.`, + ]); // namespace conflict with `the-agg-name` aggregation - expect(isAggNameConflict('the-agg-name.namespace', aggList, groupByList)).toBe(true); + expect( + getAggNameConflictToastMessages('the-agg-name.namespace', aggList, groupByList) + ).toStrictEqual([ + `Couldn't add configuration 'the-agg-name.namespace' because of a nesting conflict with 'the-agg-name'.`, + ]); // exact match conflict on group-by name - expect(isAggNameConflict('the-group-by-agg-name', aggList, groupByList)).toBe(true); + expect( + getAggNameConflictToastMessages('the-group-by-agg-name', aggList, groupByList) + ).toStrictEqual([ + `A group by configuration with the name 'the-group-by-agg-name' already exists.`, + ]); // namespace conflict with `the-group-by-agg-name` group-by - expect(isAggNameConflict('the-group-by-agg-name.namespace', aggList, groupByList)).toBe(true); + expect( + getAggNameConflictToastMessages('the-group-by-agg-name.namespace', aggList, groupByList) + ).toStrictEqual([ + `Couldn't add configuration 'the-group-by-agg-name.namespace' because of a nesting conflict with 'the-group-by-agg-name'.`, + ]); // exact match conflict on namespaced agg name - expect(isAggNameConflict('the-namespaced-agg-name.namespace', aggList, groupByList)).toBe(true); + expect( + getAggNameConflictToastMessages('the-namespaced-agg-name.namespace', aggList, groupByList) + ).toStrictEqual([ + `An aggregation configuration with the name 'the-namespaced-agg-name.namespace' already exists.`, + ]); // no conflict, same base agg name but different namespace - expect(isAggNameConflict('the-namespaced-agg-name.namespace2', aggList, groupByList)).toBe( - false - ); + expect( + getAggNameConflictToastMessages('the-namespaced-agg-name.namespace2', aggList, groupByList) + ).toHaveLength(0); // namespace conflict because the new agg name is base name of existing nested field - expect(isAggNameConflict('the-namespaced-agg-name', aggList, groupByList)).toBe(true); + expect( + getAggNameConflictToastMessages('the-namespaced-agg-name', aggList, groupByList) + ).toStrictEqual([ + `Couldn't add configuration 'the-namespaced-agg-name' because of a nesting conflict with 'the-namespaced-agg-name.namespace'.`, + ]); // exact match conflict on namespaced group-by name expect( - isAggNameConflict('the-namespaced-group-by-agg-name.namespace', aggList, groupByList) - ).toBe(true); + getAggNameConflictToastMessages( + 'the-namespaced-group-by-agg-name.namespace', + aggList, + groupByList + ) + ).toStrictEqual([ + `A group by configuration with the name 'the-namespaced-group-by-agg-name.namespace' already exists.`, + ]); // no conflict, same base group-by name but different namespace expect( - isAggNameConflict('the-namespaced-group-by-agg-name.namespace2', aggList, groupByList) - ).toBe(false); + getAggNameConflictToastMessages( + 'the-namespaced-group-by-agg-name.namespace2', + aggList, + groupByList + ) + ).toHaveLength(0); // namespace conflict because the new group-by name is base name of existing nested field - expect(isAggNameConflict('the-namespaced-group-by-agg-name', aggList, groupByList)).toBe(true); + expect( + getAggNameConflictToastMessages('the-namespaced-group-by-agg-name', aggList, groupByList) + ).toStrictEqual([ + `Couldn't add configuration 'the-namespaced-group-by-agg-name' because of a nesting conflict with 'the-namespaced-group-by-agg-name.namespace'.`, + ]); }); }); diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx index 1499f99f82824..3adb74e4704dc 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx @@ -9,9 +9,6 @@ import React, { Fragment, FC, useEffect, useState } from 'react'; import { i18n } from '@kbn/i18n'; -import { metadata } from 'ui/metadata'; -import { toastNotifications } from 'ui/notify'; - import { EuiButton, EuiCodeEditor, @@ -29,6 +26,7 @@ import { } from '@elastic/eui'; import { useXJsonMode, xJsonMode } from '../../../../hooks/use_x_json_mode'; +import { useDocumentationLinks, useToastNotifications } from '../../../../app_dependencies'; import { TransformPivotConfig } from '../../../../common'; import { dictionaryToArray, Dictionary } from '../../../../../../common/types/common'; import { DropDown } from '../aggregation_dropdown'; @@ -147,32 +145,30 @@ export function applyTransformConfigToDefineState( return state; } -export function isAggNameConflict( +export function getAggNameConflictToastMessages( aggName: AggName, aggList: PivotAggsConfigDict, groupByList: PivotGroupByConfigDict -) { +): string[] { if (aggList[aggName] !== undefined) { - toastNotifications.addDanger( + return [ i18n.translate('xpack.transform.stepDefineForm.aggExistsErrorMessage', { defaultMessage: `An aggregation configuration with the name '{aggName}' already exists.`, values: { aggName }, - }) - ); - return true; + }), + ]; } if (groupByList[aggName] !== undefined) { - toastNotifications.addDanger( + return [ i18n.translate('xpack.transform.stepDefineForm.groupByExistsErrorMessage', { defaultMessage: `A group by configuration with the name '{aggName}' already exists.`, values: { aggName }, - }) - ); - return true; + }), + ]; } - let conflict = false; + const conflicts: string[] = []; // check the new aggName against existing aggs and groupbys const aggNameSplit = aggName.split('.'); @@ -180,29 +176,28 @@ export function isAggNameConflict( aggNameSplit.forEach(aggNamePart => { aggNameCheck = aggNameCheck === undefined ? aggNamePart : `${aggNameCheck}.${aggNamePart}`; if (aggList[aggNameCheck] !== undefined || groupByList[aggNameCheck] !== undefined) { - toastNotifications.addDanger( + conflicts.push( i18n.translate('xpack.transform.stepDefineForm.nestedConflictErrorMessage', { defaultMessage: `Couldn't add configuration '{aggName}' because of a nesting conflict with '{aggNameCheck}'.`, values: { aggName, aggNameCheck }, }) ); - conflict = true; } }); - if (conflict) { - return true; + if (conflicts.length > 0) { + return conflicts; } // check all aggs against new aggName - conflict = Object.keys(aggList).some(aggListName => { + Object.keys(aggList).some(aggListName => { const aggListNameSplit = aggListName.split('.'); let aggListNameCheck: string; return aggListNameSplit.some(aggListNamePart => { aggListNameCheck = aggListNameCheck === undefined ? aggListNamePart : `${aggListNameCheck}.${aggListNamePart}`; if (aggListNameCheck === aggName) { - toastNotifications.addDanger( + conflicts.push( i18n.translate('xpack.transform.stepDefineForm.nestedAggListConflictErrorMessage', { defaultMessage: `Couldn't add configuration '{aggName}' because of a nesting conflict with '{aggListName}'.`, values: { aggName, aggListName }, @@ -214,12 +209,12 @@ export function isAggNameConflict( }); }); - if (conflict) { - return true; + if (conflicts.length > 0) { + return conflicts; } // check all group-bys against new aggName - conflict = Object.keys(groupByList).some(groupByListName => { + Object.keys(groupByList).some(groupByListName => { const groupByListNameSplit = groupByListName.split('.'); let groupByListNameCheck: string; return groupByListNameSplit.some(groupByListNamePart => { @@ -228,7 +223,7 @@ export function isAggNameConflict( ? groupByListNamePart : `${groupByListNameCheck}.${groupByListNamePart}`; if (groupByListNameCheck === aggName) { - toastNotifications.addDanger( + conflicts.push( i18n.translate('xpack.transform.stepDefineForm.nestedGroupByListConflictErrorMessage', { defaultMessage: `Couldn't add configuration '{aggName}' because of a nesting conflict with '{groupByListName}'.`, values: { aggName, groupByListName }, @@ -240,7 +235,7 @@ export function isAggNameConflict( }); }); - return conflict; + return conflicts; } interface Props { @@ -250,6 +245,8 @@ interface Props { export const StepDefineForm: FC = React.memo(({ overrides = {}, onChange }) => { const kibanaContext = useKibanaContext(); + const toastNotifications = useToastNotifications(); + const { esQueryDsl, esTransformPivot } = useDocumentationLinks(); const defaults = { ...getDefaultStepDefineState(kibanaContext), ...overrides }; @@ -288,7 +285,9 @@ export const StepDefineForm: FC = React.memo(({ overrides = {}, onChange const config: PivotGroupByConfig = groupByOptionsData[label]; const aggName: AggName = config.aggName; - if (isAggNameConflict(aggName, aggList, groupByList)) { + const aggNameConflictMessages = getAggNameConflictToastMessages(aggName, aggList, groupByList); + if (aggNameConflictMessages.length > 0) { + aggNameConflictMessages.forEach(m => toastNotifications.addDanger(m)); return; } @@ -300,7 +299,13 @@ export const StepDefineForm: FC = React.memo(({ overrides = {}, onChange const groupByListWithoutPrevious = { ...groupByList }; delete groupByListWithoutPrevious[previousAggName]; - if (isAggNameConflict(item.aggName, aggList, groupByListWithoutPrevious)) { + const aggNameConflictMessages = getAggNameConflictToastMessages( + item.aggName, + aggList, + groupByListWithoutPrevious + ); + if (aggNameConflictMessages.length > 0) { + aggNameConflictMessages.forEach(m => toastNotifications.addDanger(m)); return; } @@ -321,7 +326,9 @@ export const StepDefineForm: FC = React.memo(({ overrides = {}, onChange const config: PivotAggsConfig = aggOptionsData[label]; const aggName: AggName = config.aggName; - if (isAggNameConflict(aggName, aggList, groupByList)) { + const aggNameConflictMessages = getAggNameConflictToastMessages(aggName, aggList, groupByList); + if (aggNameConflictMessages.length > 0) { + aggNameConflictMessages.forEach(m => toastNotifications.addDanger(m)); return; } @@ -333,7 +340,13 @@ export const StepDefineForm: FC = React.memo(({ overrides = {}, onChange const aggListWithoutPrevious = { ...aggList }; delete aggListWithoutPrevious[previousAggName]; - if (isAggNameConflict(item.aggName, aggListWithoutPrevious, groupByList)) { + const aggNameConflictMessages = getAggNameConflictToastMessages( + item.aggName, + aggListWithoutPrevious, + groupByList + ); + if (aggNameConflictMessages.length > 0) { + aggNameConflictMessages.forEach(m => toastNotifications.addDanger(m)); return; } @@ -477,15 +490,13 @@ export const StepDefineForm: FC = React.memo(({ overrides = {}, onChange setAdvancedSourceEditorApplyButtonEnabled(false); }; - // metadata.branch corresponds to the version used in documentation links. - const docsUrl = `https://www.elastic.co/guide/en/elasticsearch/reference/${metadata.branch}/transform-resource.html#transform-pivot`; const advancedEditorHelpText = ( {i18n.translate('xpack.transform.stepDefineForm.advancedEditorHelpText', { defaultMessage: 'The advanced editor allows you to edit the pivot configuration of the transform.', })}{' '} - + {i18n.translate('xpack.transform.stepDefineForm.advancedEditorHelpTextLink', { defaultMessage: 'Learn more about available options.', })} @@ -493,14 +504,13 @@ export const StepDefineForm: FC = React.memo(({ overrides = {}, onChange ); - const sourceDocsUrl = `https://www.elastic.co/guide/en/elasticsearch/reference/${metadata.branch}/query-dsl.html`; const advancedSourceEditorHelpText = ( {i18n.translate('xpack.transform.stepDefineForm.advancedSourceEditorHelpText', { defaultMessage: 'The advanced editor allows you to edit the source query clause of the transform.', })}{' '} - + {i18n.translate('xpack.transform.stepDefineForm.advancedEditorHelpTextLink', { defaultMessage: 'Learn more about available options.', })} diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.test.tsx b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.test.tsx index aae366e6008d5..78f6fc30f9191 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.test.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.test.tsx @@ -18,8 +18,6 @@ import { import { StepDefineExposedState } from './step_define_form'; import { StepDefineSummary } from './step_define_summary'; -jest.mock('ui/new_platform'); - // workaround to make React.memo() work with enzyme jest.mock('react', () => { const r = jest.requireActual('react'); diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_form.tsx b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_form.tsx index 220923f88ed36..5ae2180bfe779 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_form.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_form.tsx @@ -7,8 +7,6 @@ import React, { Fragment, FC, useEffect, useState } from 'react'; import { i18n } from '@kbn/i18n'; -import { metadata } from 'ui/metadata'; -import { toastNotifications } from 'ui/notify'; import { EuiLink, EuiSwitch, EuiFieldText, EuiForm, EuiFormRow, EuiSelect } from '@elastic/eui'; @@ -16,6 +14,7 @@ import { toMountPoint } from '../../../../../../../../../../src/plugins/kibana_r import { useKibanaContext } from '../../../../lib/kibana'; import { isValidIndexName } from '../../../../../../common/utils/es_utils'; +import { useDocumentationLinks, useToastNotifications } from '../../../../app_dependencies'; import { ToastNotificationText } from '../../../../components'; import { useApi } from '../../../../hooks/use_api'; @@ -72,6 +71,8 @@ interface Props { export const StepDetailsForm: FC = React.memo(({ overrides = {}, onChange }) => { const kibanaContext = useKibanaContext(); + const toastNotifications = useToastNotifications(); + const { esIndicesCreateIndex } = useDocumentationLinks(); const defaults = { ...getDefaultStepDetailsState(), ...overrides }; @@ -274,10 +275,7 @@ export const StepDetailsForm: FC = React.memo(({ overrides = {}, onChange defaultMessage: 'Invalid destination index name.', })}
- + {i18n.translate( 'xpack.transform.stepDetailsForm.destinationIndexInvalidErrorLink', { diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/create_transform_section.tsx b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/create_transform_section.tsx index ae82f03718c02..e92ba256256a4 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/create_transform_section.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/create_transform_section.tsx @@ -22,8 +22,9 @@ import { } from '@elastic/eui'; import { APP_CREATE_TRANSFORM_CLUSTER_PRIVILEGES } from '../../../../common/constants'; + +import { useDocumentationLinks } from '../../app_dependencies'; import { breadcrumbService, docTitleService, BREADCRUMB_SECTION } from '../../services/navigation'; -import { documentationLinksService } from '../../services/documentation'; import { PrivilegesWrapper } from '../../lib/authorization'; import { KibanaProvider, RenderOnlyWithInitializedKibanaContext } from '../../lib/kibana'; @@ -37,6 +38,8 @@ export const CreateTransformSection: FC = ({ match }) => { docTitleService.setTitle('createTransform'); }, []); + const { esTransform } = useDocumentationLinks(); + return ( @@ -65,7 +68,7 @@ export const CreateTransformSection: FC = ({ match }) => { ', () => { diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/action_clone.tsx b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/action_clone.tsx index 40098ac7ef72a..4b333f73f048c 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/action_clone.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/action_clone.tsx @@ -30,7 +30,7 @@ export const CloneAction: FC = ({ itemId }) => { }); function clickHandler() { - history.push(`${CLIENT_BASE_PATH}/${SECTION_SLUG.CLONE_TRANSFORM}/${itemId}`); + history.push(`${CLIENT_BASE_PATH}${SECTION_SLUG.CLONE_TRANSFORM}/${itemId}`); } const cloneButton = ( diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/action_delete.test.tsx b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/action_delete.test.tsx index 4795a2eb7d7bc..82b9f0a292bb9 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/action_delete.test.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/action_delete.test.tsx @@ -16,7 +16,6 @@ import { DeleteAction } from './action_delete'; import transformListRow from '../../../../common/__mocks__/transform_list_row.json'; jest.mock('ui/new_platform'); - jest.mock('../../../../../shared_imports'); describe('Transform: Transform List Actions ', () => { diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/action_start.test.tsx b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/action_start.test.tsx index 5f4d4a71c71eb..002b4ea19f967 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/action_start.test.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/action_start.test.tsx @@ -16,7 +16,6 @@ import { StartAction } from './action_start'; import transformListRow from '../../../../common/__mocks__/transform_list_row.json'; jest.mock('ui/new_platform'); - jest.mock('../../../../../shared_imports'); describe('Transform: Transform List Actions ', () => { diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/action_stop.test.tsx b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/action_stop.test.tsx index f6bb1c8b60667..e2a22765dfb98 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/action_stop.test.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/action_stop.test.tsx @@ -16,7 +16,6 @@ import { StopAction } from './action_stop'; import transformListRow from '../../../../common/__mocks__/transform_list_row.json'; jest.mock('ui/new_platform'); - jest.mock('../../../../../shared_imports'); describe('Transform: Transform List Actions ', () => { diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/actions.test.tsx b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/actions.test.tsx index 12e1ba5528c43..e8ac2fa057ad8 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/actions.test.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/actions.test.tsx @@ -6,8 +6,6 @@ import { getActions } from './actions'; -jest.mock('ui/new_platform'); - jest.mock('../../../../../shared_imports'); describe('Transform: Transform List Actions', () => { diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/columns.test.tsx b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/columns.test.tsx index 42f04ed101ad6..b4198ce3c7244 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/columns.test.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/columns.test.tsx @@ -6,8 +6,6 @@ import { getColumns } from './columns'; -jest.mock('ui/new_platform'); - jest.mock('../../../../../shared_imports'); describe('Transform: Job List Columns', () => { diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_list.test.mocks.ts b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_list.test.mocks.ts deleted file mode 100644 index 1d20965526115..0000000000000 --- a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_list.test.mocks.ts +++ /dev/null @@ -1,15 +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 { chromeServiceMock } from '../../../../../../../../../../src/core/public/mocks'; - -jest.doMock('ui/new_platform', () => ({ - npStart: { - core: { - chrome: chromeServiceMock.createStartContract(), - }, - }, -})); diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_list.test.tsx b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_list.test.tsx index e1a19ddd3c742..5e0363d0a7a15 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_list.test.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_list.test.tsx @@ -7,11 +7,8 @@ import { shallow } from 'enzyme'; import React from 'react'; -import './transform_list.test.mocks'; import { TransformList } from './transform_list'; -jest.mock('ui/new_platform'); - jest.mock('../../../../../shared_imports'); describe('Transform: Transform List ', () => { diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/use_refresh_interval.ts b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/use_refresh_interval.ts index 8e505c7cccc02..812e636a3b338 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/use_refresh_interval.ts +++ b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/use_refresh_interval.ts @@ -6,12 +6,7 @@ import React, { useEffect } from 'react'; -import { timefilter } from 'ui/timefilter'; - -import { - DEFAULT_REFRESH_INTERVAL_MS, - MINIMUM_REFRESH_INTERVAL_MS, -} from '../../../../../../common/constants'; +import { DEFAULT_REFRESH_INTERVAL_MS } from '../../../../../../common/constants'; import { useRefreshTransformList } from '../../../../common'; @@ -20,62 +15,11 @@ export const useRefreshInterval = ( ) => { const { refresh } = useRefreshTransformList(); useEffect(() => { - let transformRefreshInterval: null | number = null; - const refreshIntervalSubscription = timefilter - .getRefreshIntervalUpdate$() - .subscribe(setAutoRefresh); - - timefilter.disableTimeRangeSelector(); - timefilter.enableAutoRefreshSelector(); - - initAutoRefresh(); - - function initAutoRefresh() { - const { value } = timefilter.getRefreshInterval(); - if (value === 0) { - // the auto refresher starts in an off state - // so switch it on and set the interval to 30s - timefilter.setRefreshInterval({ - pause: false, - value: DEFAULT_REFRESH_INTERVAL_MS, - }); - } - - setAutoRefresh(); - } - - function setAutoRefresh() { - const { value, pause } = timefilter.getRefreshInterval(); - if (pause) { - clearRefreshInterval(); - } else { - setRefreshInterval(value); - } - refresh(); - } - - function setRefreshInterval(interval: number) { - clearRefreshInterval(); - if (interval >= MINIMUM_REFRESH_INTERVAL_MS) { - setBlockRefresh(false); - const intervalId = window.setInterval(() => { - refresh(); - }, interval); - transformRefreshInterval = intervalId; - } - } - - function clearRefreshInterval() { - setBlockRefresh(true); - if (transformRefreshInterval !== null) { - window.clearInterval(transformRefreshInterval); - } - } + const interval = setInterval(refresh, DEFAULT_REFRESH_INTERVAL_MS); // useEffect cleanup return () => { - refreshIntervalSubscription.unsubscribe(); - clearRefreshInterval(); + clearInterval(interval); }; // custom comparison // eslint-disable-next-line react-hooks/exhaustive-deps diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/transform_management_section.test.tsx b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/transform_management_section.test.tsx index f68670f0b38b2..b1ca9e370c99e 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/transform_management_section.test.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/transform_management_section.test.tsx @@ -9,11 +9,6 @@ import React from 'react'; import { TransformManagementSection } from './transform_management_section'; -jest.mock('ui/new_platform'); -jest.mock('ui/timefilter', () => { - return {}; -}); - jest.mock('../../../shared_imports'); describe('Transform: ', () => { diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/transform_management_section.tsx b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/transform_management_section.tsx index 6eb03c6537e0e..1573d4c53c0cf 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/transform_management_section.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/transform_management_section.tsx @@ -24,12 +24,12 @@ import { } from '@elastic/eui'; import { APP_GET_TRANSFORM_CLUSTER_PRIVILEGES } from '../../../../common/constants'; +import { useDocumentationLinks } from '../../app_dependencies'; import { useRefreshTransformList, TransformListRow } from '../../common'; import { useGetTransforms } from '../../hooks'; import { RedirectToCreateTransform } from '../../common/navigation'; import { PrivilegesWrapper } from '../../lib/authorization'; import { breadcrumbService, docTitleService, BREADCRUMB_SECTION } from '../../services/navigation'; -import { documentationLinksService } from '../../services/documentation'; import { useRefreshInterval } from './components/transform_list/use_refresh_interval'; import { SearchSelection } from './components/search_selection'; @@ -37,6 +37,8 @@ import { TransformList } from './components/transform_list'; import { TransformStatsBar } from './components/transform_list/transforms_stats_bar'; export const TransformManagement: FC = () => { + const { esTransform } = useDocumentationLinks(); + const [transformsLoading, setTransformsLoading] = useState(false); const [isInitialized, setIsInitialized] = useState(false); const [blockRefresh, setBlockRefresh] = useState(false); @@ -98,7 +100,7 @@ export const TransformManagement: FC = () => { > => { - return _sendRequest(httpService.httpClient, config); -}; - -export const useRequest = (config: UseRequestConfig) => { - return _useRequest(httpService.httpClient, config); -}; diff --git a/x-pack/legacy/plugins/transform/public/app/services/http_service.ts b/x-pack/legacy/plugins/transform/public/app/services/http_service.ts index 11ba5119b1394..fa4c8d1ba7844 100644 --- a/x-pack/legacy/plugins/transform/public/app/services/http_service.ts +++ b/x-pack/legacy/plugins/transform/public/app/services/http_service.ts @@ -5,48 +5,47 @@ */ // service for interacting with the server - -import chrome from 'ui/chrome'; - -// @ts-ignore -import { addSystemApiHeader } from 'ui/system_api'; - import { Dictionary } from '../../../common/types/common'; -export function http(options: Dictionary) { - return new Promise((resolve, reject) => { - if (options && options.url) { - let url = ''; - url = url + (options.url || ''); - const headers = addSystemApiHeader({ - 'Content-Type': 'application/json', - 'kbn-version': chrome.getXsrfToken(), - ...options.headers, - }); - - const allHeaders = - options.headers === undefined ? headers : { ...options.headers, ...headers }; - const body = options.data === undefined ? null : JSON.stringify(options.data); - - const payload: Dictionary = { - method: options.method || 'GET', - headers: allHeaders, - credentials: 'same-origin', - }; - - if (body !== null) { - payload.body = body; +export type Http = (options: Dictionary) => Promise; + +export function httpFactory(xsrfToken: string) { + return function http(options: Dictionary) { + return new Promise((resolve, reject) => { + if (options && options.url) { + let url = ''; + url = url + (options.url || ''); + const headers = { + 'kbn-system-request': true, + 'Content-Type': 'application/json', + 'kbn-version': xsrfToken, + ...options.headers, + }; + + const allHeaders = + options.headers === undefined ? headers : { ...options.headers, ...headers }; + const body = options.data === undefined ? null : JSON.stringify(options.data); + + const payload: Dictionary = { + method: options.method || 'GET', + headers: allHeaders, + credentials: 'same-origin', + }; + + if (body !== null) { + payload.body = body; + } + + fetch(url, payload) + .then(resp => { + resp.json().then(resp.ok === true ? resolve : reject); + }) + .catch(resp => { + reject(resp); + }); + } else { + reject(); } - - fetch(url, payload) - .then(resp => { - resp.json().then(resp.ok === true ? resolve : reject); - }) - .catch(resp => { - reject(resp); - }); - } else { - reject(); - } - }); + }); + }; } diff --git a/x-pack/legacy/plugins/transform/public/app/services/navigation/breadcrumb.ts b/x-pack/legacy/plugins/transform/public/app/services/navigation/breadcrumb.ts index 5a2f698b35154..aa8041a1cbe23 100644 --- a/x-pack/legacy/plugins/transform/public/app/services/navigation/breadcrumb.ts +++ b/x-pack/legacy/plugins/transform/public/app/services/navigation/breadcrumb.ts @@ -7,6 +7,10 @@ import { textService } from '../text'; import { linkToHome } from './links'; +import { ManagementAppMountParams } from '../../../../../../../../src/plugins/management/public'; + +type SetBreadcrumbs = ManagementAppMountParams['setBreadcrumbs']; + export enum BREADCRUMB_SECTION { MANAGEMENT = 'management', HOME = 'home', @@ -24,17 +28,16 @@ type Breadcrumbs = { }; class BreadcrumbService { - private chrome: any; private breadcrumbs: Breadcrumbs = { management: [], home: [], cloneTransform: [], createTransform: [], }; + private setBreadcrumbsHandler?: SetBreadcrumbs; - public init(chrome: any, managementBreadcrumb: any): void { - this.chrome = chrome; - this.breadcrumbs.management = [managementBreadcrumb]; + public setup(setBreadcrumbsHandler: SetBreadcrumbs): void { + this.setBreadcrumbsHandler = setBreadcrumbsHandler; // Home and sections this.breadcrumbs.home = [ @@ -59,12 +62,19 @@ class BreadcrumbService { } public setBreadcrumbs(type: BREADCRUMB_SECTION): void { + if (!this.setBreadcrumbsHandler) { + throw new Error(`BreadcrumbService#setup() must be called first!`); + } + const newBreadcrumbs = this.breadcrumbs[type] ? [...this.breadcrumbs[type]] : [...this.breadcrumbs.home]; // Pop off last breadcrumb - const lastBreadcrumb = newBreadcrumbs.pop() as BreadcrumbItem; + const lastBreadcrumb = newBreadcrumbs.pop() as { + text: string; + href?: string; + }; // Put last breadcrumb back without href newBreadcrumbs.push({ @@ -72,7 +82,7 @@ class BreadcrumbService { href: undefined, }); - this.chrome.setBreadcrumbs(newBreadcrumbs); + this.setBreadcrumbsHandler(newBreadcrumbs); } } diff --git a/x-pack/legacy/plugins/transform/public/plugin.ts b/x-pack/legacy/plugins/transform/public/plugin.ts index d55695f891bb7..23fad00fb0786 100644 --- a/x-pack/legacy/plugins/transform/public/plugin.ts +++ b/x-pack/legacy/plugins/transform/public/plugin.ts @@ -3,121 +3,89 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { unmountComponentAtNode } from 'react-dom'; import { i18n } from '@kbn/i18n'; -import { PLUGIN } from '../common/constants'; -import { CLIENT_BASE_PATH } from './app/constants'; -import { renderReact } from './app/app'; -import { Core, Plugins } from './shim'; +import { renderApp } from './app/app'; +import { ShimCore, ShimPlugins } from './shim'; -import { breadcrumbService, docTitleService } from './app/services/navigation'; -import { documentationLinksService } from './app/services/documentation'; -import { httpService } from './app/services/http'; +import { breadcrumbService } from './app/services/navigation'; +import { docTitleService } from './app/services/navigation'; import { textService } from './app/services/text'; import { uiMetricService } from './app/services/ui_metric'; import { createSavedSearchesLoader } from '../../../../../src/plugins/discover/public'; -const REACT_ROOT_ID = 'transformReactRoot'; -const KBN_MANAGEMENT_SECTION = 'elasticsearch/transform'; - -const template = `
`; - export class Plugin { - public start(core: Core, plugins: Plugins): void { + public start(core: ShimCore, plugins: ShimPlugins): void { const { http, - routing, - legacyHttp, chrome, documentation, + docLinks, docTitle, + injectedMetadata, + notifications, uiSettings, savedObjects, overlays, } = core; - const { data, management, savedSearches: coreSavedSearches, uiMetric } = plugins; + const { data, management, savedSearches: coreSavedSearches, uiMetric, xsrfToken } = plugins; // AppCore/AppPlugins to be passed on as React context const appDependencies = { - core: { chrome, http, i18n: core.i18n, uiSettings, savedObjects, overlays }, + core: { + chrome, + documentation, + docLinks, + http, + i18n: core.i18n, + injectedMetadata, + notifications, + uiSettings, + savedObjects, + overlays, + }, plugins: { data, - management: { sections: management.sections }, + management, savedSearches: coreSavedSearches, + xsrfToken, }, }; // Register management section const esSection = management.sections.getSection('elasticsearch'); - esSection.register(PLUGIN.ID, { - visible: true, - display: i18n.translate('xpack.transform.appName', { - defaultMessage: 'Transforms', - }), - order: 3, - url: `#${CLIENT_BASE_PATH}`, - }); + if (esSection !== undefined) { + esSection.registerApp({ + id: 'transform', + title: i18n.translate('xpack.transform.appTitle', { + defaultMessage: 'Transforms', + }), + order: 3, + mount(params) { + const savedSearches = createSavedSearchesLoader({ + savedObjectsClient: core.savedObjects.client, + indexPatterns: plugins.data.indexPatterns, + chrome: core.chrome, + overlays: core.overlays, + }); + coreSavedSearches.setClient(savedSearches); + + breadcrumbService.setup(params.setBreadcrumbs); + params.setBreadcrumbs([ + { + text: i18n.translate('xpack.transform.breadcrumbsTitle', { + defaultMessage: 'Transforms', + }), + }, + ]); + + return renderApp(params.element, appDependencies); + }, + }); + } // Initialize services textService.init(); - breadcrumbService.init(chrome, management.constants.BREADCRUMB); uiMetricService.init(uiMetric.createUiStatsReporter); - documentationLinksService.init(documentation.esDocBasePath); docTitleService.init(docTitle.change); - - const unmountReactApp = (): void => { - const elem = document.getElementById(REACT_ROOT_ID); - if (elem) { - unmountComponentAtNode(elem); - } - }; - - // Register react root - routing.registerAngularRoute(`${CLIENT_BASE_PATH}/:section?/:subsection?/:view?/:id?`, { - template, - controllerAs: 'transformController', - controller: ($scope: any, $route: any, $http: ng.IHttpService) => { - const savedSearches = createSavedSearchesLoader({ - savedObjectsClient: core.savedObjects.client, - indexPatterns: plugins.data.indexPatterns, - chrome: core.chrome, - overlays: core.overlays, - }); - // NOTE: We depend upon Angular's $http service because it's decorated with interceptors, - // e.g. to check license status per request. - legacyHttp.setClient($http); - httpService.init(legacyHttp.getClient()); - coreSavedSearches.setClient(savedSearches); - - // Angular Lifecycle - const appRoute = $route.current; - const stopListeningForLocationChange = $scope.$on('$locationChangeSuccess', () => { - const currentRoute = $route.current; - const isNavigationInApp = currentRoute.$$route.template === appRoute.$$route.template; - - // When we navigate within Transform, prevent Angular from re-matching the route and rebuild the app - if (isNavigationInApp) { - $route.current = appRoute; - } else { - // Any clean up when user leaves Transform - } - - $scope.$on('$destroy', () => { - if (stopListeningForLocationChange) { - stopListeningForLocationChange(); - } - unmountReactApp(); - }); - }); - - $scope.$$postDigest(() => { - unmountReactApp(); - const elem = document.getElementById(REACT_ROOT_ID); - if (elem) { - renderReact(elem, appDependencies); - } - }); - }, - }); } } diff --git a/x-pack/legacy/plugins/transform/public/shared_imports.ts b/x-pack/legacy/plugins/transform/public/shared_imports.ts index 248eb00c67dff..b077cd8836c4b 100644 --- a/x-pack/legacy/plugins/transform/public/shared_imports.ts +++ b/x-pack/legacy/plugins/transform/public/shared_imports.ts @@ -16,7 +16,7 @@ export { UseRequestConfig, sendRequest, useRequest, -} from '../../../../../src/plugins/es_ui_shared/public/request'; +} from '../../../../../src/plugins/es_ui_shared/public/request/np_ready_request'; export { CronEditor, diff --git a/x-pack/legacy/plugins/transform/public/shim.ts b/x-pack/legacy/plugins/transform/public/shim.ts index 38bb072ff9eb7..95f54605377a8 100644 --- a/x-pack/legacy/plugins/transform/public/shim.ts +++ b/x-pack/legacy/plugins/transform/public/shim.ts @@ -6,77 +6,69 @@ import { npStart } from 'ui/new_platform'; -import { management, MANAGEMENT_BREADCRUMB } from 'ui/management'; -import routes from 'ui/routes'; +import chrome from 'ui/chrome'; import { docTitle } from 'ui/doc_title/doc_title'; -import { CoreStart } from 'kibana/public'; // @ts-ignore: allow traversal to fail on x-pack build import { createUiStatsReporter } from '../../../../../src/legacy/core_plugins/ui_metric/public'; import { SavedSearchLoader } from '../../../../../src/legacy/core_plugins/kibana/public/discover/np_ready/types'; -import { DataPublicPluginStart } from '../../../../../src/plugins/data/public'; -export type npCore = typeof npStart.core; +import { TRANSFORM_DOC_PATHS } from './app/constants'; + +export type NpCore = typeof npStart.core; +export type NpPlugins = typeof npStart.plugins; // AppCore/AppPlugins is the set of core features/plugins // we pass on via context/hooks to the app and its components. export type AppCore = Pick< - CoreStart, - 'chrome' | 'http' | 'i18n' | 'savedObjects' | 'uiSettings' | 'overlays' + ShimCore, + | 'chrome' + | 'documentation' + | 'docLinks' + | 'http' + | 'i18n' + | 'injectedMetadata' + | 'savedObjects' + | 'uiSettings' + | 'overlays' + | 'notifications' >; - -export interface AppPlugins { - data: DataPublicPluginStart; - management: { - sections: typeof management; - }; - savedSearches: { - getClient(): any; - setClient(client: any): void; - }; -} +export type AppPlugins = Pick; export interface AppDependencies { core: AppCore; plugins: AppPlugins; } -export interface Core extends npCore { - legacyHttp: { - getClient(): any; - setClient(client: any): void; - }; - routing: { - registerAngularRoute(path: string, config: object): void; - }; - documentation: { - esDocBasePath: string; - esPluginDocBasePath: string; - esStackOverviewDocBasePath: string; - esMLDocBasePath: string; - }; +export interface ShimCore extends NpCore { + documentation: Record< + | 'esDocBasePath' + | 'esIndicesCreateIndex' + | 'esPluginDocBasePath' + | 'esQueryDsl' + | 'esStackOverviewDocBasePath' + | 'esTransform' + | 'esTransformPivot' + | 'mlDocBasePath', + string + >; docTitle: { change: typeof docTitle.change; }; } -export interface Plugins extends AppPlugins { - management: { - sections: typeof management; - constants: { - BREADCRUMB: typeof MANAGEMENT_BREADCRUMB; - }; - }; +export interface ShimPlugins extends NpPlugins { uiMetric: { createUiStatsReporter: typeof createUiStatsReporter; }; - data: DataPublicPluginStart; + savedSearches: { + getClient(): any; + setClient(client: any): void; + }; + xsrfToken: string; } -export function createPublicShim(): { core: Core; plugins: Plugins } { - // This is an Angular service, which is why we use this provider pattern - // to access it within our React app. - let httpClient: ng.IHttpService; +export function createPublicShim(): { core: ShimCore; plugins: ShimPlugins } { // This is an Angular service, which is why we use this provider pattern // to access it within our React app. let savedSearches: SavedSearchLoader; @@ -86,35 +78,22 @@ export function createPublicShim(): { core: Core; plugins: Plugins } { return { core: { ...npStart.core, - routing: { - registerAngularRoute: (path: string, config: object): void => { - routes.when(path, config); - }, - }, - legacyHttp: { - setClient: (client: any): void => { - httpClient = client; - }, - getClient: (): any => httpClient, - }, documentation: { esDocBasePath: `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/reference/${DOC_LINK_VERSION}/`, + esIndicesCreateIndex: `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/reference/${DOC_LINK_VERSION}/indices-create-index.html#indices-create-index`, esPluginDocBasePath: `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/plugins/${DOC_LINK_VERSION}/`, + esQueryDsl: `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/reference/${DOC_LINK_VERSION}/query-dsl.html`, esStackOverviewDocBasePath: `${ELASTIC_WEBSITE_URL}guide/en/elastic-stack-overview/${DOC_LINK_VERSION}/`, - esMLDocBasePath: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/`, + esTransform: `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/reference/${DOC_LINK_VERSION}/${TRANSFORM_DOC_PATHS.transforms}`, + esTransformPivot: `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/reference/${DOC_LINK_VERSION}/put-transform.html#put-transform-request-body`, + mlDocBasePath: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/`, }, docTitle: { change: docTitle.change, }, }, plugins: { - data: npStart.plugins.data, - management: { - sections: management, - constants: { - BREADCRUMB: MANAGEMENT_BREADCRUMB, - }, - }, + ...npStart.plugins, savedSearches: { setClient: (client: any): void => { savedSearches = client; @@ -124,6 +103,7 @@ export function createPublicShim(): { core: Core; plugins: Plugins } { uiMetric: { createUiStatsReporter, }, + xsrfToken: chrome.getXsrfToken(), }, }; } diff --git a/x-pack/plugins/apm/server/lib/index_pattern/get_dynamic_index_pattern.ts b/x-pack/plugins/apm/server/lib/index_pattern/get_dynamic_index_pattern.ts index b1e4906317f81..a7476bf564a16 100644 --- a/x-pack/plugins/apm/server/lib/index_pattern/get_dynamic_index_pattern.ts +++ b/x-pack/plugins/apm/server/lib/index_pattern/get_dynamic_index_pattern.ts @@ -62,8 +62,7 @@ export const getDynamicIndexPattern = async ({ cache.set(CACHE_KEY, undefined); const notExists = e.output?.statusCode === 404; if (notExists) { - // eslint-disable-next-line no-console - console.error( + context.logger.error( `Could not get dynamic index pattern because indices "${indexPatternTitle}" don't exist` ); return; diff --git a/x-pack/plugins/apm/server/lib/settings/agent_configuration/create_agent_config_index.ts b/x-pack/plugins/apm/server/lib/settings/agent_configuration/create_agent_config_index.ts index af2d2a13eaa2f..8cfb7e7edb4c6 100644 --- a/x-pack/plugins/apm/server/lib/settings/agent_configuration/create_agent_config_index.ts +++ b/x-pack/plugins/apm/server/lib/settings/agent_configuration/create_agent_config_index.ts @@ -4,17 +4,19 @@ * you may not use this file except in compliance with the Elastic License. */ -import { IClusterClient } from 'src/core/server'; +import { IClusterClient, Logger } from 'src/core/server'; import { CallCluster } from 'src/legacy/core_plugins/elasticsearch'; import { APMConfig } from '../../..'; import { getApmIndicesConfig } from '../apm_indices/get_apm_indices'; export async function createApmAgentConfigurationIndex({ esClient, - config + config, + logger }: { esClient: IClusterClient; config: APMConfig; + logger: Logger; }) { try { const index = getApmIndicesConfig(config).apmAgentConfigurationIndex; @@ -32,8 +34,7 @@ export async function createApmAgentConfigurationIndex({ ); } } catch (e) { - // eslint-disable-next-line no-console - console.error('Could not create APM Agent configuration:', e.message); + logger.error(`Could not create APM Agent configuration: ${e.message}`); } } diff --git a/x-pack/plugins/apm/server/plugin.ts b/x-pack/plugins/apm/server/plugin.ts index adc80cb43620b..773f0d4e6fac5 100644 --- a/x-pack/plugins/apm/server/plugin.ts +++ b/x-pack/plugins/apm/server/plugin.ts @@ -5,12 +5,12 @@ */ import { PluginInitializerContext, Plugin, CoreSetup } from 'src/core/server'; import { Observable, combineLatest, AsyncSubject } from 'rxjs'; -import { map } from 'rxjs/operators'; +import { map, take } from 'rxjs/operators'; import { Server } from 'hapi'; import { once } from 'lodash'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; +import { APMOSSPluginSetup } from '../../../../src/plugins/apm_oss/server'; import { makeApmUsageCollector } from './lib/apm_telemetry'; -import { Plugin as APMOSSPlugin } from '../../../../src/plugins/apm_oss/server'; import { createApmAgentConfigurationIndex } from './lib/settings/agent_configuration/create_agent_config_index'; import { createApmApi } from './routes/create_apm_api'; import { getApmIndices } from './lib/settings/apm_indices/get_apm_indices'; @@ -33,26 +33,23 @@ export interface APMPluginContract { export class APMPlugin implements Plugin { legacySetup$: AsyncSubject; - currentConfig: APMConfig; constructor(private readonly initContext: PluginInitializerContext) { this.initContext = initContext; this.legacySetup$ = new AsyncSubject(); - this.currentConfig = {} as APMConfig; } public async setup( core: CoreSetup, plugins: { - apm_oss: APMOSSPlugin extends Plugin ? TSetup : never; + apm_oss: APMOSSPluginSetup; home: HomeServerPluginSetup; licensing: LicensingPluginSetup; cloud?: CloudSetup; usageCollection?: UsageCollectionSetup; } ) { - const config$ = this.initContext.config.create(); const logger = this.initContext.logger.get('apm'); - + const config$ = this.initContext.config.create(); const mergedConfig$ = combineLatest(plugins.apm_oss.config$, config$).pipe( map(([apmOssConfig, apmConfig]) => mergeConfigs(apmOssConfig, apmConfig)) ); @@ -61,28 +58,26 @@ export class APMPlugin implements Plugin { createApmApi().init(core, { config$: mergedConfig$, logger, __LEGACY }); }); - await new Promise(resolve => { - mergedConfig$.subscribe(async config => { - this.currentConfig = config; - await createApmAgentConfigurationIndex({ - esClient: core.elasticsearch.dataClient, - config - }); - resolve(); - }); + const currentConfig = await mergedConfig$.pipe(take(1)).toPromise(); + + // create agent configuration index without blocking setup lifecycle + createApmAgentConfigurationIndex({ + esClient: core.elasticsearch.dataClient, + config: currentConfig, + logger }); plugins.home.tutorials.registerTutorial( tutorialProvider({ - isEnabled: this.currentConfig['xpack.apm.ui.enabled'], - indexPatternTitle: this.currentConfig['apm_oss.indexPattern'], + isEnabled: currentConfig['xpack.apm.ui.enabled'], + indexPatternTitle: currentConfig['apm_oss.indexPattern'], cloud: plugins.cloud, indices: { - errorIndices: this.currentConfig['apm_oss.errorIndices'], - metricsIndices: this.currentConfig['apm_oss.metricsIndices'], - onboardingIndices: this.currentConfig['apm_oss.onboardingIndices'], - sourcemapIndices: this.currentConfig['apm_oss.sourcemapIndices'], - transactionIndices: this.currentConfig['apm_oss.transactionIndices'] + errorIndices: currentConfig['apm_oss.errorIndices'], + metricsIndices: currentConfig['apm_oss.metricsIndices'], + onboardingIndices: currentConfig['apm_oss.onboardingIndices'], + sourcemapIndices: currentConfig['apm_oss.sourcemapIndices'], + transactionIndices: currentConfig['apm_oss.transactionIndices'] } }) ); @@ -108,7 +103,7 @@ export class APMPlugin implements Plugin { getApmIndices: async () => getApmIndices({ savedObjectsClient: await getInternalSavedObjectsClient(core), - config: this.currentConfig + config: currentConfig }) }; } diff --git a/x-pack/plugins/case/server/routes/api/__fixtures__/authc_mock.ts b/x-pack/plugins/case/server/routes/api/__fixtures__/authc_mock.ts index 94ce9627b9ac6..17a2518482637 100644 --- a/x-pack/plugins/case/server/routes/api/__fixtures__/authc_mock.ts +++ b/x-pack/plugins/case/server/routes/api/__fixtures__/authc_mock.ts @@ -3,33 +3,22 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { Authentication } from '../../../../../security/server'; +import { AuthenticatedUser } from '../../../../../security/server'; +import { securityMock } from '../../../../../security/server/mocks'; -const getCurrentUser = jest.fn().mockReturnValue({ - username: 'awesome', - full_name: 'Awesome D00d', -}); -const getCurrentUserThrow = jest.fn().mockImplementation(() => { - throw new Error('Bad User - the user is not authenticated'); -}); +function createAuthenticationMock({ + currentUser, +}: { currentUser?: AuthenticatedUser | null } = {}) { + const { authc } = securityMock.createSetup(); + authc.getCurrentUser.mockReturnValue( + currentUser !== undefined + ? currentUser + : ({ username: 'awesome', full_name: 'Awesome D00d' } as AuthenticatedUser) + ); + return authc; +} export const authenticationMock = { - create: (): jest.Mocked => ({ - login: jest.fn(), - createAPIKey: jest.fn(), - getCurrentUser, - invalidateAPIKey: jest.fn(), - isAuthenticated: jest.fn(), - logout: jest.fn(), - getSessionInfo: jest.fn(), - }), - createInvalid: (): jest.Mocked => ({ - login: jest.fn(), - createAPIKey: jest.fn(), - getCurrentUser: getCurrentUserThrow, - invalidateAPIKey: jest.fn(), - isAuthenticated: jest.fn(), - logout: jest.fn(), - getSessionInfo: jest.fn(), - }), + create: () => createAuthenticationMock(), + createInvalid: () => createAuthenticationMock({ currentUser: null }), }; diff --git a/x-pack/plugins/case/server/services/index.ts b/x-pack/plugins/case/server/services/index.ts index d6d4bd606676c..e6416e268e30b 100644 --- a/x-pack/plugins/case/server/services/index.ts +++ b/x-pack/plugins/case/server/services/index.ts @@ -149,14 +149,8 @@ export class CaseService { } }, getUser: async ({ request, response }: GetUserArgs) => { - let user; - try { - this.log.debug(`Attempting to authenticate a user`); - user = await authentication!.getCurrentUser(request); - } catch (error) { - this.log.debug(`Error on GET user: ${error}`); - throw error; - } + this.log.debug(`Attempting to authenticate a user`); + const user = authentication!.getCurrentUser(request); if (!user) { this.log.debug(`Error on GET user: Bad User`); throw new Error('Bad User - the user is not authenticated'); diff --git a/x-pack/plugins/console_extensions/server/spec/generated/monitoring.bulk.json b/x-pack/plugins/console_extensions/server/spec/generated/monitoring.bulk.json index 2b27950e7b097..26a9078f73ce8 100644 --- a/x-pack/plugins/console_extensions/server/spec/generated/monitoring.bulk.json +++ b/x-pack/plugins/console_extensions/server/spec/generated/monitoring.bulk.json @@ -12,6 +12,6 @@ "patterns": [ "_monitoring/bulk" ], - "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/master/es-monitoring.html" + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/master/monitor-elasticsearch-cluster.html" } } diff --git a/x-pack/plugins/features/server/index.ts b/x-pack/plugins/features/server/index.ts index 2b4f85aa04f04..48ef97a494f7e 100644 --- a/x-pack/plugins/features/server/index.ts +++ b/x-pack/plugins/features/server/index.ts @@ -14,7 +14,7 @@ import { Plugin } from './plugin'; export { uiCapabilitiesRegex } from './feature_schema'; export { Feature, FeatureWithAllOrReadPrivileges, FeatureKibanaPrivileges } from '../common'; -export { PluginSetupContract } from './plugin'; +export { PluginSetupContract, PluginStartContract } from './plugin'; export const plugin = (initializerContext: PluginInitializerContext) => new Plugin(initializerContext); diff --git a/x-pack/plugins/features/server/mocks.ts b/x-pack/plugins/features/server/mocks.ts new file mode 100644 index 0000000000000..ebaa5f1a504ca --- /dev/null +++ b/x-pack/plugins/features/server/mocks.ts @@ -0,0 +1,27 @@ +/* + * 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 { PluginSetupContract, PluginStartContract } from './plugin'; + +const createSetup = (): jest.Mocked => { + return { + getFeatures: jest.fn(), + getFeaturesUICapabilities: jest.fn(), + registerFeature: jest.fn(), + registerLegacyAPI: jest.fn(), + }; +}; + +const createStart = (): jest.Mocked => { + return { + getFeatures: jest.fn(), + }; +}; + +export const featuresPluginMock = { + createSetup, + createStart, +}; diff --git a/x-pack/plugins/features/server/plugin.ts b/x-pack/plugins/features/server/plugin.ts index 96a8e68f8326d..e77fa218c0681 100644 --- a/x-pack/plugins/features/server/plugin.ts +++ b/x-pack/plugins/features/server/plugin.ts @@ -30,6 +30,10 @@ export interface PluginSetupContract { registerLegacyAPI: (legacyAPI: LegacyAPI) => void; } +export interface PluginStartContract { + getFeatures(): Feature[]; +} + /** * Describes a set of APIs that are available in the legacy platform only and required by this plugin * to function properly. @@ -45,6 +49,8 @@ export interface LegacyAPI { export class Plugin { private readonly logger: Logger; + private readonly featureRegistry: FeatureRegistry = new FeatureRegistry(); + private legacyAPI?: LegacyAPI; private readonly getLegacyAPI = () => { if (!this.legacyAPI) { @@ -61,18 +67,16 @@ export class Plugin { core: CoreSetup, { timelion }: { timelion?: TimelionSetupContract } ): Promise> { - const featureRegistry = new FeatureRegistry(); - defineRoutes({ router: core.http.createRouter(), - featureRegistry, + featureRegistry: this.featureRegistry, getLegacyAPI: this.getLegacyAPI, }); return deepFreeze({ - registerFeature: featureRegistry.register.bind(featureRegistry), - getFeatures: featureRegistry.getAll.bind(featureRegistry), - getFeaturesUICapabilities: () => uiCapabilitiesForFeatures(featureRegistry.getAll()), + registerFeature: this.featureRegistry.register.bind(this.featureRegistry), + getFeatures: this.featureRegistry.getAll.bind(this.featureRegistry), + getFeaturesUICapabilities: () => uiCapabilitiesForFeatures(this.featureRegistry.getAll()), registerLegacyAPI: (legacyAPI: LegacyAPI) => { this.legacyAPI = legacyAPI; @@ -82,14 +86,17 @@ export class Plugin { savedObjectTypes: this.legacyAPI.savedObjectTypes, includeTimelion: timelion !== undefined && timelion.uiEnabled, })) { - featureRegistry.register(feature); + this.featureRegistry.register(feature); } }, }); } - public start() { + public start(): RecursiveReadonly { this.logger.debug('Starting plugin'); + return deepFreeze({ + getFeatures: this.featureRegistry.getAll.bind(this.featureRegistry), + }); } public stop() { diff --git a/x-pack/plugins/infra/kibana.json b/x-pack/plugins/infra/kibana.json index 7ba81127c1e67..f12bbd6cf7723 100644 --- a/x-pack/plugins/infra/kibana.json +++ b/x-pack/plugins/infra/kibana.json @@ -2,7 +2,17 @@ "id": "infra", "version": "8.0.0", "kibanaVersion": "kibana", - "requiredPlugins": ["features", "apm", "usageCollection", "spaces", "home", "data", "data_enhanced", "metrics"], + "requiredPlugins": [ + "features", + "apm", + "usageCollection", + "spaces", + "home", + "data", + "data_enhanced", + "metrics", + "alerting" + ], "server": true, "ui": true, "configPath": ["xpack", "infra"] diff --git a/x-pack/plugins/infra/server/lib/adapters/framework/adapter_types.ts b/x-pack/plugins/infra/server/lib/adapters/framework/adapter_types.ts index f8177365c9bdd..c7ac577dd8a81 100644 --- a/x-pack/plugins/infra/server/lib/adapters/framework/adapter_types.ts +++ b/x-pack/plugins/infra/server/lib/adapters/framework/adapter_types.ts @@ -13,6 +13,7 @@ import { SpacesPluginSetup } from '../../../../../../plugins/spaces/server'; import { VisTypeTimeseriesSetup } from '../../../../../../../src/plugins/vis_type_timeseries/server'; import { APMPluginContract } from '../../../../../../plugins/apm/server'; import { HomeServerPluginSetup } from '../../../../../../../src/plugins/home/server'; +import { PluginSetupContract as AlertingPluginContract } from '../../../../../../plugins/alerting/server'; // NP_TODO: Compose real types from plugins we depend on, no "any" export interface InfraServerPluginDeps { @@ -25,6 +26,7 @@ export interface InfraServerPluginDeps { }; features: FeaturesPluginSetup; apm: APMPluginContract; + alerting: AlertingPluginContract; } export interface CallWithRequestParams extends GenericParams { diff --git a/x-pack/legacy/plugins/transform/public/app/services/http/index.ts b/x-pack/plugins/infra/server/lib/alerting/index.ts similarity index 79% rename from x-pack/legacy/plugins/transform/public/app/services/http/index.ts rename to x-pack/plugins/infra/server/lib/alerting/index.ts index a129998e48e1f..90287d8f219f9 100644 --- a/x-pack/legacy/plugins/transform/public/app/services/http/index.ts +++ b/x-pack/plugins/infra/server/lib/alerting/index.ts @@ -3,4 +3,5 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -export { httpService } from './http'; + +export { registerAlertTypes } from './register_alert_types'; diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_alert_type.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_alert_type.ts new file mode 100644 index 0000000000000..9bc54c1a49c8f --- /dev/null +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_alert_type.ts @@ -0,0 +1,132 @@ +/* + * 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 uuid from 'uuid'; +import { i18n } from '@kbn/i18n'; +import { schema } from '@kbn/config-schema'; +import { + MetricThresholdAlertTypeParams, + Comparator, + AlertStates, + METRIC_THRESHOLD_ALERT_TYPE_ID, +} from './types'; +import { AlertServices, PluginSetupContract } from '../../../../../alerting/server'; + +const FIRED_ACTIONS = { + id: 'metrics.threshold.fired', + name: i18n.translate('xpack.infra.metrics.alerting.threshold.fired', { + defaultMessage: 'Fired', + }), +}; + +async function getMetric( + { callCluster }: AlertServices, + { metric, aggType, timeUnit, timeSize, indexPattern }: MetricThresholdAlertTypeParams +) { + const interval = `${timeSize}${timeUnit}`; + const searchBody = { + query: { + bool: { + filter: [ + { + range: { + '@timestamp': { + gte: `now-${interval}`, + }, + }, + exists: { + field: metric, + }, + }, + ], + }, + }, + size: 0, + aggs: { + aggregatedIntervals: { + date_histogram: { + field: '@timestamp', + fixed_interval: interval, + }, + aggregations: { + aggregatedValue: { + [aggType]: { + field: metric, + }, + }, + }, + }, + }, + }; + + const result = await callCluster('search', { + body: searchBody, + index: indexPattern, + }); + + const { buckets } = result.aggregations.aggregatedIntervals; + const { value } = buckets[buckets.length - 1].aggregatedValue; + return value; +} + +const comparatorMap = { + [Comparator.BETWEEN]: (value: number, [a, b]: number[]) => + value >= Math.min(a, b) && value <= Math.max(a, b), + // `threshold` is always an array of numbers in case the BETWEEN comparator is + // used; all other compartors will just destructure the first value in the array + [Comparator.GT]: (a: number, [b]: number[]) => a > b, + [Comparator.LT]: (a: number, [b]: number[]) => a < b, + [Comparator.GT_OR_EQ]: (a: number, [b]: number[]) => a >= b, + [Comparator.LT_OR_EQ]: (a: number, [b]: number[]) => a <= b, +}; + +export async function registerMetricThresholdAlertType(alertingPlugin: PluginSetupContract) { + if (!alertingPlugin) { + throw new Error( + 'Cannot register metric threshold alert type. Both the actions and alerting plugins need to be enabled.' + ); + } + const alertUUID = uuid.v4(); + + alertingPlugin.registerType({ + id: METRIC_THRESHOLD_ALERT_TYPE_ID, + name: 'Metric Alert - Threshold', + validate: { + params: schema.object({ + threshold: schema.arrayOf(schema.number()), + comparator: schema.string(), + aggType: schema.string(), + metric: schema.string(), + timeUnit: schema.string(), + timeSize: schema.number(), + indexPattern: schema.string(), + }), + }, + defaultActionGroupId: FIRED_ACTIONS.id, + actionGroups: [FIRED_ACTIONS], + async executor({ services, params }) { + const { threshold, comparator } = params as MetricThresholdAlertTypeParams; + const alertInstance = services.alertInstanceFactory(alertUUID); + const currentValue = await getMetric(services, params as MetricThresholdAlertTypeParams); + if (typeof currentValue === 'undefined') + throw new Error('Could not get current value of metric'); + + const comparisonFunction = comparatorMap[comparator]; + + const isValueInAlertState = comparisonFunction(currentValue, threshold); + + if (isValueInAlertState) { + alertInstance.scheduleActions(FIRED_ACTIONS.id, { + value: currentValue, + }); + } + + // Future use: ability to fetch display current alert state + alertInstance.replaceState({ + alertState: isValueInAlertState ? AlertStates.ALERT : AlertStates.OK, + }); + }, + }); +} diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/types.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/types.ts new file mode 100644 index 0000000000000..2618469ff693c --- /dev/null +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/types.ts @@ -0,0 +1,34 @@ +/* + * 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 { MetricsExplorerAggregation } from '../../../../common/http_api/metrics_explorer'; + +export const METRIC_THRESHOLD_ALERT_TYPE_ID = 'metrics.alert.threshold'; + +export enum Comparator { + GT = '>', + LT = '<', + GT_OR_EQ = '>=', + LT_OR_EQ = '<=', + BETWEEN = 'between', +} + +export enum AlertStates { + OK, + ALERT, +} + +export type TimeUnit = 's' | 'm' | 'h' | 'd'; + +export interface MetricThresholdAlertTypeParams { + aggType: MetricsExplorerAggregation; + metric: string; + timeSize: number; + timeUnit: TimeUnit; + indexPattern: string; + threshold: number[]; + comparator: Comparator; +} diff --git a/x-pack/plugins/infra/server/lib/alerting/register_alert_types.ts b/x-pack/plugins/infra/server/lib/alerting/register_alert_types.ts new file mode 100644 index 0000000000000..6ec6f31256b78 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/alerting/register_alert_types.ts @@ -0,0 +1,20 @@ +/* + * 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 { PluginSetupContract } from '../../../../alerting/server'; +import { registerMetricThresholdAlertType } from './metric_threshold/register_metric_threshold_alert_type'; + +const registerAlertTypes = (alertingPlugin: PluginSetupContract) => { + if (alertingPlugin) { + const registerFns = [registerMetricThresholdAlertType]; + + registerFns.forEach(fn => { + fn(alertingPlugin); + }); + } +}; + +export { registerAlertTypes }; diff --git a/x-pack/plugins/infra/server/lib/snapshot/create_timerange_with_interval.ts b/x-pack/plugins/infra/server/lib/snapshot/create_timerange_with_interval.ts index 6f036475a1e1c..cf2b1e59b2a22 100644 --- a/x-pack/plugins/infra/server/lib/snapshot/create_timerange_with_interval.ts +++ b/x-pack/plugins/infra/server/lib/snapshot/create_timerange_with_interval.ts @@ -21,7 +21,7 @@ export const createTimeRangeWithInterval = async ( ): Promise => { const aggregations = getMetricsAggregations(options); const modules = await aggregationsToModules(framework, requestContext, aggregations, options); - const interval = + const interval = Math.max( (await calculateMetricInterval( framework, requestContext, @@ -32,7 +32,9 @@ export const createTimeRangeWithInterval = async ( }, modules, options.nodeType - )) || 60000; + )) || 60, + 60 + ); return { interval: `${interval}s`, from: options.timerange.to - interval * 5000, // We need at least 5 buckets worth of data diff --git a/x-pack/plugins/infra/server/plugin.ts b/x-pack/plugins/infra/server/plugin.ts index bcdbccf6f2294..64fc496f3597e 100644 --- a/x-pack/plugins/infra/server/plugin.ts +++ b/x-pack/plugins/infra/server/plugin.ts @@ -27,6 +27,7 @@ import { InfraServerPluginDeps } from './lib/adapters/framework'; import { METRICS_FEATURE, LOGS_FEATURE } from './features'; import { UsageCollector } from './usage/usage_collector'; import { InfraStaticSourceConfiguration } from './lib/sources/types'; +import { registerAlertTypes } from './lib/alerting'; export const config = { schema: schema.object({ @@ -146,6 +147,7 @@ export class InfraServerPlugin { ]); initInfraServer(this.libs); + registerAlertTypes(plugins.alerting); // Telemetry UsageCollector.registerUsageCollector(plugins.usageCollection); diff --git a/x-pack/plugins/ml/kibana.json b/x-pack/plugins/ml/kibana.json new file mode 100644 index 0000000000000..e944af6821c0b --- /dev/null +++ b/x-pack/plugins/ml/kibana.json @@ -0,0 +1,9 @@ +{ + "id": "ml", + "version": "0.0.1", + "kibanaVersion": "kibana", + "configPath": ["ml"], + "requiredPlugins": ["cloud", "features", "home", "licensing", "security", "spaces", "usageCollection"], + "server": true, + "ui": false +} diff --git a/x-pack/legacy/plugins/ml/server/client/__tests__/elasticsearch_ml.js b/x-pack/plugins/ml/server/client/__tests__/elasticsearch_ml.js similarity index 100% rename from x-pack/legacy/plugins/ml/server/client/__tests__/elasticsearch_ml.js rename to x-pack/plugins/ml/server/client/__tests__/elasticsearch_ml.js diff --git a/x-pack/legacy/plugins/ml/server/client/elasticsearch_ml.js b/x-pack/plugins/ml/server/client/elasticsearch_ml.js similarity index 100% rename from x-pack/legacy/plugins/ml/server/client/elasticsearch_ml.js rename to x-pack/plugins/ml/server/client/elasticsearch_ml.js diff --git a/x-pack/legacy/plugins/ml/server/client/error_wrapper.ts b/x-pack/plugins/ml/server/client/error_wrapper.ts similarity index 100% rename from x-pack/legacy/plugins/ml/server/client/error_wrapper.ts rename to x-pack/plugins/ml/server/client/error_wrapper.ts diff --git a/x-pack/legacy/plugins/ml/server/client/errors.js b/x-pack/plugins/ml/server/client/errors.js similarity index 100% rename from x-pack/legacy/plugins/ml/server/client/errors.js rename to x-pack/plugins/ml/server/client/errors.js diff --git a/x-pack/legacy/plugins/ml/server/client/log.ts b/x-pack/plugins/ml/server/client/log.ts similarity index 94% rename from x-pack/legacy/plugins/ml/server/client/log.ts rename to x-pack/plugins/ml/server/client/log.ts index ae82383ead605..8ee5882f6c2c1 100644 --- a/x-pack/legacy/plugins/ml/server/client/log.ts +++ b/x-pack/plugins/ml/server/client/log.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Logger } from '../../../../../../src/core/server'; +import { Logger } from '../../../../../src/core/server'; export interface LogInitialization { log: Logger; diff --git a/x-pack/legacy/plugins/ml/server/new_platform/index.ts b/x-pack/plugins/ml/server/index.ts similarity index 57% rename from x-pack/legacy/plugins/ml/server/new_platform/index.ts rename to x-pack/plugins/ml/server/index.ts index b03f2dac613b0..55e87ed6f0c6a 100644 --- a/x-pack/legacy/plugins/ml/server/new_platform/index.ts +++ b/x-pack/plugins/ml/server/index.ts @@ -4,8 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Plugin, MlInitializerContext } from './plugin'; +import { PluginInitializerContext } from 'kibana/server'; +import { MlServerPlugin } from './plugin'; -export function plugin(initializerContext: MlInitializerContext) { - return new Plugin(initializerContext); -} +export const plugin = (ctx: PluginInitializerContext) => new MlServerPlugin(ctx); diff --git a/x-pack/legacy/plugins/ml/server/lib/__tests__/query_utils.js b/x-pack/plugins/ml/server/lib/__tests__/query_utils.js similarity index 100% rename from x-pack/legacy/plugins/ml/server/lib/__tests__/query_utils.js rename to x-pack/plugins/ml/server/lib/__tests__/query_utils.js diff --git a/x-pack/legacy/plugins/ml/server/lib/check_annotations/index.js b/x-pack/plugins/ml/server/lib/check_annotations/index.ts similarity index 74% rename from x-pack/legacy/plugins/ml/server/lib/check_annotations/index.js rename to x-pack/plugins/ml/server/lib/check_annotations/index.ts index 186c27b0326d7..8d9d56ad665c4 100644 --- a/x-pack/legacy/plugins/ml/server/lib/check_annotations/index.js +++ b/x-pack/plugins/ml/server/lib/check_annotations/index.ts @@ -4,35 +4,44 @@ * you may not use this file except in compliance with the Elastic License. */ +import { APICaller } from 'src/core/server'; import { mlLog } from '../../client/log'; import { ML_ANNOTATIONS_INDEX_ALIAS_READ, ML_ANNOTATIONS_INDEX_ALIAS_WRITE, ML_ANNOTATIONS_INDEX_PATTERN, -} from '../../../common/constants/index_patterns'; +} from '../../../../../legacy/plugins/ml/common/constants/index_patterns'; // Annotations Feature is available if: // - ML_ANNOTATIONS_INDEX_PATTERN index is present // - ML_ANNOTATIONS_INDEX_ALIAS_READ alias is present // - ML_ANNOTATIONS_INDEX_ALIAS_WRITE alias is present -export async function isAnnotationsFeatureAvailable(callAsCurrentUser) { +export async function isAnnotationsFeatureAvailable(callAsCurrentUser: APICaller) { try { const indexParams = { index: ML_ANNOTATIONS_INDEX_PATTERN }; const annotationsIndexExists = await callAsCurrentUser('indices.exists', indexParams); - if (!annotationsIndexExists) return false; + if (!annotationsIndexExists) { + return false; + } const annotationsReadAliasExists = await callAsCurrentUser('indices.existsAlias', { + index: ML_ANNOTATIONS_INDEX_ALIAS_READ, name: ML_ANNOTATIONS_INDEX_ALIAS_READ, }); - if (!annotationsReadAliasExists) return false; + if (!annotationsReadAliasExists) { + return false; + } const annotationsWriteAliasExists = await callAsCurrentUser('indices.existsAlias', { + index: ML_ANNOTATIONS_INDEX_ALIAS_WRITE, name: ML_ANNOTATIONS_INDEX_ALIAS_WRITE, }); - if (!annotationsWriteAliasExists) return false; + if (!annotationsWriteAliasExists) { + return false; + } } catch (err) { mlLog.info('Disabling ML annotations feature because the index/alias integrity check failed.'); return false; diff --git a/x-pack/legacy/plugins/ml/server/lib/check_license/check_license.test.ts b/x-pack/plugins/ml/server/lib/check_license/check_license.test.ts similarity index 81% rename from x-pack/legacy/plugins/ml/server/lib/check_license/check_license.test.ts rename to x-pack/plugins/ml/server/lib/check_license/check_license.test.ts index 1d80a226486bb..942dbe3722617 100644 --- a/x-pack/legacy/plugins/ml/server/lib/check_license/check_license.test.ts +++ b/x-pack/plugins/ml/server/lib/check_license/check_license.test.ts @@ -7,12 +7,12 @@ import expect from '@kbn/expect'; import sinon from 'sinon'; import { set } from 'lodash'; -import { XPackInfo } from '../../../../xpack_main/server/lib/xpack_info'; +import { LicenseCheckResult } from '../../types'; import { checkLicense } from './check_license'; describe('check_license', () => { - let mockLicenseInfo: XPackInfo; - beforeEach(() => (mockLicenseInfo = {} as XPackInfo)); + let mockLicenseInfo: LicenseCheckResult; + beforeEach(() => (mockLicenseInfo = {} as LicenseCheckResult)); describe('license information is undefined', () => { it('should set isAvailable to false', () => { @@ -33,7 +33,9 @@ describe('check_license', () => { }); describe('license information is not available', () => { - beforeEach(() => (mockLicenseInfo.isAvailable = () => false)); + beforeEach(() => { + mockLicenseInfo.isAvailable = false; + }); it('should set isAvailable to false', () => { expect(checkLicense(mockLicenseInfo).isAvailable).to.be(false); @@ -54,8 +56,8 @@ describe('check_license', () => { describe('license information is available', () => { beforeEach(() => { - mockLicenseInfo.isAvailable = () => true; - set(mockLicenseInfo, 'license.getType', () => 'basic'); + mockLicenseInfo.isAvailable = true; + mockLicenseInfo.type = 'basic'; }); describe('& ML is disabled in Elasticsearch', () => { @@ -66,7 +68,7 @@ describe('check_license', () => { sinon .stub() .withArgs('ml') - .returns({ isEnabled: () => false }) + .returns({ isEnabled: false }) ); }); @@ -89,21 +91,17 @@ describe('check_license', () => { describe('& ML is enabled in Elasticsearch', () => { beforeEach(() => { - set( - mockLicenseInfo, - 'feature', - sinon - .stub() - .withArgs('ml') - .returns({ isEnabled: () => true }) - ); + mockLicenseInfo.isEnabled = true; }); describe('& license is >= platinum', () => { - beforeEach(() => set(mockLicenseInfo, 'license.isOneOf', () => true)); - + beforeEach(() => { + mockLicenseInfo.type = 'platinum'; + }); describe('& license is active', () => { - beforeEach(() => set(mockLicenseInfo, 'license.isActive', () => true)); + beforeEach(() => { + mockLicenseInfo.isActive = true; + }); it('should set isAvailable to true', () => { expect(checkLicense(mockLicenseInfo).isAvailable).to.be(true); @@ -123,7 +121,9 @@ describe('check_license', () => { }); describe('& license is expired', () => { - beforeEach(() => set(mockLicenseInfo, 'license.isActive', () => false)); + beforeEach(() => { + mockLicenseInfo.isActive = false; + }); it('should set isAvailable to true', () => { expect(checkLicense(mockLicenseInfo).isAvailable).to.be(true); @@ -144,10 +144,14 @@ describe('check_license', () => { }); describe('& license is basic', () => { - beforeEach(() => set(mockLicenseInfo, 'license.isOneOf', () => false)); + beforeEach(() => { + mockLicenseInfo.type = 'basic'; + }); describe('& license is active', () => { - beforeEach(() => set(mockLicenseInfo, 'license.isActive', () => true)); + beforeEach(() => { + mockLicenseInfo.isActive = true; + }); it('should set isAvailable to true', () => { expect(checkLicense(mockLicenseInfo).isAvailable).to.be(true); diff --git a/x-pack/legacy/plugins/ml/server/lib/check_license/check_license.ts b/x-pack/plugins/ml/server/lib/check_license/check_license.ts similarity index 75% rename from x-pack/legacy/plugins/ml/server/lib/check_license/check_license.ts rename to x-pack/plugins/ml/server/lib/check_license/check_license.ts index c88ab087a8198..5bf3d590a1912 100644 --- a/x-pack/legacy/plugins/ml/server/lib/check_license/check_license.ts +++ b/x-pack/plugins/ml/server/lib/check_license/check_license.ts @@ -5,8 +5,11 @@ */ import { i18n } from '@kbn/i18n'; -import { LICENSE_TYPE } from '../../../common/constants/license'; -import { XPackInfo } from '../../../../../../legacy/plugins/xpack_main/server/lib/xpack_info'; +import { + LICENSE_TYPE, + VALID_FULL_LICENSE_MODES, +} from '../../../../../legacy/plugins/ml/common/constants/license'; +import { LicenseCheckResult } from '../../types'; interface Response { isAvailable: boolean; @@ -17,10 +20,10 @@ interface Response { message?: string; } -export function checkLicense(xpackLicenseInfo: XPackInfo): Response { +export function checkLicense(licenseCheckResult: LicenseCheckResult): Response { // If, for some reason, we cannot get the license information // from Elasticsearch, assume worst case and disable the Machine Learning UI - if (!xpackLicenseInfo || !xpackLicenseInfo.isAvailable()) { + if (licenseCheckResult === undefined || !licenseCheckResult.isAvailable) { return { isAvailable: false, showLinks: true, @@ -35,7 +38,7 @@ export function checkLicense(xpackLicenseInfo: XPackInfo): Response { }; } - const featureEnabled = xpackLicenseInfo.feature('ml').isEnabled(); + const featureEnabled = licenseCheckResult.isEnabled; if (!featureEnabled) { return { isAvailable: false, @@ -47,12 +50,11 @@ export function checkLicense(xpackLicenseInfo: XPackInfo): Response { }; } - const VALID_FULL_LICENSE_MODES = ['platinum', 'enterprise', 'trial']; - - const isLicenseModeValid = xpackLicenseInfo.license.isOneOf(VALID_FULL_LICENSE_MODES); + const isLicenseModeValid = + licenseCheckResult.type && VALID_FULL_LICENSE_MODES.includes(licenseCheckResult.type); const licenseType = isLicenseModeValid === true ? LICENSE_TYPE.FULL : LICENSE_TYPE.BASIC; - const isLicenseActive = xpackLicenseInfo.license.isActive(); - const licenseTypeName = xpackLicenseInfo.license.getType(); + const isLicenseActive = licenseCheckResult.isActive; + const licenseTypeName = licenseCheckResult.type; // Platinum or trial license is valid but not active, i.e. expired if (licenseType === LICENSE_TYPE.FULL && isLicenseActive === false) { diff --git a/x-pack/legacy/plugins/ml/server/lib/check_license/index.ts b/x-pack/plugins/ml/server/lib/check_license/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/server/lib/check_license/index.ts rename to x-pack/plugins/ml/server/lib/check_license/index.ts diff --git a/x-pack/legacy/plugins/ml/server/lib/check_privileges/__mocks__/call_with_request.ts b/x-pack/plugins/ml/server/lib/check_privileges/__mocks__/call_with_request.ts similarity index 100% rename from x-pack/legacy/plugins/ml/server/lib/check_privileges/__mocks__/call_with_request.ts rename to x-pack/plugins/ml/server/lib/check_privileges/__mocks__/call_with_request.ts diff --git a/x-pack/legacy/plugins/ml/server/lib/check_privileges/check_privileges.test.ts b/x-pack/plugins/ml/server/lib/check_privileges/check_privileges.test.ts similarity index 91% rename from x-pack/legacy/plugins/ml/server/lib/check_privileges/check_privileges.test.ts rename to x-pack/plugins/ml/server/lib/check_privileges/check_privileges.test.ts index da8ef25b2f4df..0690aa53576a5 100644 --- a/x-pack/legacy/plugins/ml/server/lib/check_privileges/check_privileges.test.ts +++ b/x-pack/plugins/ml/server/lib/check_privileges/check_privileges.test.ts @@ -8,81 +8,29 @@ import { callWithRequestProvider } from './__mocks__/call_with_request'; import { privilegesProvider } from './check_privileges'; import { mlPrivileges } from './privileges'; -const xpackMainPluginWithSecurity = { - info: { - isAvailable: () => true, - feature: (f: string) => { - switch (f) { - case 'ml': - return { isEnabled: () => true }; - case 'security': - return { isEnabled: () => true }; - } - }, - license: { - isOneOf: () => true, - isActive: () => true, - getType: () => 'platinum', - }, - }, -} as any; +const licenseCheckResultWithSecurity = { + isAvailable: true, + isEnabled: true, + isSecurityDisabled: false, + type: 'platinum', + isActive: true, +}; -const xpackMainPluginWithOutSecurity = { - info: { - isAvailable: () => true, - feature: (f: string) => { - switch (f) { - case 'ml': - return { isEnabled: () => true }; - case 'security': - return { isEnabled: () => false }; - } - }, - license: { - isOneOf: () => true, - isActive: () => true, - getType: () => 'platinum', - }, - }, -} as any; +const licenseCheckResultWithOutSecurity = { + ...licenseCheckResultWithSecurity, + isSecurityDisabled: true, +}; -const xpackMainPluginWithOutSecurityBasicLicense = { - info: { - isAvailable: () => true, - feature: (f: string) => { - switch (f) { - case 'ml': - return { isEnabled: () => true }; - case 'security': - return { isEnabled: () => false }; - } - }, - license: { - isOneOf: () => false, - isActive: () => true, - getType: () => 'basic', - }, - }, -} as any; +const licenseCheckResultWithOutSecurityBasicLicense = { + ...licenseCheckResultWithSecurity, + isSecurityDisabled: true, + type: 'basic', +}; -const xpackMainPluginWithSecurityBasicLicense = { - info: { - isAvailable: () => true, - feature: (f: string) => { - switch (f) { - case 'ml': - return { isEnabled: () => true }; - case 'security': - return { isEnabled: () => true }; - } - }, - license: { - isOneOf: () => false, - isActive: () => true, - getType: () => 'basic', - }, - }, -} as any; +const licenseCheckResultWithSecurityBasicLicense = { + ...licenseCheckResultWithSecurity, + type: 'basic', +}; const mlIsEnabled = async () => true; const mlIsNotEnabled = async () => false; @@ -99,7 +47,7 @@ describe('check_privileges', () => { const callWithRequest = callWithRequestProvider('partialPrivileges'); const { getPrivileges } = privilegesProvider( callWithRequest, - xpackMainPluginWithSecurity, + licenseCheckResultWithSecurity, mlIsEnabled ); const { capabilities } = await getPrivileges(); @@ -114,7 +62,7 @@ describe('check_privileges', () => { const callWithRequest = callWithRequestProvider('partialPrivileges'); const { getPrivileges } = privilegesProvider( callWithRequest, - xpackMainPluginWithSecurity, + licenseCheckResultWithSecurity, mlIsEnabled ); const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getPrivileges(); @@ -149,7 +97,7 @@ describe('check_privileges', () => { const callWithRequest = callWithRequestProvider('fullPrivileges'); const { getPrivileges } = privilegesProvider( callWithRequest, - xpackMainPluginWithSecurity, + licenseCheckResultWithSecurity, mlIsEnabled ); const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getPrivileges(); @@ -184,7 +132,7 @@ describe('check_privileges', () => { const callWithRequest = callWithRequestProvider('upgradeWithFullPrivileges'); const { getPrivileges } = privilegesProvider( callWithRequest, - xpackMainPluginWithSecurity, + licenseCheckResultWithSecurity, mlIsEnabled ); const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getPrivileges(); @@ -219,7 +167,7 @@ describe('check_privileges', () => { const callWithRequest = callWithRequestProvider('upgradeWithPartialPrivileges'); const { getPrivileges } = privilegesProvider( callWithRequest, - xpackMainPluginWithSecurity, + licenseCheckResultWithSecurity, mlIsEnabled ); const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getPrivileges(); @@ -254,7 +202,7 @@ describe('check_privileges', () => { const callWithRequest = callWithRequestProvider('partialPrivileges'); const { getPrivileges } = privilegesProvider( callWithRequest, - xpackMainPluginWithSecurityBasicLicense, + licenseCheckResultWithSecurityBasicLicense, mlIsEnabled ); const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getPrivileges(); @@ -289,7 +237,7 @@ describe('check_privileges', () => { const callWithRequest = callWithRequestProvider('fullPrivileges'); const { getPrivileges } = privilegesProvider( callWithRequest, - xpackMainPluginWithSecurityBasicLicense, + licenseCheckResultWithSecurityBasicLicense, mlIsEnabled ); const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getPrivileges(); @@ -324,7 +272,7 @@ describe('check_privileges', () => { const callWithRequest = callWithRequestProvider('fullPrivileges'); const { getPrivileges } = privilegesProvider( callWithRequest, - xpackMainPluginWithSecurity, + licenseCheckResultWithSecurity, mlIsNotEnabled ); const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getPrivileges(); @@ -361,7 +309,7 @@ describe('check_privileges', () => { const callWithRequest = callWithRequestProvider('partialPrivileges'); const { getPrivileges } = privilegesProvider( callWithRequest, - xpackMainPluginWithOutSecurity, + licenseCheckResultWithOutSecurity, mlIsEnabled ); const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getPrivileges(); @@ -396,7 +344,7 @@ describe('check_privileges', () => { const callWithRequest = callWithRequestProvider('upgradeWithFullPrivileges'); const { getPrivileges } = privilegesProvider( callWithRequest, - xpackMainPluginWithOutSecurity, + licenseCheckResultWithOutSecurity, mlIsEnabled ); const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getPrivileges(); @@ -431,7 +379,7 @@ describe('check_privileges', () => { const callWithRequest = callWithRequestProvider('upgradeWithPartialPrivileges'); const { getPrivileges } = privilegesProvider( callWithRequest, - xpackMainPluginWithOutSecurity, + licenseCheckResultWithOutSecurity, mlIsEnabled ); const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getPrivileges(); @@ -466,7 +414,7 @@ describe('check_privileges', () => { const callWithRequest = callWithRequestProvider('partialPrivileges'); const { getPrivileges } = privilegesProvider( callWithRequest, - xpackMainPluginWithOutSecurityBasicLicense, + licenseCheckResultWithOutSecurityBasicLicense, mlIsEnabled ); const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getPrivileges(); @@ -501,7 +449,7 @@ describe('check_privileges', () => { const callWithRequest = callWithRequestProvider('fullPrivileges'); const { getPrivileges } = privilegesProvider( callWithRequest, - xpackMainPluginWithOutSecurityBasicLicense, + licenseCheckResultWithOutSecurityBasicLicense, mlIsEnabled ); const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getPrivileges(); @@ -536,7 +484,7 @@ describe('check_privileges', () => { const callWithRequest = callWithRequestProvider('partialPrivileges'); const { getPrivileges } = privilegesProvider( callWithRequest, - xpackMainPluginWithOutSecurity, + licenseCheckResultWithOutSecurity, mlIsNotEnabled ); const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getPrivileges(); diff --git a/x-pack/legacy/plugins/ml/server/lib/check_privileges/check_privileges.ts b/x-pack/plugins/ml/server/lib/check_privileges/check_privileges.ts similarity index 94% rename from x-pack/legacy/plugins/ml/server/lib/check_privileges/check_privileges.ts rename to x-pack/plugins/ml/server/lib/check_privileges/check_privileges.ts index 617778afbe121..a427780d13344 100644 --- a/x-pack/legacy/plugins/ml/server/lib/check_privileges/check_privileges.ts +++ b/x-pack/plugins/ml/server/lib/check_privileges/check_privileges.ts @@ -5,12 +5,14 @@ */ import { IScopedClusterClient } from 'kibana/server'; -import { Privileges, getDefaultPrivileges } from '../../../common/types/privileges'; -import { XPackMainPlugin } from '../../../../xpack_main/server/xpack_main'; -import { isSecurityDisabled } from '../../lib/security_utils'; +import { + Privileges, + getDefaultPrivileges, +} from '../../../../../legacy/plugins/ml/common/types/privileges'; import { upgradeCheckProvider } from './upgrade'; import { checkLicense } from '../check_license'; -import { LICENSE_TYPE } from '../../../common/constants/license'; +import { LICENSE_TYPE } from '../../../../../legacy/plugins/ml/common/constants/license'; +import { LicenseCheckResult } from '../../types'; import { mlPrivileges } from './privileges'; @@ -25,7 +27,7 @@ interface Response { export function privilegesProvider( callAsCurrentUser: IScopedClusterClient['callAsCurrentUser'], - xpackMainPlugin: XPackMainPlugin, + licenseCheckResult: LicenseCheckResult, isMlEnabledInSpace: () => Promise, ignoreSpaces: boolean = false ) { @@ -35,8 +37,8 @@ export function privilegesProvider( const privileges = getDefaultPrivileges(); const upgradeInProgress = await isUpgradeInProgress(); - const securityDisabled = isSecurityDisabled(xpackMainPlugin); - const license = checkLicense(xpackMainPlugin.info); + const securityDisabled = licenseCheckResult.isSecurityDisabled; + const license = checkLicense(licenseCheckResult); const isPlatinumOrTrialLicense = license.licenseType === LICENSE_TYPE.FULL; const mlFeatureEnabledInSpace = await isMlEnabledInSpace(); diff --git a/x-pack/legacy/plugins/ml/server/lib/check_privileges/index.ts b/x-pack/plugins/ml/server/lib/check_privileges/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/server/lib/check_privileges/index.ts rename to x-pack/plugins/ml/server/lib/check_privileges/index.ts diff --git a/x-pack/legacy/plugins/ml/server/lib/check_privileges/privileges.ts b/x-pack/plugins/ml/server/lib/check_privileges/privileges.ts similarity index 100% rename from x-pack/legacy/plugins/ml/server/lib/check_privileges/privileges.ts rename to x-pack/plugins/ml/server/lib/check_privileges/privileges.ts diff --git a/x-pack/legacy/plugins/ml/server/lib/check_privileges/upgrade.ts b/x-pack/plugins/ml/server/lib/check_privileges/upgrade.ts similarity index 100% rename from x-pack/legacy/plugins/ml/server/lib/check_privileges/upgrade.ts rename to x-pack/plugins/ml/server/lib/check_privileges/upgrade.ts diff --git a/x-pack/legacy/plugins/ml/server/lib/ml_telemetry/index.ts b/x-pack/plugins/ml/server/lib/ml_telemetry/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/server/lib/ml_telemetry/index.ts rename to x-pack/plugins/ml/server/lib/ml_telemetry/index.ts diff --git a/x-pack/legacy/plugins/ml/server/lib/ml_telemetry/make_ml_usage_collector.ts b/x-pack/plugins/ml/server/lib/ml_telemetry/make_ml_usage_collector.ts similarity index 100% rename from x-pack/legacy/plugins/ml/server/lib/ml_telemetry/make_ml_usage_collector.ts rename to x-pack/plugins/ml/server/lib/ml_telemetry/make_ml_usage_collector.ts diff --git a/x-pack/legacy/plugins/ml/server/lib/ml_telemetry/ml_telemetry.test.ts b/x-pack/plugins/ml/server/lib/ml_telemetry/ml_telemetry.test.ts similarity index 85% rename from x-pack/legacy/plugins/ml/server/lib/ml_telemetry/ml_telemetry.test.ts rename to x-pack/plugins/ml/server/lib/ml_telemetry/ml_telemetry.test.ts index 9d14ffb31be63..c03396445f868 100644 --- a/x-pack/legacy/plugins/ml/server/lib/ml_telemetry/ml_telemetry.test.ts +++ b/x-pack/plugins/ml/server/lib/ml_telemetry/ml_telemetry.test.ts @@ -55,10 +55,9 @@ describe('ml_telemetry', () => { }); describe('incrementFileDataVisualizerIndexCreationCount', () => { - let savedObjects: any; - let internalRepository: any; + let savedObjectsClient: any; - function createInternalRepositoryInstance( + function createSavedObjectsClientInstance( telemetryEnabled?: boolean, indexCreationCount?: number ) { @@ -93,42 +92,39 @@ describe('ml_telemetry', () => { } function mockInit(telemetryEnabled?: boolean, indexCreationCount?: number): void { - internalRepository = createInternalRepositoryInstance(telemetryEnabled, indexCreationCount); - savedObjects = { - createInternalRepository: jest.fn(() => internalRepository), - }; + savedObjectsClient = createSavedObjectsClientInstance(telemetryEnabled, indexCreationCount); } it('should not increment if telemetry status cannot be determined', async () => { mockInit(); - await incrementFileDataVisualizerIndexCreationCount(savedObjects); + await incrementFileDataVisualizerIndexCreationCount(savedObjectsClient); - expect(internalRepository.create.mock.calls).toHaveLength(0); + expect(savedObjectsClient.create.mock.calls).toHaveLength(0); }); it('should not increment if telemetry status is disabled', async () => { mockInit(false); - await incrementFileDataVisualizerIndexCreationCount(savedObjects); + await incrementFileDataVisualizerIndexCreationCount(savedObjectsClient); - expect(internalRepository.create.mock.calls).toHaveLength(0); + expect(savedObjectsClient.create.mock.calls).toHaveLength(0); }); it('should initialize index_creation_count with 1', async () => { mockInit(true); - await incrementFileDataVisualizerIndexCreationCount(savedObjects); + await incrementFileDataVisualizerIndexCreationCount(savedObjectsClient); - expect(internalRepository.create.mock.calls[0][0]).toBe('ml-telemetry'); - expect(internalRepository.create.mock.calls[0][1]).toEqual({ + expect(savedObjectsClient.create.mock.calls[0][0]).toBe('ml-telemetry'); + expect(savedObjectsClient.create.mock.calls[0][1]).toEqual({ file_data_visualizer: { index_creation_count: 1 }, }); }); it('should increment index_creation_count to 2', async () => { mockInit(true, 1); - await incrementFileDataVisualizerIndexCreationCount(savedObjects); + await incrementFileDataVisualizerIndexCreationCount(savedObjectsClient); - expect(internalRepository.create.mock.calls[0][0]).toBe('ml-telemetry'); - expect(internalRepository.create.mock.calls[0][1]).toEqual({ + expect(savedObjectsClient.create.mock.calls[0][0]).toBe('ml-telemetry'); + expect(savedObjectsClient.create.mock.calls[0][1]).toEqual({ file_data_visualizer: { index_creation_count: 2 }, }); }); diff --git a/x-pack/legacy/plugins/ml/server/lib/ml_telemetry/ml_telemetry.ts b/x-pack/plugins/ml/server/lib/ml_telemetry/ml_telemetry.ts similarity index 74% rename from x-pack/legacy/plugins/ml/server/lib/ml_telemetry/ml_telemetry.ts rename to x-pack/plugins/ml/server/lib/ml_telemetry/ml_telemetry.ts index d76b1ee94e21e..8cf24213961b1 100644 --- a/x-pack/legacy/plugins/ml/server/lib/ml_telemetry/ml_telemetry.ts +++ b/x-pack/plugins/ml/server/lib/ml_telemetry/ml_telemetry.ts @@ -4,11 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - SavedObjectAttributes, - SavedObjectsServiceStart, - ISavedObjectsRepository, -} from 'src/core/server'; +import { SavedObjectAttributes, SavedObjectsClientContract } from 'src/core/server'; export interface MlTelemetry extends SavedObjectAttributes { file_data_visualizer: { @@ -31,21 +27,20 @@ export function createMlTelemetry(count: number = 0): MlTelemetry { } // savedObjects export function storeMlTelemetry( - internalRepository: ISavedObjectsRepository, + savedObjectsClient: SavedObjectsClientContract, mlTelemetry: MlTelemetry ): void { - internalRepository.create('ml-telemetry', mlTelemetry, { + savedObjectsClient.create('ml-telemetry', mlTelemetry, { id: ML_TELEMETRY_DOC_ID, overwrite: true, }); } export async function incrementFileDataVisualizerIndexCreationCount( - savedObjects: SavedObjectsServiceStart + savedObjectsClient: SavedObjectsClientContract ): Promise { - const internalRepository = await savedObjects.createInternalRepository(); try { - const { attributes } = await internalRepository.get('telemetry', 'telemetry'); + const { attributes } = await savedObjectsClient.get('telemetry', 'telemetry'); if (attributes.enabled === false) { return; @@ -59,7 +54,7 @@ export async function incrementFileDataVisualizerIndexCreationCount( let indicesCount = 1; try { - const { attributes } = (await internalRepository.get( + const { attributes } = (await savedObjectsClient.get( 'ml-telemetry', ML_TELEMETRY_DOC_ID )) as MlTelemetrySavedObject; @@ -69,5 +64,5 @@ export async function incrementFileDataVisualizerIndexCreationCount( } const mlTelemetry = createMlTelemetry(indicesCount); - storeMlTelemetry(internalRepository, mlTelemetry); + storeMlTelemetry(savedObjectsClient, mlTelemetry); } diff --git a/x-pack/legacy/plugins/ml/server/lib/query_utils.ts b/x-pack/plugins/ml/server/lib/query_utils.ts similarity index 100% rename from x-pack/legacy/plugins/ml/server/lib/query_utils.ts rename to x-pack/plugins/ml/server/lib/query_utils.ts diff --git a/x-pack/legacy/plugins/ml/server/lib/sample_data_sets/index.ts b/x-pack/plugins/ml/server/lib/sample_data_sets/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/server/lib/sample_data_sets/index.ts rename to x-pack/plugins/ml/server/lib/sample_data_sets/index.ts diff --git a/x-pack/legacy/plugins/ml/server/lib/sample_data_sets/sample_data_sets.ts b/x-pack/plugins/ml/server/lib/sample_data_sets/sample_data_sets.ts similarity index 100% rename from x-pack/legacy/plugins/ml/server/lib/sample_data_sets/sample_data_sets.ts rename to x-pack/plugins/ml/server/lib/sample_data_sets/sample_data_sets.ts diff --git a/x-pack/legacy/plugins/ml/server/lib/spaces_utils.ts b/x-pack/plugins/ml/server/lib/spaces_utils.ts similarity index 75% rename from x-pack/legacy/plugins/ml/server/lib/spaces_utils.ts rename to x-pack/plugins/ml/server/lib/spaces_utils.ts index 92373bae4ea1d..ed684eadb9570 100644 --- a/x-pack/legacy/plugins/ml/server/lib/spaces_utils.ts +++ b/x-pack/plugins/ml/server/lib/spaces_utils.ts @@ -5,20 +5,19 @@ */ import { Request } from 'hapi'; -import { Space } from '../../../../../plugins/spaces/server'; -import { LegacySpacesPlugin } from '../../../spaces'; +import { Space, SpacesPluginSetup } from '../../../spaces/server'; interface GetActiveSpaceResponse { valid: boolean; space?: Space; } -export function spacesUtilsProvider(spacesPlugin: LegacySpacesPlugin, request: Request) { +export function spacesUtilsProvider(spacesPlugin: SpacesPluginSetup, request: Request) { async function activeSpace(): Promise { try { return { valid: true, - space: await spacesPlugin.getActiveSpace(request), + space: await spacesPlugin.spacesService.getActiveSpace(request), }; } catch (e) { return { diff --git a/x-pack/legacy/plugins/ml/server/models/annotation_service/__mocks__/get_annotations_request.json b/x-pack/plugins/ml/server/models/annotation_service/__mocks__/get_annotations_request.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/annotation_service/__mocks__/get_annotations_request.json rename to x-pack/plugins/ml/server/models/annotation_service/__mocks__/get_annotations_request.json diff --git a/x-pack/legacy/plugins/ml/server/models/annotation_service/__mocks__/get_annotations_response.json b/x-pack/plugins/ml/server/models/annotation_service/__mocks__/get_annotations_response.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/annotation_service/__mocks__/get_annotations_response.json rename to x-pack/plugins/ml/server/models/annotation_service/__mocks__/get_annotations_response.json diff --git a/x-pack/legacy/plugins/ml/server/models/annotation_service/annotation.test.ts b/x-pack/plugins/ml/server/models/annotation_service/annotation.test.ts similarity index 96% rename from x-pack/legacy/plugins/ml/server/models/annotation_service/annotation.test.ts rename to x-pack/plugins/ml/server/models/annotation_service/annotation.test.ts index 7e0649d15bfb0..d7a13154a6f37 100644 --- a/x-pack/legacy/plugins/ml/server/models/annotation_service/annotation.test.ts +++ b/x-pack/plugins/ml/server/models/annotation_service/annotation.test.ts @@ -8,9 +8,12 @@ import getAnnotationsRequestMock from './__mocks__/get_annotations_request.json' import getAnnotationsResponseMock from './__mocks__/get_annotations_response.json'; import { RequestHandlerContext } from 'src/core/server'; -import { ANNOTATION_TYPE } from '../../../common/constants/annotations'; -import { ML_ANNOTATIONS_INDEX_ALIAS_WRITE } from '../../../common/constants/index_patterns'; -import { Annotation, isAnnotations } from '../../../common/types/annotations'; +import { ANNOTATION_TYPE } from '../../../../../legacy/plugins/ml/common/constants/annotations'; +import { ML_ANNOTATIONS_INDEX_ALIAS_WRITE } from '../../../../../legacy/plugins/ml/common/constants/index_patterns'; +import { + Annotation, + isAnnotations, +} from '../../../../../legacy/plugins/ml/common/types/annotations'; import { DeleteParams, GetResponse, IndexAnnotationArgs } from './annotation'; import { annotationServiceProvider } from './index'; diff --git a/x-pack/legacy/plugins/ml/server/models/annotation_service/annotation.ts b/x-pack/plugins/ml/server/models/annotation_service/annotation.ts similarity index 96% rename from x-pack/legacy/plugins/ml/server/models/annotation_service/annotation.ts rename to x-pack/plugins/ml/server/models/annotation_service/annotation.ts index 399305ea2603e..042d7bbc80653 100644 --- a/x-pack/legacy/plugins/ml/server/models/annotation_service/annotation.ts +++ b/x-pack/plugins/ml/server/models/annotation_service/annotation.ts @@ -8,18 +8,18 @@ import Boom from 'boom'; import _ from 'lodash'; import { RequestHandlerContext } from 'src/core/server'; -import { ANNOTATION_TYPE } from '../../../common/constants/annotations'; +import { ANNOTATION_TYPE } from '../../../../../legacy/plugins/ml/common/constants/annotations'; import { ML_ANNOTATIONS_INDEX_ALIAS_READ, ML_ANNOTATIONS_INDEX_ALIAS_WRITE, -} from '../../../common/constants/index_patterns'; +} from '../../../../../legacy/plugins/ml/common/constants/index_patterns'; import { Annotation, Annotations, isAnnotation, isAnnotations, -} from '../../../common/types/annotations'; +} from '../../../../../legacy/plugins/ml/common/types/annotations'; // TODO All of the following interface/type definitions should // eventually be replaced by the proper upstream definitions diff --git a/x-pack/legacy/plugins/ml/server/models/annotation_service/index.ts b/x-pack/plugins/ml/server/models/annotation_service/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/annotation_service/index.ts rename to x-pack/plugins/ml/server/models/annotation_service/index.ts diff --git a/x-pack/legacy/plugins/ml/server/models/bucket_span_estimator/__tests__/bucket_span_estimator.js b/x-pack/plugins/ml/server/models/bucket_span_estimator/__tests__/bucket_span_estimator.js similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/bucket_span_estimator/__tests__/bucket_span_estimator.js rename to x-pack/plugins/ml/server/models/bucket_span_estimator/__tests__/bucket_span_estimator.js diff --git a/x-pack/legacy/plugins/ml/server/models/bucket_span_estimator/bucket_span_estimator.d.ts b/x-pack/plugins/ml/server/models/bucket_span_estimator/bucket_span_estimator.d.ts similarity index 75% rename from x-pack/legacy/plugins/ml/server/models/bucket_span_estimator/bucket_span_estimator.d.ts rename to x-pack/plugins/ml/server/models/bucket_span_estimator/bucket_span_estimator.d.ts index ea986feab4e99..e39a0177c31b9 100644 --- a/x-pack/legacy/plugins/ml/server/models/bucket_span_estimator/bucket_span_estimator.d.ts +++ b/x-pack/plugins/ml/server/models/bucket_span_estimator/bucket_span_estimator.d.ts @@ -5,10 +5,10 @@ */ import { APICaller } from 'src/core/server'; -import { BucketSpanEstimatorData } from '../../../public/application/services/ml_api_service'; +import { BucketSpanEstimatorData } from '../../../../../legacy/plugins/ml/public/application/services/ml_api_service'; export function estimateBucketSpanFactory( callAsCurrentUser: APICaller, callAsInternalUser: APICaller, - xpackMainPlugin: any + isSecurityDisabled: boolean ): (config: BucketSpanEstimatorData) => Promise; diff --git a/x-pack/legacy/plugins/ml/server/models/bucket_span_estimator/bucket_span_estimator.js b/x-pack/plugins/ml/server/models/bucket_span_estimator/bucket_span_estimator.js similarity index 98% rename from x-pack/legacy/plugins/ml/server/models/bucket_span_estimator/bucket_span_estimator.js rename to x-pack/plugins/ml/server/models/bucket_span_estimator/bucket_span_estimator.js index aec677dd57d61..53b9d75304963 100644 --- a/x-pack/legacy/plugins/ml/server/models/bucket_span_estimator/bucket_span_estimator.js +++ b/x-pack/plugins/ml/server/models/bucket_span_estimator/bucket_span_estimator.js @@ -12,9 +12,11 @@ import { INTERVALS } from './intervals'; import { singleSeriesCheckerFactory } from './single_series_checker'; import { polledDataCheckerFactory } from './polled_data_checker'; -import { isSecurityDisabled } from '../../lib/security_utils'; - -export function estimateBucketSpanFactory(callAsCurrentUser, callAsInternalUser, xpackMainPlugin) { +export function estimateBucketSpanFactory( + callAsCurrentUser, + callAsInternalUser, + isSecurityDisabled +) { const PolledDataChecker = polledDataCheckerFactory(callAsCurrentUser); const SingleSeriesChecker = singleSeriesCheckerFactory(callAsCurrentUser); @@ -384,7 +386,7 @@ export function estimateBucketSpanFactory(callAsCurrentUser, callAsInternalUser, }); } - if (isSecurityDisabled(xpackMainPlugin)) { + if (isSecurityDisabled) { getBucketSpanEstimation(); } else { // if security is enabled, check that the user has permission to diff --git a/x-pack/legacy/plugins/ml/server/models/bucket_span_estimator/index.ts b/x-pack/plugins/ml/server/models/bucket_span_estimator/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/bucket_span_estimator/index.ts rename to x-pack/plugins/ml/server/models/bucket_span_estimator/index.ts diff --git a/x-pack/legacy/plugins/ml/server/models/bucket_span_estimator/intervals.js b/x-pack/plugins/ml/server/models/bucket_span_estimator/intervals.js similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/bucket_span_estimator/intervals.js rename to x-pack/plugins/ml/server/models/bucket_span_estimator/intervals.js diff --git a/x-pack/legacy/plugins/ml/server/models/bucket_span_estimator/polled_data_checker.js b/x-pack/plugins/ml/server/models/bucket_span_estimator/polled_data_checker.js similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/bucket_span_estimator/polled_data_checker.js rename to x-pack/plugins/ml/server/models/bucket_span_estimator/polled_data_checker.js diff --git a/x-pack/legacy/plugins/ml/server/models/bucket_span_estimator/single_series_checker.js b/x-pack/plugins/ml/server/models/bucket_span_estimator/single_series_checker.js similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/bucket_span_estimator/single_series_checker.js rename to x-pack/plugins/ml/server/models/bucket_span_estimator/single_series_checker.js diff --git a/x-pack/legacy/plugins/ml/server/models/calculate_model_memory_limit/calculate_model_memory_limit.d.ts b/x-pack/plugins/ml/server/models/calculate_model_memory_limit/calculate_model_memory_limit.d.ts similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/calculate_model_memory_limit/calculate_model_memory_limit.d.ts rename to x-pack/plugins/ml/server/models/calculate_model_memory_limit/calculate_model_memory_limit.d.ts diff --git a/x-pack/legacy/plugins/ml/server/models/calculate_model_memory_limit/calculate_model_memory_limit.js b/x-pack/plugins/ml/server/models/calculate_model_memory_limit/calculate_model_memory_limit.js similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/calculate_model_memory_limit/calculate_model_memory_limit.js rename to x-pack/plugins/ml/server/models/calculate_model_memory_limit/calculate_model_memory_limit.js diff --git a/x-pack/legacy/plugins/ml/server/models/calculate_model_memory_limit/index.ts b/x-pack/plugins/ml/server/models/calculate_model_memory_limit/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/calculate_model_memory_limit/index.ts rename to x-pack/plugins/ml/server/models/calculate_model_memory_limit/index.ts diff --git a/x-pack/legacy/plugins/ml/server/models/calendar/calendar_manager.ts b/x-pack/plugins/ml/server/models/calendar/calendar_manager.ts similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/calendar/calendar_manager.ts rename to x-pack/plugins/ml/server/models/calendar/calendar_manager.ts diff --git a/x-pack/legacy/plugins/ml/server/models/calendar/event_manager.ts b/x-pack/plugins/ml/server/models/calendar/event_manager.ts similarity index 94% rename from x-pack/legacy/plugins/ml/server/models/calendar/event_manager.ts rename to x-pack/plugins/ml/server/models/calendar/event_manager.ts index 488839f68b3fe..0a3108016da0e 100644 --- a/x-pack/legacy/plugins/ml/server/models/calendar/event_manager.ts +++ b/x-pack/plugins/ml/server/models/calendar/event_manager.ts @@ -6,7 +6,7 @@ import Boom from 'boom'; -import { GLOBAL_CALENDAR } from '../../../common/constants/calendars'; +import { GLOBAL_CALENDAR } from '../../../../../legacy/plugins/ml/common/constants/calendars'; export interface CalendarEvent { calendar_id?: string; diff --git a/x-pack/legacy/plugins/ml/server/models/calendar/index.ts b/x-pack/plugins/ml/server/models/calendar/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/calendar/index.ts rename to x-pack/plugins/ml/server/models/calendar/index.ts diff --git a/x-pack/legacy/plugins/ml/server/models/data_frame_analytics/analytics_audit_messages.ts b/x-pack/plugins/ml/server/models/data_frame_analytics/analytics_audit_messages.ts similarity index 88% rename from x-pack/legacy/plugins/ml/server/models/data_frame_analytics/analytics_audit_messages.ts rename to x-pack/plugins/ml/server/models/data_frame_analytics/analytics_audit_messages.ts index abe389165182f..a8757e289dcf7 100644 --- a/x-pack/legacy/plugins/ml/server/models/data_frame_analytics/analytics_audit_messages.ts +++ b/x-pack/plugins/ml/server/models/data_frame_analytics/analytics_audit_messages.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { callWithRequestType } from '../../../common/types/kibana'; -import { ML_NOTIFICATION_INDEX_PATTERN } from '../../../common/constants/index_patterns'; -import { JobMessage } from '../../../common/types/audit_message'; +import { callWithRequestType } from '../../../../../legacy/plugins/ml/common/types/kibana'; +import { ML_NOTIFICATION_INDEX_PATTERN } from '../../../../../legacy/plugins/ml/common/constants/index_patterns'; +import { JobMessage } from '../../../../../legacy/plugins/ml/common/types/audit_message'; const SIZE = 50; diff --git a/x-pack/legacy/plugins/ml/server/models/data_frame_analytics/index.js b/x-pack/plugins/ml/server/models/data_frame_analytics/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_frame_analytics/index.js rename to x-pack/plugins/ml/server/models/data_frame_analytics/index.js diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/data_recognizer.test.ts b/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.test.ts similarity index 97% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/data_recognizer.test.ts rename to x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.test.ts index de23950e5cc1c..c51f65714bc05 100644 --- a/x-pack/legacy/plugins/ml/server/models/data_recognizer/data_recognizer.test.ts +++ b/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.test.ts @@ -5,7 +5,7 @@ */ import { RequestHandlerContext } from 'kibana/server'; -import { Module } from '../../../common/types/modules'; +import { Module } from '../../../../../legacy/plugins/ml/common/types/modules'; import { DataRecognizer } from '../data_recognizer'; describe('ML - data recognizer', () => { diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/data_recognizer.ts b/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.ts similarity index 99% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/data_recognizer.ts rename to x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.ts index 553de75e38e05..8d2a6c9955da3 100644 --- a/x-pack/legacy/plugins/ml/server/models/data_recognizer/data_recognizer.ts +++ b/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.ts @@ -10,7 +10,7 @@ import numeral from '@elastic/numeral'; import { CallAPIOptions, RequestHandlerContext, SavedObjectsClientContract } from 'kibana/server'; import { IndexPatternAttributes } from 'src/plugins/data/server'; import { merge } from 'lodash'; -import { MlJob } from '../../../common/types/jobs'; +import { MlJob } from '../../../../../legacy/plugins/ml/common/types/jobs'; import { KibanaObjects, ModuleDataFeed, @@ -23,8 +23,11 @@ import { JobResponse, KibanaObjectResponse, DataRecognizerConfigResponse, -} from '../../../common/types/modules'; -import { getLatestDataOrBucketTimestamp, prefixDatafeedId } from '../../../common/util/job_utils'; +} from '../../../../../legacy/plugins/ml/common/types/modules'; +import { + getLatestDataOrBucketTimestamp, + prefixDatafeedId, +} from '../../../../../legacy/plugins/ml/common/util/job_utils'; import { mlLog } from '../../client/log'; // @ts-ignore import { jobServiceProvider } from '../job_service'; diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/index.ts b/x-pack/plugins/ml/server/models/data_recognizer/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/index.ts rename to x-pack/plugins/ml/server/models/data_recognizer/index.ts diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apache_ecs/kibana/dashboard/ml_http_access_explorer_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apache_ecs/kibana/dashboard/ml_http_access_explorer_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apache_ecs/kibana/dashboard/ml_http_access_explorer_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/apache_ecs/kibana/dashboard/ml_http_access_explorer_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apache_ecs/kibana/search/ml_http_access_filebeat_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apache_ecs/kibana/search/ml_http_access_filebeat_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apache_ecs/kibana/search/ml_http_access_filebeat_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/apache_ecs/kibana/search/ml_http_access_filebeat_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apache_ecs/kibana/visualization/ml_http_access_events_timechart_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apache_ecs/kibana/visualization/ml_http_access_events_timechart_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apache_ecs/kibana/visualization/ml_http_access_events_timechart_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/apache_ecs/kibana/visualization/ml_http_access_events_timechart_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apache_ecs/kibana/visualization/ml_http_access_map_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apache_ecs/kibana/visualization/ml_http_access_map_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apache_ecs/kibana/visualization/ml_http_access_map_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/apache_ecs/kibana/visualization/ml_http_access_map_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apache_ecs/kibana/visualization/ml_http_access_source_ip_timechart_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apache_ecs/kibana/visualization/ml_http_access_source_ip_timechart_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apache_ecs/kibana/visualization/ml_http_access_source_ip_timechart_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/apache_ecs/kibana/visualization/ml_http_access_source_ip_timechart_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apache_ecs/kibana/visualization/ml_http_access_status_code_timechart_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apache_ecs/kibana/visualization/ml_http_access_status_code_timechart_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apache_ecs/kibana/visualization/ml_http_access_status_code_timechart_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/apache_ecs/kibana/visualization/ml_http_access_status_code_timechart_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apache_ecs/kibana/visualization/ml_http_access_top_source_ips_table_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apache_ecs/kibana/visualization/ml_http_access_top_source_ips_table_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apache_ecs/kibana/visualization/ml_http_access_top_source_ips_table_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/apache_ecs/kibana/visualization/ml_http_access_top_source_ips_table_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apache_ecs/kibana/visualization/ml_http_access_top_urls_table_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apache_ecs/kibana/visualization/ml_http_access_top_urls_table_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apache_ecs/kibana/visualization/ml_http_access_top_urls_table_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/apache_ecs/kibana/visualization/ml_http_access_top_urls_table_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apache_ecs/kibana/visualization/ml_http_access_unique_count_url_timechart_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apache_ecs/kibana/visualization/ml_http_access_unique_count_url_timechart_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apache_ecs/kibana/visualization/ml_http_access_unique_count_url_timechart_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/apache_ecs/kibana/visualization/ml_http_access_unique_count_url_timechart_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apache_ecs/logo.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apache_ecs/logo.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apache_ecs/logo.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/apache_ecs/logo.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apache_ecs/manifest.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apache_ecs/manifest.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apache_ecs/manifest.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/apache_ecs/manifest.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/datafeed_low_request_rate_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/datafeed_low_request_rate_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/datafeed_low_request_rate_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/datafeed_low_request_rate_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/datafeed_source_ip_request_rate_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/datafeed_source_ip_request_rate_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/datafeed_source_ip_request_rate_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/datafeed_source_ip_request_rate_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/datafeed_source_ip_url_count_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/datafeed_source_ip_url_count_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/datafeed_source_ip_url_count_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/datafeed_source_ip_url_count_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/datafeed_status_code_rate_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/datafeed_status_code_rate_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/datafeed_status_code_rate_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/datafeed_status_code_rate_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/datafeed_visitor_rate_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/datafeed_visitor_rate_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/datafeed_visitor_rate_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/datafeed_visitor_rate_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/low_request_rate_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/low_request_rate_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/low_request_rate_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/low_request_rate_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/source_ip_request_rate_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/source_ip_request_rate_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/source_ip_request_rate_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/source_ip_request_rate_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/source_ip_url_count_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/source_ip_url_count_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/source_ip_url_count_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/source_ip_url_count_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/status_code_rate_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/status_code_rate_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/status_code_rate_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/status_code_rate_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/visitor_rate_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/visitor_rate_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/visitor_rate_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/visitor_rate_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apm_jsbase/logo.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_jsbase/logo.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apm_jsbase/logo.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/apm_jsbase/logo.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apm_jsbase/manifest.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_jsbase/manifest.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apm_jsbase/manifest.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/apm_jsbase/manifest.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apm_jsbase/ml/abnormal_span_durations_jsbase.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_jsbase/ml/abnormal_span_durations_jsbase.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apm_jsbase/ml/abnormal_span_durations_jsbase.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/apm_jsbase/ml/abnormal_span_durations_jsbase.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apm_jsbase/ml/anomalous_error_rate_for_user_agents_jsbase.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_jsbase/ml/anomalous_error_rate_for_user_agents_jsbase.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apm_jsbase/ml/anomalous_error_rate_for_user_agents_jsbase.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/apm_jsbase/ml/anomalous_error_rate_for_user_agents_jsbase.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apm_jsbase/ml/datafeed_abnormal_span_durations_jsbase.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_jsbase/ml/datafeed_abnormal_span_durations_jsbase.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apm_jsbase/ml/datafeed_abnormal_span_durations_jsbase.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/apm_jsbase/ml/datafeed_abnormal_span_durations_jsbase.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apm_jsbase/ml/datafeed_anomalous_error_rate_for_user_agents_jsbase.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_jsbase/ml/datafeed_anomalous_error_rate_for_user_agents_jsbase.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apm_jsbase/ml/datafeed_anomalous_error_rate_for_user_agents_jsbase.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/apm_jsbase/ml/datafeed_anomalous_error_rate_for_user_agents_jsbase.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apm_jsbase/ml/datafeed_decreased_throughput_jsbase.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_jsbase/ml/datafeed_decreased_throughput_jsbase.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apm_jsbase/ml/datafeed_decreased_throughput_jsbase.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/apm_jsbase/ml/datafeed_decreased_throughput_jsbase.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apm_jsbase/ml/datafeed_high_count_by_user_agent_jsbase.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_jsbase/ml/datafeed_high_count_by_user_agent_jsbase.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apm_jsbase/ml/datafeed_high_count_by_user_agent_jsbase.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/apm_jsbase/ml/datafeed_high_count_by_user_agent_jsbase.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apm_jsbase/ml/decreased_throughput_jsbase.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_jsbase/ml/decreased_throughput_jsbase.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apm_jsbase/ml/decreased_throughput_jsbase.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/apm_jsbase/ml/decreased_throughput_jsbase.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apm_jsbase/ml/high_count_by_user_agent_jsbase.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_jsbase/ml/high_count_by_user_agent_jsbase.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apm_jsbase/ml/high_count_by_user_agent_jsbase.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/apm_jsbase/ml/high_count_by_user_agent_jsbase.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apm_nodejs/logo.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_nodejs/logo.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apm_nodejs/logo.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/apm_nodejs/logo.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apm_nodejs/manifest.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_nodejs/manifest.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apm_nodejs/manifest.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/apm_nodejs/manifest.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apm_nodejs/ml/abnormal_span_durations_nodejs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_nodejs/ml/abnormal_span_durations_nodejs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apm_nodejs/ml/abnormal_span_durations_nodejs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/apm_nodejs/ml/abnormal_span_durations_nodejs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apm_nodejs/ml/abnormal_trace_durations_nodejs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_nodejs/ml/abnormal_trace_durations_nodejs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apm_nodejs/ml/abnormal_trace_durations_nodejs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/apm_nodejs/ml/abnormal_trace_durations_nodejs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apm_nodejs/ml/datafeed_abnormal_span_durations_nodejs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_nodejs/ml/datafeed_abnormal_span_durations_nodejs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apm_nodejs/ml/datafeed_abnormal_span_durations_nodejs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/apm_nodejs/ml/datafeed_abnormal_span_durations_nodejs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apm_nodejs/ml/datafeed_abnormal_trace_durations_nodejs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_nodejs/ml/datafeed_abnormal_trace_durations_nodejs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apm_nodejs/ml/datafeed_abnormal_trace_durations_nodejs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/apm_nodejs/ml/datafeed_abnormal_trace_durations_nodejs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apm_nodejs/ml/datafeed_decreased_throughput_nodejs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_nodejs/ml/datafeed_decreased_throughput_nodejs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apm_nodejs/ml/datafeed_decreased_throughput_nodejs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/apm_nodejs/ml/datafeed_decreased_throughput_nodejs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apm_nodejs/ml/decreased_throughput_nodejs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_nodejs/ml/decreased_throughput_nodejs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apm_nodejs/ml/decreased_throughput_nodejs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/apm_nodejs/ml/decreased_throughput_nodejs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apm_transaction/logo.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_transaction/logo.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apm_transaction/logo.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/apm_transaction/logo.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apm_transaction/manifest.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_transaction/manifest.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apm_transaction/manifest.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/apm_transaction/manifest.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apm_transaction/ml/datafeed_high_mean_response_time.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_transaction/ml/datafeed_high_mean_response_time.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apm_transaction/ml/datafeed_high_mean_response_time.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/apm_transaction/ml/datafeed_high_mean_response_time.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apm_transaction/ml/high_mean_response_time.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_transaction/ml/high_mean_response_time.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apm_transaction/ml/high_mean_response_time.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/apm_transaction/ml/high_mean_response_time.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/kibana/dashboard/ml_auditbeat_docker_process_event_rate_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/kibana/dashboard/ml_auditbeat_docker_process_event_rate_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/kibana/dashboard/ml_auditbeat_docker_process_event_rate_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/kibana/dashboard/ml_auditbeat_docker_process_event_rate_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/kibana/dashboard/ml_auditbeat_docker_process_explorer_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/kibana/dashboard/ml_auditbeat_docker_process_explorer_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/kibana/dashboard/ml_auditbeat_docker_process_explorer_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/kibana/dashboard/ml_auditbeat_docker_process_explorer_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/kibana/search/ml_auditbeat_docker_process_events_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/kibana/search/ml_auditbeat_docker_process_events_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/kibana/search/ml_auditbeat_docker_process_events_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/kibana/search/ml_auditbeat_docker_process_events_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/kibana/visualization/ml_auditbeat_docker_process_event_rate_by_process_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/kibana/visualization/ml_auditbeat_docker_process_event_rate_by_process_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/kibana/visualization/ml_auditbeat_docker_process_event_rate_by_process_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/kibana/visualization/ml_auditbeat_docker_process_event_rate_by_process_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/kibana/visualization/ml_auditbeat_docker_process_event_rate_vis_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/kibana/visualization/ml_auditbeat_docker_process_event_rate_vis_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/kibana/visualization/ml_auditbeat_docker_process_event_rate_vis_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/kibana/visualization/ml_auditbeat_docker_process_event_rate_vis_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/kibana/visualization/ml_auditbeat_docker_process_occurrence_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/kibana/visualization/ml_auditbeat_docker_process_occurrence_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/kibana/visualization/ml_auditbeat_docker_process_occurrence_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/kibana/visualization/ml_auditbeat_docker_process_occurrence_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/logo.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/logo.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/logo.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/logo.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/manifest.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/manifest.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/manifest.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/manifest.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/ml/datafeed_docker_high_count_process_events_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/ml/datafeed_docker_high_count_process_events_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/ml/datafeed_docker_high_count_process_events_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/ml/datafeed_docker_high_count_process_events_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/ml/datafeed_docker_rare_process_activity_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/ml/datafeed_docker_rare_process_activity_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/ml/datafeed_docker_rare_process_activity_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/ml/datafeed_docker_rare_process_activity_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/ml/docker_high_count_process_events_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/ml/docker_high_count_process_events_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/ml/docker_high_count_process_events_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/ml/docker_high_count_process_events_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/ml/docker_rare_process_activity_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/ml/docker_rare_process_activity_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/ml/docker_rare_process_activity_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/ml/docker_rare_process_activity_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/kibana/dashboard/ml_auditbeat_hosts_process_event_rate_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/kibana/dashboard/ml_auditbeat_hosts_process_event_rate_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/kibana/dashboard/ml_auditbeat_hosts_process_event_rate_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/kibana/dashboard/ml_auditbeat_hosts_process_event_rate_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/kibana/dashboard/ml_auditbeat_hosts_process_explorer_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/kibana/dashboard/ml_auditbeat_hosts_process_explorer_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/kibana/dashboard/ml_auditbeat_hosts_process_explorer_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/kibana/dashboard/ml_auditbeat_hosts_process_explorer_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/kibana/search/ml_auditbeat_hosts_process_events_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/kibana/search/ml_auditbeat_hosts_process_events_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/kibana/search/ml_auditbeat_hosts_process_events_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/kibana/search/ml_auditbeat_hosts_process_events_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/kibana/visualization/ml_auditbeat_hosts_process_event_rate_by_process_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/kibana/visualization/ml_auditbeat_hosts_process_event_rate_by_process_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/kibana/visualization/ml_auditbeat_hosts_process_event_rate_by_process_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/kibana/visualization/ml_auditbeat_hosts_process_event_rate_by_process_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/kibana/visualization/ml_auditbeat_hosts_process_event_rate_vis_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/kibana/visualization/ml_auditbeat_hosts_process_event_rate_vis_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/kibana/visualization/ml_auditbeat_hosts_process_event_rate_vis_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/kibana/visualization/ml_auditbeat_hosts_process_event_rate_vis_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/kibana/visualization/ml_auditbeat_hosts_process_occurrence_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/kibana/visualization/ml_auditbeat_hosts_process_occurrence_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/kibana/visualization/ml_auditbeat_hosts_process_occurrence_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/kibana/visualization/ml_auditbeat_hosts_process_occurrence_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/logo.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/logo.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/logo.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/logo.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/manifest.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/manifest.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/manifest.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/manifest.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/ml/datafeed_hosts_high_count_process_events_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/ml/datafeed_hosts_high_count_process_events_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/ml/datafeed_hosts_high_count_process_events_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/ml/datafeed_hosts_high_count_process_events_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/ml/datafeed_hosts_rare_process_activity_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/ml/datafeed_hosts_rare_process_activity_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/ml/datafeed_hosts_rare_process_activity_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/ml/datafeed_hosts_rare_process_activity_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/ml/hosts_high_count_process_events_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/ml/hosts_high_count_process_events_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/ml/hosts_high_count_process_events_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/ml/hosts_high_count_process_events_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/ml/hosts_rare_process_activity_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/ml/hosts_rare_process_activity_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/ml/hosts_rare_process_activity_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/ml/hosts_rare_process_activity_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/logs_ui_analysis/logo.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/logs_ui_analysis/logo.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/logs_ui_analysis/logo.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/logs_ui_analysis/logo.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/logs_ui_analysis/manifest.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/logs_ui_analysis/manifest.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/logs_ui_analysis/manifest.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/logs_ui_analysis/manifest.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/logs_ui_analysis/ml/datafeed_log_entry_rate.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/logs_ui_analysis/ml/datafeed_log_entry_rate.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/logs_ui_analysis/ml/datafeed_log_entry_rate.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/logs_ui_analysis/ml/datafeed_log_entry_rate.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/logs_ui_analysis/ml/log_entry_rate.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/logs_ui_analysis/ml/log_entry_rate.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/logs_ui_analysis/ml/log_entry_rate.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/logs_ui_analysis/ml/log_entry_rate.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/logs_ui_categories/logo.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/logs_ui_categories/logo.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/logs_ui_categories/logo.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/logs_ui_categories/logo.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/logs_ui_categories/manifest.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/logs_ui_categories/manifest.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/logs_ui_categories/manifest.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/logs_ui_categories/manifest.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/logs_ui_categories/ml/datafeed_log_entry_categories_count.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/logs_ui_categories/ml/datafeed_log_entry_categories_count.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/logs_ui_categories/ml/datafeed_log_entry_categories_count.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/logs_ui_categories/ml/datafeed_log_entry_categories_count.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/logs_ui_categories/ml/log_entry_categories_count.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/logs_ui_categories/ml/log_entry_categories_count.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/logs_ui_categories/ml/log_entry_categories_count.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/logs_ui_categories/ml/log_entry_categories_count.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/metricbeat_system_ecs/logo.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/metricbeat_system_ecs/logo.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/metricbeat_system_ecs/logo.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/metricbeat_system_ecs/logo.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/metricbeat_system_ecs/manifest.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/metricbeat_system_ecs/manifest.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/metricbeat_system_ecs/manifest.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/metricbeat_system_ecs/manifest.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/metricbeat_system_ecs/ml/datafeed_high_mean_cpu_iowait_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/metricbeat_system_ecs/ml/datafeed_high_mean_cpu_iowait_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/metricbeat_system_ecs/ml/datafeed_high_mean_cpu_iowait_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/metricbeat_system_ecs/ml/datafeed_high_mean_cpu_iowait_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/metricbeat_system_ecs/ml/datafeed_max_disk_utilization_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/metricbeat_system_ecs/ml/datafeed_max_disk_utilization_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/metricbeat_system_ecs/ml/datafeed_max_disk_utilization_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/metricbeat_system_ecs/ml/datafeed_max_disk_utilization_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/metricbeat_system_ecs/ml/datafeed_metricbeat_outages_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/metricbeat_system_ecs/ml/datafeed_metricbeat_outages_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/metricbeat_system_ecs/ml/datafeed_metricbeat_outages_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/metricbeat_system_ecs/ml/datafeed_metricbeat_outages_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/metricbeat_system_ecs/ml/high_mean_cpu_iowait_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/metricbeat_system_ecs/ml/high_mean_cpu_iowait_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/metricbeat_system_ecs/ml/high_mean_cpu_iowait_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/metricbeat_system_ecs/ml/high_mean_cpu_iowait_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/metricbeat_system_ecs/ml/max_disk_utilization_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/metricbeat_system_ecs/ml/max_disk_utilization_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/metricbeat_system_ecs/ml/max_disk_utilization_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/metricbeat_system_ecs/ml/max_disk_utilization_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/metricbeat_system_ecs/ml/metricbeat_outages_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/metricbeat_system_ecs/ml/metricbeat_outages_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/metricbeat_system_ecs/ml/metricbeat_outages_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/metricbeat_system_ecs/ml/metricbeat_outages_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/kibana/dashboard/ml_http_access_explorer_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/kibana/dashboard/ml_http_access_explorer_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/kibana/dashboard/ml_http_access_explorer_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/kibana/dashboard/ml_http_access_explorer_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/kibana/search/ml_http_access_filebeat_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/kibana/search/ml_http_access_filebeat_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/kibana/search/ml_http_access_filebeat_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/kibana/search/ml_http_access_filebeat_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/kibana/visualization/ml_http_access_events_timechart_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/kibana/visualization/ml_http_access_events_timechart_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/kibana/visualization/ml_http_access_events_timechart_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/kibana/visualization/ml_http_access_events_timechart_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/kibana/visualization/ml_http_access_map_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/kibana/visualization/ml_http_access_map_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/kibana/visualization/ml_http_access_map_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/kibana/visualization/ml_http_access_map_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/kibana/visualization/ml_http_access_source_ip_timechart_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/kibana/visualization/ml_http_access_source_ip_timechart_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/kibana/visualization/ml_http_access_source_ip_timechart_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/kibana/visualization/ml_http_access_source_ip_timechart_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/kibana/visualization/ml_http_access_status_code_timechart_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/kibana/visualization/ml_http_access_status_code_timechart_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/kibana/visualization/ml_http_access_status_code_timechart_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/kibana/visualization/ml_http_access_status_code_timechart_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/kibana/visualization/ml_http_access_top_source_ips_table_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/kibana/visualization/ml_http_access_top_source_ips_table_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/kibana/visualization/ml_http_access_top_source_ips_table_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/kibana/visualization/ml_http_access_top_source_ips_table_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/kibana/visualization/ml_http_access_top_urls_table_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/kibana/visualization/ml_http_access_top_urls_table_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/kibana/visualization/ml_http_access_top_urls_table_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/kibana/visualization/ml_http_access_top_urls_table_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/kibana/visualization/ml_http_access_unique_count_url_timechart_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/kibana/visualization/ml_http_access_unique_count_url_timechart_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/kibana/visualization/ml_http_access_unique_count_url_timechart_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/kibana/visualization/ml_http_access_unique_count_url_timechart_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/logo.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/logo.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/logo.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/logo.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/manifest.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/manifest.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/manifest.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/manifest.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/datafeed_low_request_rate_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/datafeed_low_request_rate_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/datafeed_low_request_rate_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/datafeed_low_request_rate_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/datafeed_source_ip_request_rate_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/datafeed_source_ip_request_rate_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/datafeed_source_ip_request_rate_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/datafeed_source_ip_request_rate_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/datafeed_source_ip_url_count_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/datafeed_source_ip_url_count_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/datafeed_source_ip_url_count_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/datafeed_source_ip_url_count_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/datafeed_status_code_rate_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/datafeed_status_code_rate_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/datafeed_status_code_rate_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/datafeed_status_code_rate_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/datafeed_visitor_rate_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/datafeed_visitor_rate_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/datafeed_visitor_rate_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/datafeed_visitor_rate_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/low_request_rate_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/low_request_rate_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/low_request_rate_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/low_request_rate_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/source_ip_request_rate_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/source_ip_request_rate_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/source_ip_request_rate_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/source_ip_request_rate_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/source_ip_url_count_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/source_ip_url_count_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/source_ip_url_count_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/source_ip_url_count_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/status_code_rate_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/status_code_rate_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/status_code_rate_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/status_code_rate_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/visitor_rate_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/visitor_rate_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/visitor_rate_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/visitor_rate_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/sample_data_ecommerce/logo.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/sample_data_ecommerce/logo.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/sample_data_ecommerce/logo.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/sample_data_ecommerce/logo.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/sample_data_ecommerce/manifest.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/sample_data_ecommerce/manifest.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/sample_data_ecommerce/manifest.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/sample_data_ecommerce/manifest.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/sample_data_ecommerce/ml/datafeed_high_sum_total_sales.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/sample_data_ecommerce/ml/datafeed_high_sum_total_sales.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/sample_data_ecommerce/ml/datafeed_high_sum_total_sales.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/sample_data_ecommerce/ml/datafeed_high_sum_total_sales.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/sample_data_ecommerce/ml/high_sum_total_sales.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/sample_data_ecommerce/ml/high_sum_total_sales.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/sample_data_ecommerce/ml/high_sum_total_sales.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/sample_data_ecommerce/ml/high_sum_total_sales.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/sample_data_weblogs/logo.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/sample_data_weblogs/logo.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/sample_data_weblogs/logo.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/sample_data_weblogs/logo.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/sample_data_weblogs/manifest.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/sample_data_weblogs/manifest.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/sample_data_weblogs/manifest.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/sample_data_weblogs/manifest.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/sample_data_weblogs/ml/datafeed_low_request_rate.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/sample_data_weblogs/ml/datafeed_low_request_rate.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/sample_data_weblogs/ml/datafeed_low_request_rate.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/sample_data_weblogs/ml/datafeed_low_request_rate.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/sample_data_weblogs/ml/datafeed_response_code_rates.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/sample_data_weblogs/ml/datafeed_response_code_rates.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/sample_data_weblogs/ml/datafeed_response_code_rates.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/sample_data_weblogs/ml/datafeed_response_code_rates.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/sample_data_weblogs/ml/datafeed_url_scanning.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/sample_data_weblogs/ml/datafeed_url_scanning.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/sample_data_weblogs/ml/datafeed_url_scanning.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/sample_data_weblogs/ml/datafeed_url_scanning.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/sample_data_weblogs/ml/low_request_rate.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/sample_data_weblogs/ml/low_request_rate.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/sample_data_weblogs/ml/low_request_rate.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/sample_data_weblogs/ml/low_request_rate.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/sample_data_weblogs/ml/response_code_rates.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/sample_data_weblogs/ml/response_code_rates.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/sample_data_weblogs/ml/response_code_rates.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/sample_data_weblogs/ml/response_code_rates.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/sample_data_weblogs/ml/url_scanning.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/sample_data_weblogs/ml/url_scanning.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/sample_data_weblogs/ml/url_scanning.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/sample_data_weblogs/ml/url_scanning.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/logo.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/logo.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/logo.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/logo.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/manifest.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/manifest.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/manifest.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/manifest.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/datafeed_linux_anomalous_network_activity_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/datafeed_linux_anomalous_network_activity_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/datafeed_linux_anomalous_network_activity_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/datafeed_linux_anomalous_network_activity_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/datafeed_linux_anomalous_network_port_activity_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/datafeed_linux_anomalous_network_port_activity_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/datafeed_linux_anomalous_network_port_activity_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/datafeed_linux_anomalous_network_port_activity_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/datafeed_linux_anomalous_network_service.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/datafeed_linux_anomalous_network_service.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/datafeed_linux_anomalous_network_service.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/datafeed_linux_anomalous_network_service.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/datafeed_linux_anomalous_network_url_activity_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/datafeed_linux_anomalous_network_url_activity_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/datafeed_linux_anomalous_network_url_activity_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/datafeed_linux_anomalous_network_url_activity_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/datafeed_linux_anomalous_process_all_hosts_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/datafeed_linux_anomalous_process_all_hosts_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/datafeed_linux_anomalous_process_all_hosts_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/datafeed_linux_anomalous_process_all_hosts_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/datafeed_linux_anomalous_user_name_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/datafeed_linux_anomalous_user_name_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/datafeed_linux_anomalous_user_name_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/datafeed_linux_anomalous_user_name_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/datafeed_rare_process_by_host_linux_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/datafeed_rare_process_by_host_linux_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/datafeed_rare_process_by_host_linux_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/datafeed_rare_process_by_host_linux_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_anomalous_network_activity_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_anomalous_network_activity_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_anomalous_network_activity_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_anomalous_network_activity_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_anomalous_network_port_activity_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_anomalous_network_port_activity_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_anomalous_network_port_activity_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_anomalous_network_port_activity_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_anomalous_network_service.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_anomalous_network_service.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_anomalous_network_service.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_anomalous_network_service.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_anomalous_network_url_activity_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_anomalous_network_url_activity_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_anomalous_network_url_activity_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_anomalous_network_url_activity_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_anomalous_process_all_hosts_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_anomalous_process_all_hosts_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_anomalous_process_all_hosts_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_anomalous_process_all_hosts_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_anomalous_user_name_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_anomalous_user_name_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_anomalous_user_name_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_anomalous_user_name_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/rare_process_by_host_linux_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/rare_process_by_host_linux_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/rare_process_by_host_linux_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/rare_process_by_host_linux_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat_auth/logo.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat_auth/logo.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat_auth/logo.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat_auth/logo.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat_auth/manifest.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat_auth/manifest.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat_auth/manifest.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat_auth/manifest.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat_auth/ml/datafeed_suspicious_login_activity_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat_auth/ml/datafeed_suspicious_login_activity_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat_auth/ml/datafeed_suspicious_login_activity_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat_auth/ml/datafeed_suspicious_login_activity_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat_auth/ml/suspicious_login_activity_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat_auth/ml/suspicious_login_activity_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat_auth/ml/suspicious_login_activity_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat_auth/ml/suspicious_login_activity_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/logo.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/logo.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/logo.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/logo.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/manifest.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/manifest.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/manifest.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/manifest.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/ml/datafeed_packetbeat_dns_tunneling.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/ml/datafeed_packetbeat_dns_tunneling.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/ml/datafeed_packetbeat_dns_tunneling.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/ml/datafeed_packetbeat_dns_tunneling.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/ml/datafeed_packetbeat_rare_dns_question.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/ml/datafeed_packetbeat_rare_dns_question.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/ml/datafeed_packetbeat_rare_dns_question.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/ml/datafeed_packetbeat_rare_dns_question.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/ml/datafeed_packetbeat_rare_server_domain.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/ml/datafeed_packetbeat_rare_server_domain.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/ml/datafeed_packetbeat_rare_server_domain.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/ml/datafeed_packetbeat_rare_server_domain.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/ml/datafeed_packetbeat_rare_urls.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/ml/datafeed_packetbeat_rare_urls.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/ml/datafeed_packetbeat_rare_urls.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/ml/datafeed_packetbeat_rare_urls.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/ml/datafeed_packetbeat_rare_user_agent.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/ml/datafeed_packetbeat_rare_user_agent.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/ml/datafeed_packetbeat_rare_user_agent.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/ml/datafeed_packetbeat_rare_user_agent.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/ml/packetbeat_dns_tunneling.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/ml/packetbeat_dns_tunneling.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/ml/packetbeat_dns_tunneling.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/ml/packetbeat_dns_tunneling.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/ml/packetbeat_rare_dns_question.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/ml/packetbeat_rare_dns_question.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/ml/packetbeat_rare_dns_question.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/ml/packetbeat_rare_dns_question.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/ml/packetbeat_rare_server_domain.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/ml/packetbeat_rare_server_domain.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/ml/packetbeat_rare_server_domain.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/ml/packetbeat_rare_server_domain.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/ml/packetbeat_rare_urls.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/ml/packetbeat_rare_urls.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/ml/packetbeat_rare_urls.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/ml/packetbeat_rare_urls.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/ml/packetbeat_rare_user_agent.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/ml/packetbeat_rare_user_agent.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/ml/packetbeat_rare_user_agent.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/ml/packetbeat_rare_user_agent.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/logo.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/logo.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/logo.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/logo.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/manifest.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/manifest.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/manifest.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/manifest.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/datafeed_rare_process_by_host_windows_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/datafeed_rare_process_by_host_windows_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/datafeed_rare_process_by_host_windows_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/datafeed_rare_process_by_host_windows_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/datafeed_windows_anomalous_network_activity_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/datafeed_windows_anomalous_network_activity_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/datafeed_windows_anomalous_network_activity_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/datafeed_windows_anomalous_network_activity_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/datafeed_windows_anomalous_path_activity_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/datafeed_windows_anomalous_path_activity_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/datafeed_windows_anomalous_path_activity_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/datafeed_windows_anomalous_path_activity_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/datafeed_windows_anomalous_process_all_hosts_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/datafeed_windows_anomalous_process_all_hosts_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/datafeed_windows_anomalous_process_all_hosts_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/datafeed_windows_anomalous_process_all_hosts_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/datafeed_windows_anomalous_process_creation.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/datafeed_windows_anomalous_process_creation.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/datafeed_windows_anomalous_process_creation.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/datafeed_windows_anomalous_process_creation.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/datafeed_windows_anomalous_script.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/datafeed_windows_anomalous_script.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/datafeed_windows_anomalous_script.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/datafeed_windows_anomalous_script.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/datafeed_windows_anomalous_service.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/datafeed_windows_anomalous_service.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/datafeed_windows_anomalous_service.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/datafeed_windows_anomalous_service.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/datafeed_windows_anomalous_user_name_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/datafeed_windows_anomalous_user_name_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/datafeed_windows_anomalous_user_name_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/datafeed_windows_anomalous_user_name_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/datafeed_windows_rare_user_runas_event.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/datafeed_windows_rare_user_runas_event.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/datafeed_windows_rare_user_runas_event.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/datafeed_windows_rare_user_runas_event.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/rare_process_by_host_windows_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/rare_process_by_host_windows_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/rare_process_by_host_windows_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/rare_process_by_host_windows_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_anomalous_network_activity_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_anomalous_network_activity_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_anomalous_network_activity_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_anomalous_network_activity_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_anomalous_path_activity_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_anomalous_path_activity_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_anomalous_path_activity_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_anomalous_path_activity_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_anomalous_process_all_hosts_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_anomalous_process_all_hosts_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_anomalous_process_all_hosts_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_anomalous_process_all_hosts_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_anomalous_process_creation.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_anomalous_process_creation.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_anomalous_process_creation.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_anomalous_process_creation.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_anomalous_script.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_anomalous_script.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_anomalous_script.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_anomalous_script.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_anomalous_service.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_anomalous_service.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_anomalous_service.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_anomalous_service.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_anomalous_user_name_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_anomalous_user_name_ecs.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_anomalous_user_name_ecs.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_anomalous_user_name_ecs.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_rare_user_runas_event.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_rare_user_runas_event.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_rare_user_runas_event.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_rare_user_runas_event.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat_auth/logo.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat_auth/logo.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat_auth/logo.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat_auth/logo.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat_auth/manifest.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat_auth/manifest.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat_auth/manifest.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat_auth/manifest.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat_auth/ml/datafeed_windows_rare_user_type10_remote_login.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat_auth/ml/datafeed_windows_rare_user_type10_remote_login.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat_auth/ml/datafeed_windows_rare_user_type10_remote_login.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat_auth/ml/datafeed_windows_rare_user_type10_remote_login.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat_auth/ml/windows_rare_user_type10_remote_login.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat_auth/ml/windows_rare_user_type10_remote_login.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat_auth/ml/windows_rare_user_type10_remote_login.json rename to x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat_auth/ml/windows_rare_user_type10_remote_login.json diff --git a/x-pack/legacy/plugins/ml/server/models/data_visualizer/data_visualizer.ts b/x-pack/plugins/ml/server/models/data_visualizer/data_visualizer.ts similarity index 99% rename from x-pack/legacy/plugins/ml/server/models/data_visualizer/data_visualizer.ts rename to x-pack/plugins/ml/server/models/data_visualizer/data_visualizer.ts index b0a61b1232dc0..9463f74e1e746 100644 --- a/x-pack/legacy/plugins/ml/server/models/data_visualizer/data_visualizer.ts +++ b/x-pack/plugins/ml/server/models/data_visualizer/data_visualizer.ts @@ -6,8 +6,8 @@ import { CallAPIOptions, IScopedClusterClient } from 'src/core/server'; import _ from 'lodash'; -import { ML_JOB_FIELD_TYPES } from '../../../common/constants/field_types'; -import { getSafeAggregationName } from '../../../common/util/job_utils'; +import { ML_JOB_FIELD_TYPES } from '../../../../../legacy/plugins/ml/common/constants/field_types'; +import { getSafeAggregationName } from '../../../../../legacy/plugins/ml/common/util/job_utils'; import { buildBaseFilterCriteria, buildSamplerAggregation, diff --git a/x-pack/legacy/plugins/ml/server/models/data_visualizer/index.ts b/x-pack/plugins/ml/server/models/data_visualizer/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/data_visualizer/index.ts rename to x-pack/plugins/ml/server/models/data_visualizer/index.ts diff --git a/x-pack/legacy/plugins/ml/server/models/fields_service/fields_service.d.ts b/x-pack/plugins/ml/server/models/fields_service/fields_service.d.ts similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/fields_service/fields_service.d.ts rename to x-pack/plugins/ml/server/models/fields_service/fields_service.d.ts diff --git a/x-pack/legacy/plugins/ml/server/models/fields_service/fields_service.js b/x-pack/plugins/ml/server/models/fields_service/fields_service.js similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/fields_service/fields_service.js rename to x-pack/plugins/ml/server/models/fields_service/fields_service.js diff --git a/x-pack/legacy/plugins/ml/server/models/fields_service/index.ts b/x-pack/plugins/ml/server/models/fields_service/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/fields_service/index.ts rename to x-pack/plugins/ml/server/models/fields_service/index.ts diff --git a/x-pack/legacy/plugins/ml/server/models/file_data_visualizer/file_data_visualizer.ts b/x-pack/plugins/ml/server/models/file_data_visualizer/file_data_visualizer.ts similarity index 95% rename from x-pack/legacy/plugins/ml/server/models/file_data_visualizer/file_data_visualizer.ts rename to x-pack/plugins/ml/server/models/file_data_visualizer/file_data_visualizer.ts index 9f30f609c60b6..1d0452f2337f9 100644 --- a/x-pack/legacy/plugins/ml/server/models/file_data_visualizer/file_data_visualizer.ts +++ b/x-pack/plugins/ml/server/models/file_data_visualizer/file_data_visualizer.ts @@ -6,7 +6,7 @@ import Boom from 'boom'; import { RequestHandlerContext } from 'kibana/server'; -import { FindFileStructureResponse } from '../../../common/types/file_datavisualizer'; +import { FindFileStructureResponse } from '../../../../../legacy/plugins/ml/common/types/file_datavisualizer'; export type InputData = any[]; diff --git a/x-pack/legacy/plugins/ml/server/models/file_data_visualizer/import_data.ts b/x-pack/plugins/ml/server/models/file_data_visualizer/import_data.ts similarity index 97% rename from x-pack/legacy/plugins/ml/server/models/file_data_visualizer/import_data.ts rename to x-pack/plugins/ml/server/models/file_data_visualizer/import_data.ts index 008efb43a6c07..e4de71ad0793d 100644 --- a/x-pack/legacy/plugins/ml/server/models/file_data_visualizer/import_data.ts +++ b/x-pack/plugins/ml/server/models/file_data_visualizer/import_data.ts @@ -5,7 +5,7 @@ */ import { RequestHandlerContext } from 'kibana/server'; -import { INDEX_META_DATA_CREATED_BY } from '../../../common/constants/file_datavisualizer'; +import { INDEX_META_DATA_CREATED_BY } from '../../../../../legacy/plugins/ml/common/constants/file_datavisualizer'; import { InputData } from './file_data_visualizer'; export interface Settings { diff --git a/x-pack/legacy/plugins/ml/server/models/file_data_visualizer/index.ts b/x-pack/plugins/ml/server/models/file_data_visualizer/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/file_data_visualizer/index.ts rename to x-pack/plugins/ml/server/models/file_data_visualizer/index.ts diff --git a/x-pack/legacy/plugins/ml/server/models/filter/filter_manager.ts b/x-pack/plugins/ml/server/models/filter/filter_manager.ts similarity index 98% rename from x-pack/legacy/plugins/ml/server/models/filter/filter_manager.ts rename to x-pack/plugins/ml/server/models/filter/filter_manager.ts index f40663a5eb6b2..baba495257aca 100644 --- a/x-pack/legacy/plugins/ml/server/models/filter/filter_manager.ts +++ b/x-pack/plugins/ml/server/models/filter/filter_manager.ts @@ -7,7 +7,10 @@ import Boom from 'boom'; import { IScopedClusterClient } from 'src/core/server'; -import { DetectorRule, DetectorRuleScope } from '../../../common/types/detector_rules'; +import { + DetectorRule, + DetectorRuleScope, +} from '../../../../../legacy/plugins/ml/common/types/detector_rules'; export interface Filter { filter_id: string; diff --git a/x-pack/legacy/plugins/ml/server/models/filter/index.js b/x-pack/plugins/ml/server/models/filter/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/filter/index.js rename to x-pack/plugins/ml/server/models/filter/index.js diff --git a/x-pack/legacy/plugins/ml/server/models/filter/index.ts b/x-pack/plugins/ml/server/models/filter/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/filter/index.ts rename to x-pack/plugins/ml/server/models/filter/index.ts diff --git a/x-pack/legacy/plugins/ml/server/models/job_audit_messages/index.ts b/x-pack/plugins/ml/server/models/job_audit_messages/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/job_audit_messages/index.ts rename to x-pack/plugins/ml/server/models/job_audit_messages/index.ts diff --git a/x-pack/legacy/plugins/ml/server/models/job_audit_messages/job_audit_messages.d.ts b/x-pack/plugins/ml/server/models/job_audit_messages/job_audit_messages.d.ts similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/job_audit_messages/job_audit_messages.d.ts rename to x-pack/plugins/ml/server/models/job_audit_messages/job_audit_messages.d.ts diff --git a/x-pack/legacy/plugins/ml/server/models/job_audit_messages/job_audit_messages.js b/x-pack/plugins/ml/server/models/job_audit_messages/job_audit_messages.js similarity index 98% rename from x-pack/legacy/plugins/ml/server/models/job_audit_messages/job_audit_messages.js rename to x-pack/plugins/ml/server/models/job_audit_messages/job_audit_messages.js index 2cdfc0ef4f4c5..b434846d6f0f4 100644 --- a/x-pack/legacy/plugins/ml/server/models/job_audit_messages/job_audit_messages.js +++ b/x-pack/plugins/ml/server/models/job_audit_messages/job_audit_messages.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ML_NOTIFICATION_INDEX_PATTERN } from '../../../common/constants/index_patterns'; +import { ML_NOTIFICATION_INDEX_PATTERN } from '../../../../../legacy/plugins/ml/common/constants/index_patterns'; import moment from 'moment'; const SIZE = 1000; diff --git a/x-pack/legacy/plugins/ml/server/models/job_service/datafeeds.js b/x-pack/plugins/ml/server/models/job_service/datafeeds.js similarity index 97% rename from x-pack/legacy/plugins/ml/server/models/job_service/datafeeds.js rename to x-pack/plugins/ml/server/models/job_service/datafeeds.js index c3b54fff0682d..961b712610512 100644 --- a/x-pack/legacy/plugins/ml/server/models/job_service/datafeeds.js +++ b/x-pack/plugins/ml/server/models/job_service/datafeeds.js @@ -5,7 +5,10 @@ */ import { i18n } from '@kbn/i18n'; -import { JOB_STATE, DATAFEED_STATE } from '../../../common/constants/states'; +import { + JOB_STATE, + DATAFEED_STATE, +} from '../../../../../legacy/plugins/ml/common/constants/states'; import { fillResultsWithTimeouts, isRequestTimeout } from './error_utils'; export function datafeedsProvider(callWithRequest) { diff --git a/x-pack/legacy/plugins/ml/server/models/job_service/error_utils.js b/x-pack/plugins/ml/server/models/job_service/error_utils.js similarity index 94% rename from x-pack/legacy/plugins/ml/server/models/job_service/error_utils.js rename to x-pack/plugins/ml/server/models/job_service/error_utils.js index 6f25b5870f85c..21e45110e7093 100644 --- a/x-pack/legacy/plugins/ml/server/models/job_service/error_utils.js +++ b/x-pack/plugins/ml/server/models/job_service/error_utils.js @@ -5,7 +5,10 @@ */ import { i18n } from '@kbn/i18n'; -import { JOB_STATE, DATAFEED_STATE } from '../../../common/constants/states'; +import { + JOB_STATE, + DATAFEED_STATE, +} from '../../../../../legacy/plugins/ml/common/constants/states'; const REQUEST_TIMEOUT = 'RequestTimeout'; diff --git a/x-pack/legacy/plugins/ml/server/models/job_service/groups.js b/x-pack/plugins/ml/server/models/job_service/groups.js similarity index 95% rename from x-pack/legacy/plugins/ml/server/models/job_service/groups.js rename to x-pack/plugins/ml/server/models/job_service/groups.js index 6fbc071ef9854..b30e9cdc6048b 100644 --- a/x-pack/legacy/plugins/ml/server/models/job_service/groups.js +++ b/x-pack/plugins/ml/server/models/job_service/groups.js @@ -5,7 +5,7 @@ */ import { CalendarManager } from '../calendar'; -import { GLOBAL_CALENDAR } from '../../../common/constants/calendars'; +import { GLOBAL_CALENDAR } from '../../../../../legacy/plugins/ml/common/constants/calendars'; export function groupsProvider(callWithRequest) { const calMngr = new CalendarManager(callWithRequest); diff --git a/x-pack/legacy/plugins/ml/server/models/job_service/index.js b/x-pack/plugins/ml/server/models/job_service/index.js similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/job_service/index.js rename to x-pack/plugins/ml/server/models/job_service/index.js diff --git a/x-pack/legacy/plugins/ml/server/models/job_service/jobs.js b/x-pack/plugins/ml/server/models/job_service/jobs.js similarity index 98% rename from x-pack/legacy/plugins/ml/server/models/job_service/jobs.js rename to x-pack/plugins/ml/server/models/job_service/jobs.js index b4b476c1f926e..16d3c30bb0a28 100644 --- a/x-pack/legacy/plugins/ml/server/models/job_service/jobs.js +++ b/x-pack/plugins/ml/server/models/job_service/jobs.js @@ -5,7 +5,10 @@ */ import { i18n } from '@kbn/i18n'; -import { JOB_STATE, DATAFEED_STATE } from '../../../common/constants/states'; +import { + JOB_STATE, + DATAFEED_STATE, +} from '../../../../../legacy/plugins/ml/common/constants/states'; import { datafeedsProvider } from './datafeeds'; import { jobAuditMessagesProvider } from '../job_audit_messages'; import { resultsServiceProvider } from '../results_service'; @@ -14,7 +17,7 @@ import { fillResultsWithTimeouts, isRequestTimeout } from './error_utils'; import { getLatestDataOrBucketTimestamp, isTimeSeriesViewJob, -} from '../../../common/util/job_utils'; +} from '../../../../../legacy/plugins/ml/common/util/job_utils'; import { groupsProvider } from './groups'; import { uniq } from 'lodash'; diff --git a/x-pack/legacy/plugins/ml/server/models/job_service/new_job/categorization/examples.ts b/x-pack/plugins/ml/server/models/job_service/new_job/categorization/examples.ts similarity index 95% rename from x-pack/legacy/plugins/ml/server/models/job_service/new_job/categorization/examples.ts rename to x-pack/plugins/ml/server/models/job_service/new_job/categorization/examples.ts index ea2c71b04f56d..1a098fdf16bb7 100644 --- a/x-pack/legacy/plugins/ml/server/models/job_service/new_job/categorization/examples.ts +++ b/x-pack/plugins/ml/server/models/job_service/new_job/categorization/examples.ts @@ -6,13 +6,13 @@ import { chunk } from 'lodash'; import { SearchResponse } from 'elasticsearch'; -import { CATEGORY_EXAMPLES_SAMPLE_SIZE } from '../../../../../common/constants/new_job'; +import { CATEGORY_EXAMPLES_SAMPLE_SIZE } from '../../../../../../../legacy/plugins/ml/common/constants/new_job'; import { Token, CategorizationAnalyzer, CategoryFieldExample, -} from '../../../../../common/types/categories'; -import { callWithRequestType } from '../../../../../common/types/kibana'; +} from '../../../../../../../legacy/plugins/ml/common/types/categories'; +import { callWithRequestType } from '../../../../../../../legacy/plugins/ml/common/types/kibana'; import { ValidationResults } from './validation_results'; const CHUNK_SIZE = 100; diff --git a/x-pack/legacy/plugins/ml/server/models/job_service/new_job/categorization/index.ts b/x-pack/plugins/ml/server/models/job_service/new_job/categorization/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/job_service/new_job/categorization/index.ts rename to x-pack/plugins/ml/server/models/job_service/new_job/categorization/index.ts diff --git a/x-pack/legacy/plugins/ml/server/models/job_service/new_job/categorization/top_categories.ts b/x-pack/plugins/ml/server/models/job_service/new_job/categorization/top_categories.ts similarity index 92% rename from x-pack/legacy/plugins/ml/server/models/job_service/new_job/categorization/top_categories.ts rename to x-pack/plugins/ml/server/models/job_service/new_job/categorization/top_categories.ts index 3361cc454e2b7..c8eb0002a31c8 100644 --- a/x-pack/legacy/plugins/ml/server/models/job_service/new_job/categorization/top_categories.ts +++ b/x-pack/plugins/ml/server/models/job_service/new_job/categorization/top_categories.ts @@ -5,9 +5,12 @@ */ import { SearchResponse } from 'elasticsearch'; -import { ML_RESULTS_INDEX_PATTERN } from '../../../../../common/constants/index_patterns'; -import { CategoryId, Category } from '../../../../../common/types/categories'; -import { callWithRequestType } from '../../../../../common/types/kibana'; +import { ML_RESULTS_INDEX_PATTERN } from '../../../../../../../legacy/plugins/ml/common/constants/index_patterns'; +import { + CategoryId, + Category, +} from '../../../../../../../legacy/plugins/ml/common/types/categories'; +import { callWithRequestType } from '../../../../../../../legacy/plugins/ml/common/types/kibana'; export function topCategoriesProvider(callWithRequest: callWithRequestType) { async function getTotalCategories(jobId: string): Promise<{ total: number }> { diff --git a/x-pack/legacy/plugins/ml/server/models/job_service/new_job/categorization/validation_results.ts b/x-pack/plugins/ml/server/models/job_service/new_job/categorization/validation_results.ts similarity index 96% rename from x-pack/legacy/plugins/ml/server/models/job_service/new_job/categorization/validation_results.ts rename to x-pack/plugins/ml/server/models/job_service/new_job/categorization/validation_results.ts index 34e63eabb405e..bb1106b4d6396 100644 --- a/x-pack/legacy/plugins/ml/server/models/job_service/new_job/categorization/validation_results.ts +++ b/x-pack/plugins/ml/server/models/job_service/new_job/categorization/validation_results.ts @@ -9,13 +9,13 @@ import { CATEGORY_EXAMPLES_VALIDATION_STATUS, CATEGORY_EXAMPLES_ERROR_LIMIT, CATEGORY_EXAMPLES_WARNING_LIMIT, -} from '../../../../../common/constants/new_job'; +} from '../../../../../../../legacy/plugins/ml/common/constants/new_job'; import { FieldExampleCheck, CategoryFieldExample, VALIDATION_RESULT, -} from '../../../../../common/types/categories'; -import { getMedianStringLength } from '../../../../../common/util/string_utils'; +} from '../../../../../../../legacy/plugins/ml/common/types/categories'; +import { getMedianStringLength } from '../../../../../../../legacy/plugins/ml/common/util/string_utils'; const VALID_TOKEN_COUNT = 3; const MEDIAN_LINE_LENGTH_LIMIT = 400; diff --git a/x-pack/legacy/plugins/ml/server/models/job_service/new_job/charts.ts b/x-pack/plugins/ml/server/models/job_service/new_job/charts.ts similarity index 87% rename from x-pack/legacy/plugins/ml/server/models/job_service/new_job/charts.ts rename to x-pack/plugins/ml/server/models/job_service/new_job/charts.ts index 88ae8caa91e4a..e662e3ca03ded 100644 --- a/x-pack/legacy/plugins/ml/server/models/job_service/new_job/charts.ts +++ b/x-pack/plugins/ml/server/models/job_service/new_job/charts.ts @@ -6,7 +6,7 @@ import { newJobLineChartProvider } from './line_chart'; import { newJobPopulationChartProvider } from './population_chart'; -import { callWithRequestType } from '../../../../common/types/kibana'; +import { callWithRequestType } from '../../../../../../legacy/plugins/ml/common/types/kibana'; export function newJobChartsProvider(callWithRequest: callWithRequestType) { const { newJobLineChart } = newJobLineChartProvider(callWithRequest); diff --git a/x-pack/legacy/plugins/ml/server/models/job_service/new_job/index.ts b/x-pack/plugins/ml/server/models/job_service/new_job/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/job_service/new_job/index.ts rename to x-pack/plugins/ml/server/models/job_service/new_job/index.ts diff --git a/x-pack/legacy/plugins/ml/server/models/job_service/new_job/line_chart.ts b/x-pack/plugins/ml/server/models/job_service/new_job/line_chart.ts similarity index 92% rename from x-pack/legacy/plugins/ml/server/models/job_service/new_job/line_chart.ts rename to x-pack/plugins/ml/server/models/job_service/new_job/line_chart.ts index c1a5ad5e38ecc..3dfe935c655d5 100644 --- a/x-pack/legacy/plugins/ml/server/models/job_service/new_job/line_chart.ts +++ b/x-pack/plugins/ml/server/models/job_service/new_job/line_chart.ts @@ -5,9 +5,12 @@ */ import { get } from 'lodash'; -import { AggFieldNamePair, EVENT_RATE_FIELD_ID } from '../../../../common/types/fields'; -import { callWithRequestType } from '../../../../common/types/kibana'; -import { ML_MEDIAN_PERCENTS } from '../../../../common/util/job_utils'; +import { + AggFieldNamePair, + EVENT_RATE_FIELD_ID, +} from '../../../../../../legacy/plugins/ml/common/types/fields'; +import { callWithRequestType } from '../../../../../../legacy/plugins/ml/common/types/kibana'; +import { ML_MEDIAN_PERCENTS } from '../../../../../../legacy/plugins/ml/common/util/job_utils'; type DtrIndex = number; type TimeStamp = number; diff --git a/x-pack/legacy/plugins/ml/server/models/job_service/new_job/population_chart.ts b/x-pack/plugins/ml/server/models/job_service/new_job/population_chart.ts similarity index 95% rename from x-pack/legacy/plugins/ml/server/models/job_service/new_job/population_chart.ts rename to x-pack/plugins/ml/server/models/job_service/new_job/population_chart.ts index ee35f13c44ee6..d1ef9773f8f17 100644 --- a/x-pack/legacy/plugins/ml/server/models/job_service/new_job/population_chart.ts +++ b/x-pack/plugins/ml/server/models/job_service/new_job/population_chart.ts @@ -5,9 +5,12 @@ */ import { get } from 'lodash'; -import { AggFieldNamePair, EVENT_RATE_FIELD_ID } from '../../../../common/types/fields'; -import { callWithRequestType } from '../../../../common/types/kibana'; -import { ML_MEDIAN_PERCENTS } from '../../../../common/util/job_utils'; +import { + AggFieldNamePair, + EVENT_RATE_FIELD_ID, +} from '../../../../../../legacy/plugins/ml/common/types/fields'; +import { callWithRequestType } from '../../../../../../legacy/plugins/ml/common/types/kibana'; +import { ML_MEDIAN_PERCENTS } from '../../../../../../legacy/plugins/ml/common/util/job_utils'; const OVER_FIELD_EXAMPLES_COUNT = 40; diff --git a/x-pack/legacy/plugins/ml/server/models/job_service/new_job_caps/__mocks__/responses/cloudwatch_field_caps.json b/x-pack/plugins/ml/server/models/job_service/new_job_caps/__mocks__/responses/cloudwatch_field_caps.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/job_service/new_job_caps/__mocks__/responses/cloudwatch_field_caps.json rename to x-pack/plugins/ml/server/models/job_service/new_job_caps/__mocks__/responses/cloudwatch_field_caps.json diff --git a/x-pack/legacy/plugins/ml/server/models/job_service/new_job_caps/__mocks__/responses/farequote_field_caps.json b/x-pack/plugins/ml/server/models/job_service/new_job_caps/__mocks__/responses/farequote_field_caps.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/job_service/new_job_caps/__mocks__/responses/farequote_field_caps.json rename to x-pack/plugins/ml/server/models/job_service/new_job_caps/__mocks__/responses/farequote_field_caps.json diff --git a/x-pack/legacy/plugins/ml/server/models/job_service/new_job_caps/__mocks__/responses/kibana_saved_objects.json b/x-pack/plugins/ml/server/models/job_service/new_job_caps/__mocks__/responses/kibana_saved_objects.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/job_service/new_job_caps/__mocks__/responses/kibana_saved_objects.json rename to x-pack/plugins/ml/server/models/job_service/new_job_caps/__mocks__/responses/kibana_saved_objects.json diff --git a/x-pack/legacy/plugins/ml/server/models/job_service/new_job_caps/__mocks__/responses/rollup_caps.json b/x-pack/plugins/ml/server/models/job_service/new_job_caps/__mocks__/responses/rollup_caps.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/job_service/new_job_caps/__mocks__/responses/rollup_caps.json rename to x-pack/plugins/ml/server/models/job_service/new_job_caps/__mocks__/responses/rollup_caps.json diff --git a/x-pack/legacy/plugins/ml/server/models/job_service/new_job_caps/__mocks__/results/cloudwatch_rollup_job_caps.json b/x-pack/plugins/ml/server/models/job_service/new_job_caps/__mocks__/results/cloudwatch_rollup_job_caps.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/job_service/new_job_caps/__mocks__/results/cloudwatch_rollup_job_caps.json rename to x-pack/plugins/ml/server/models/job_service/new_job_caps/__mocks__/results/cloudwatch_rollup_job_caps.json diff --git a/x-pack/legacy/plugins/ml/server/models/job_service/new_job_caps/__mocks__/results/farequote_job_caps.json b/x-pack/plugins/ml/server/models/job_service/new_job_caps/__mocks__/results/farequote_job_caps.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/job_service/new_job_caps/__mocks__/results/farequote_job_caps.json rename to x-pack/plugins/ml/server/models/job_service/new_job_caps/__mocks__/results/farequote_job_caps.json diff --git a/x-pack/legacy/plugins/ml/server/models/job_service/new_job_caps/__mocks__/results/farequote_job_caps_empty.json b/x-pack/plugins/ml/server/models/job_service/new_job_caps/__mocks__/results/farequote_job_caps_empty.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/job_service/new_job_caps/__mocks__/results/farequote_job_caps_empty.json rename to x-pack/plugins/ml/server/models/job_service/new_job_caps/__mocks__/results/farequote_job_caps_empty.json diff --git a/x-pack/legacy/plugins/ml/server/models/job_service/new_job_caps/aggregations.ts b/x-pack/plugins/ml/server/models/job_service/new_job_caps/aggregations.ts similarity index 97% rename from x-pack/legacy/plugins/ml/server/models/job_service/new_job_caps/aggregations.ts rename to x-pack/plugins/ml/server/models/job_service/new_job_caps/aggregations.ts index efe06f8b5ad4a..475612f276c72 100644 --- a/x-pack/legacy/plugins/ml/server/models/job_service/new_job_caps/aggregations.ts +++ b/x-pack/plugins/ml/server/models/job_service/new_job_caps/aggregations.ts @@ -4,12 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Aggregation, METRIC_AGG_TYPE } from '../../../../common/types/fields'; +import { + Aggregation, + METRIC_AGG_TYPE, +} from '../../../../../../legacy/plugins/ml/common/types/fields'; import { ML_JOB_AGGREGATION, KIBANA_AGGREGATION, ES_AGGREGATION, -} from '../../../../common/constants/aggregation_types'; +} from '../../../../../../legacy/plugins/ml/common/constants/aggregation_types'; // aggregation object missing id, title and fields and has null for kibana and dsl aggregation names. // this is used as the basis for the ML only aggregations diff --git a/x-pack/legacy/plugins/ml/server/models/job_service/new_job_caps/field_service.ts b/x-pack/plugins/ml/server/models/job_service/new_job_caps/field_service.ts similarity index 96% rename from x-pack/legacy/plugins/ml/server/models/job_service/new_job_caps/field_service.ts rename to x-pack/plugins/ml/server/models/job_service/new_job_caps/field_service.ts index 5827201a63661..446c71dd40f68 100644 --- a/x-pack/legacy/plugins/ml/server/models/job_service/new_job_caps/field_service.ts +++ b/x-pack/plugins/ml/server/models/job_service/new_job_caps/field_service.ts @@ -12,9 +12,9 @@ import { FieldId, NewJobCaps, METRIC_AGG_TYPE, -} from '../../../../common/types/fields'; -import { ES_FIELD_TYPES } from '../../../../../../../../src/plugins/data/server'; -import { ML_JOB_AGGREGATION } from '../../../../common/constants/aggregation_types'; +} from '../../../../../../legacy/plugins/ml/common/types/fields'; +import { ES_FIELD_TYPES } from '../../../../../../../src/plugins/data/server'; +import { ML_JOB_AGGREGATION } from '../../../../../../legacy/plugins/ml/common/constants/aggregation_types'; import { rollupServiceProvider, RollupJob, RollupFields } from './rollup'; import { aggregations, mlOnlyAggregations } from './aggregations'; diff --git a/x-pack/legacy/plugins/ml/server/models/job_service/new_job_caps/index.ts b/x-pack/plugins/ml/server/models/job_service/new_job_caps/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/job_service/new_job_caps/index.ts rename to x-pack/plugins/ml/server/models/job_service/new_job_caps/index.ts diff --git a/x-pack/legacy/plugins/ml/server/models/job_service/new_job_caps/new_job_caps.test.ts b/x-pack/plugins/ml/server/models/job_service/new_job_caps/new_job_caps.test.ts similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/job_service/new_job_caps/new_job_caps.test.ts rename to x-pack/plugins/ml/server/models/job_service/new_job_caps/new_job_caps.test.ts diff --git a/x-pack/legacy/plugins/ml/server/models/job_service/new_job_caps/new_job_caps.ts b/x-pack/plugins/ml/server/models/job_service/new_job_caps/new_job_caps.ts similarity index 93% rename from x-pack/legacy/plugins/ml/server/models/job_service/new_job_caps/new_job_caps.ts rename to x-pack/plugins/ml/server/models/job_service/new_job_caps/new_job_caps.ts index 3a9d979ccb22c..0a967c760a193 100644 --- a/x-pack/legacy/plugins/ml/server/models/job_service/new_job_caps/new_job_caps.ts +++ b/x-pack/plugins/ml/server/models/job_service/new_job_caps/new_job_caps.ts @@ -5,7 +5,11 @@ */ import { SavedObjectsClientContract } from 'kibana/server'; -import { Aggregation, Field, NewJobCaps } from '../../../../common/types/fields'; +import { + Aggregation, + Field, + NewJobCaps, +} from '../../../../../../legacy/plugins/ml/common/types/fields'; import { fieldServiceProvider } from './field_service'; interface NewJobCapsResponse { diff --git a/x-pack/legacy/plugins/ml/server/models/job_service/new_job_caps/rollup.ts b/x-pack/plugins/ml/server/models/job_service/new_job_caps/rollup.ts similarity index 92% rename from x-pack/legacy/plugins/ml/server/models/job_service/new_job_caps/rollup.ts rename to x-pack/plugins/ml/server/models/job_service/new_job_caps/rollup.ts index 1e9ce3d8d5022..4cbdfe4f360e0 100644 --- a/x-pack/legacy/plugins/ml/server/models/job_service/new_job_caps/rollup.ts +++ b/x-pack/plugins/ml/server/models/job_service/new_job_caps/rollup.ts @@ -7,8 +7,8 @@ import { SavedObject } from 'src/core/server'; import { IndexPatternAttributes } from 'src/plugins/data/server'; import { SavedObjectsClientContract } from 'kibana/server'; -import { FieldId } from '../../../../common/types/fields'; -import { ES_AGGREGATION } from '../../../../common/constants/aggregation_types'; +import { FieldId } from '../../../../../../legacy/plugins/ml/common/types/fields'; +import { ES_AGGREGATION } from '../../../../../../legacy/plugins/ml/common/constants/aggregation_types'; export type RollupFields = Record]>; diff --git a/x-pack/legacy/plugins/ml/server/models/job_validation/__tests__/job_validation.js b/x-pack/plugins/ml/server/models/job_validation/__tests__/job_validation.js similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/job_validation/__tests__/job_validation.js rename to x-pack/plugins/ml/server/models/job_validation/__tests__/job_validation.js diff --git a/x-pack/legacy/plugins/ml/server/models/job_validation/__tests__/mock_farequote_cardinality.json b/x-pack/plugins/ml/server/models/job_validation/__tests__/mock_farequote_cardinality.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/job_validation/__tests__/mock_farequote_cardinality.json rename to x-pack/plugins/ml/server/models/job_validation/__tests__/mock_farequote_cardinality.json diff --git a/x-pack/legacy/plugins/ml/server/models/job_validation/__tests__/mock_farequote_search_response.json b/x-pack/plugins/ml/server/models/job_validation/__tests__/mock_farequote_search_response.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/job_validation/__tests__/mock_farequote_search_response.json rename to x-pack/plugins/ml/server/models/job_validation/__tests__/mock_farequote_search_response.json diff --git a/x-pack/legacy/plugins/ml/server/models/job_validation/__tests__/mock_field_caps.json b/x-pack/plugins/ml/server/models/job_validation/__tests__/mock_field_caps.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/job_validation/__tests__/mock_field_caps.json rename to x-pack/plugins/ml/server/models/job_validation/__tests__/mock_field_caps.json diff --git a/x-pack/legacy/plugins/ml/server/models/job_validation/__tests__/mock_it_search_response.json b/x-pack/plugins/ml/server/models/job_validation/__tests__/mock_it_search_response.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/job_validation/__tests__/mock_it_search_response.json rename to x-pack/plugins/ml/server/models/job_validation/__tests__/mock_it_search_response.json diff --git a/x-pack/legacy/plugins/ml/server/models/job_validation/__tests__/mock_time_field.json b/x-pack/plugins/ml/server/models/job_validation/__tests__/mock_time_field.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/job_validation/__tests__/mock_time_field.json rename to x-pack/plugins/ml/server/models/job_validation/__tests__/mock_time_field.json diff --git a/x-pack/legacy/plugins/ml/server/models/job_validation/__tests__/mock_time_field_nested.json b/x-pack/plugins/ml/server/models/job_validation/__tests__/mock_time_field_nested.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/job_validation/__tests__/mock_time_field_nested.json rename to x-pack/plugins/ml/server/models/job_validation/__tests__/mock_time_field_nested.json diff --git a/x-pack/legacy/plugins/ml/server/models/job_validation/__tests__/mock_time_range.json b/x-pack/plugins/ml/server/models/job_validation/__tests__/mock_time_range.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/job_validation/__tests__/mock_time_range.json rename to x-pack/plugins/ml/server/models/job_validation/__tests__/mock_time_range.json diff --git a/x-pack/legacy/plugins/ml/server/models/job_validation/__tests__/validate_bucket_span.js b/x-pack/plugins/ml/server/models/job_validation/__tests__/validate_bucket_span.js similarity index 98% rename from x-pack/legacy/plugins/ml/server/models/job_validation/__tests__/validate_bucket_span.js rename to x-pack/plugins/ml/server/models/job_validation/__tests__/validate_bucket_span.js index 3dc2bee1e8705..023e0f5b614ed 100644 --- a/x-pack/legacy/plugins/ml/server/models/job_validation/__tests__/validate_bucket_span.js +++ b/x-pack/plugins/ml/server/models/job_validation/__tests__/validate_bucket_span.js @@ -6,7 +6,7 @@ import expect from '@kbn/expect'; import { validateBucketSpan } from '../validate_bucket_span'; -import { SKIP_BUCKET_SPAN_ESTIMATION } from '../../../../common/constants/validation'; +import { SKIP_BUCKET_SPAN_ESTIMATION } from '../../../../../../legacy/plugins/ml/common/constants/validation'; // farequote2017 snapshot snapshot mock search response // it returns a mock for the response of PolledDataChecker's search request diff --git a/x-pack/legacy/plugins/ml/server/models/job_validation/__tests__/validate_cardinality.js b/x-pack/plugins/ml/server/models/job_validation/__tests__/validate_cardinality.js similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/job_validation/__tests__/validate_cardinality.js rename to x-pack/plugins/ml/server/models/job_validation/__tests__/validate_cardinality.js diff --git a/x-pack/legacy/plugins/ml/server/models/job_validation/__tests__/validate_influencers.js b/x-pack/plugins/ml/server/models/job_validation/__tests__/validate_influencers.js similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/job_validation/__tests__/validate_influencers.js rename to x-pack/plugins/ml/server/models/job_validation/__tests__/validate_influencers.js diff --git a/x-pack/legacy/plugins/ml/server/models/job_validation/__tests__/validate_model_memory_limit.js b/x-pack/plugins/ml/server/models/job_validation/__tests__/validate_model_memory_limit.js similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/job_validation/__tests__/validate_model_memory_limit.js rename to x-pack/plugins/ml/server/models/job_validation/__tests__/validate_model_memory_limit.js diff --git a/x-pack/legacy/plugins/ml/server/models/job_validation/__tests__/validate_time_range.js b/x-pack/plugins/ml/server/models/job_validation/__tests__/validate_time_range.js similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/job_validation/__tests__/validate_time_range.js rename to x-pack/plugins/ml/server/models/job_validation/__tests__/validate_time_range.js diff --git a/x-pack/legacy/plugins/ml/server/models/job_validation/index.ts b/x-pack/plugins/ml/server/models/job_validation/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/job_validation/index.ts rename to x-pack/plugins/ml/server/models/job_validation/index.ts diff --git a/x-pack/legacy/plugins/ml/server/models/job_validation/job_validation.d.ts b/x-pack/plugins/ml/server/models/job_validation/job_validation.d.ts similarity index 83% rename from x-pack/legacy/plugins/ml/server/models/job_validation/job_validation.d.ts rename to x-pack/plugins/ml/server/models/job_validation/job_validation.d.ts index 4580602b0af23..bb8a372eaba30 100644 --- a/x-pack/legacy/plugins/ml/server/models/job_validation/job_validation.d.ts +++ b/x-pack/plugins/ml/server/models/job_validation/job_validation.d.ts @@ -6,7 +6,7 @@ import { APICaller } from 'src/core/server'; import { TypeOf } from '@kbn/config-schema'; -import { validateJobSchema } from '../../new_platform/job_validation_schema'; +import { validateJobSchema } from '../../routes/schemas/job_validation_schema'; type ValidateJobPayload = TypeOf; @@ -15,5 +15,5 @@ export function validateJob( payload: ValidateJobPayload, kbnVersion: string, callAsInternalUser: APICaller, - xpackMainPlugin: any + isSecurityDisabled: boolean ): string[]; diff --git a/x-pack/legacy/plugins/ml/server/models/job_validation/job_validation.js b/x-pack/plugins/ml/server/models/job_validation/job_validation.js similarity index 94% rename from x-pack/legacy/plugins/ml/server/models/job_validation/job_validation.js rename to x-pack/plugins/ml/server/models/job_validation/job_validation.js index ab1fbb39ee706..d453c9add97d1 100644 --- a/x-pack/legacy/plugins/ml/server/models/job_validation/job_validation.js +++ b/x-pack/plugins/ml/server/models/job_validation/job_validation.js @@ -8,11 +8,14 @@ import { i18n } from '@kbn/i18n'; import Boom from 'boom'; import { fieldsServiceProvider } from '../fields_service'; -import { renderTemplate } from '../../../common/util/string_utils'; +import { renderTemplate } from '../../../../../legacy/plugins/ml/common/util/string_utils'; import { getMessages } from './messages'; -import { VALIDATION_STATUS } from '../../../common/constants/validation'; +import { VALIDATION_STATUS } from '../../../../../legacy/plugins/ml/common/constants/validation'; -import { basicJobValidation, uniqWithIsEqual } from '../../../common/util/job_utils'; +import { + basicJobValidation, + uniqWithIsEqual, +} from '../../../../../legacy/plugins/ml/common/util/job_utils'; import { validateBucketSpan } from './validate_bucket_span'; import { validateCardinality } from './validate_cardinality'; import { validateInfluencers } from './validate_influencers'; @@ -24,7 +27,7 @@ export async function validateJob( payload, kbnVersion = 'current', callAsInternalUser, - xpackMainPlugin + isSecurityDisabled ) { const messages = getMessages(); @@ -112,7 +115,7 @@ export async function validateJob( job, duration, callAsInternalUser, - xpackMainPlugin + isSecurityDisabled )) ); validationMessages.push(...(await validateTimeRange(callWithRequest, job, duration))); diff --git a/x-pack/legacy/plugins/ml/server/models/job_validation/messages.js b/x-pack/plugins/ml/server/models/job_validation/messages.js similarity index 99% rename from x-pack/legacy/plugins/ml/server/models/job_validation/messages.js rename to x-pack/plugins/ml/server/models/job_validation/messages.js index 2c0c218bf86b5..33931f03facc3 100644 --- a/x-pack/legacy/plugins/ml/server/models/job_validation/messages.js +++ b/x-pack/plugins/ml/server/models/job_validation/messages.js @@ -5,7 +5,7 @@ */ import { i18n } from '@kbn/i18n'; -import { JOB_ID_MAX_LENGTH } from '../../../common/constants/validation'; +import { JOB_ID_MAX_LENGTH } from '../../../../../legacy/plugins/ml/common/constants/validation'; let messages; diff --git a/x-pack/legacy/plugins/ml/server/models/job_validation/validate_bucket_span.js b/x-pack/plugins/ml/server/models/job_validation/validate_bucket_span.js similarity index 93% rename from x-pack/legacy/plugins/ml/server/models/job_validation/validate_bucket_span.js rename to x-pack/plugins/ml/server/models/job_validation/validate_bucket_span.js index 2914f086c1a83..9e96e2219fb0f 100644 --- a/x-pack/legacy/plugins/ml/server/models/job_validation/validate_bucket_span.js +++ b/x-pack/plugins/ml/server/models/job_validation/validate_bucket_span.js @@ -5,9 +5,9 @@ */ import { estimateBucketSpanFactory } from '../../models/bucket_span_estimator'; -import { mlFunctionToESAggregation } from '../../../common/util/job_utils'; -import { SKIP_BUCKET_SPAN_ESTIMATION } from '../../../common/constants/validation'; -import { parseInterval } from '../../../common/util/parse_interval'; +import { mlFunctionToESAggregation } from '../../../../../legacy/plugins/ml/common/util/job_utils'; +import { SKIP_BUCKET_SPAN_ESTIMATION } from '../../../../../legacy/plugins/ml/common/constants/validation'; +import { parseInterval } from '../../../../../legacy/plugins/ml/common/util/parse_interval'; import { validateJobObject } from './validate_job_object'; @@ -51,7 +51,7 @@ export async function validateBucketSpan( job, duration, callAsInternalUser, - xpackMainPlugin + isSecurityDisabled ) { validateJobObject(job); @@ -124,7 +124,7 @@ export async function validateBucketSpan( estimateBucketSpanFactory( callWithRequest, callAsInternalUser, - xpackMainPlugin + isSecurityDisabled )(data) .then(resolve) // this catch gets triggered when the estimation code runs without error diff --git a/x-pack/legacy/plugins/ml/server/models/job_validation/validate_cardinality.d.ts b/x-pack/plugins/ml/server/models/job_validation/validate_cardinality.d.ts similarity index 78% rename from x-pack/legacy/plugins/ml/server/models/job_validation/validate_cardinality.d.ts rename to x-pack/plugins/ml/server/models/job_validation/validate_cardinality.d.ts index dc10905533788..d3930ecf44c8d 100644 --- a/x-pack/legacy/plugins/ml/server/models/job_validation/validate_cardinality.d.ts +++ b/x-pack/plugins/ml/server/models/job_validation/validate_cardinality.d.ts @@ -5,7 +5,10 @@ */ import { APICaller } from 'src/core/server'; -import { Job, Datafeed } from '../../../public/application/jobs/new_job/common/job_creator/configs'; +import { + Job, + Datafeed, +} from '../../../../../legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/configs'; interface ValidateCardinalityConfig extends Job { datafeed_config?: Datafeed; diff --git a/x-pack/legacy/plugins/ml/server/models/job_validation/validate_cardinality.js b/x-pack/plugins/ml/server/models/job_validation/validate_cardinality.js similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/job_validation/validate_cardinality.js rename to x-pack/plugins/ml/server/models/job_validation/validate_cardinality.js diff --git a/x-pack/legacy/plugins/ml/server/models/job_validation/validate_influencers.js b/x-pack/plugins/ml/server/models/job_validation/validate_influencers.js similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/job_validation/validate_influencers.js rename to x-pack/plugins/ml/server/models/job_validation/validate_influencers.js diff --git a/x-pack/legacy/plugins/ml/server/models/job_validation/validate_job_object.js b/x-pack/plugins/ml/server/models/job_validation/validate_job_object.js similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/job_validation/validate_job_object.js rename to x-pack/plugins/ml/server/models/job_validation/validate_job_object.js diff --git a/x-pack/legacy/plugins/ml/server/models/job_validation/validate_model_memory_limit.js b/x-pack/plugins/ml/server/models/job_validation/validate_model_memory_limit.js similarity index 98% rename from x-pack/legacy/plugins/ml/server/models/job_validation/validate_model_memory_limit.js rename to x-pack/plugins/ml/server/models/job_validation/validate_model_memory_limit.js index 733ed9c3c22c6..354a3124a534f 100644 --- a/x-pack/legacy/plugins/ml/server/models/job_validation/validate_model_memory_limit.js +++ b/x-pack/plugins/ml/server/models/job_validation/validate_model_memory_limit.js @@ -7,7 +7,7 @@ import numeral from '@elastic/numeral'; import { validateJobObject } from './validate_job_object'; import { calculateModelMemoryLimitProvider } from '../../models/calculate_model_memory_limit'; -import { ALLOWED_DATA_UNITS } from '../../../common/constants/validation'; +import { ALLOWED_DATA_UNITS } from '../../../../../legacy/plugins/ml/common/constants/validation'; // The minimum value the backend expects is 1MByte const MODEL_MEMORY_LIMIT_MINIMUM_BYTES = 1048576; diff --git a/x-pack/legacy/plugins/ml/server/models/job_validation/validate_time_range.js b/x-pack/plugins/ml/server/models/job_validation/validate_time_range.js similarity index 93% rename from x-pack/legacy/plugins/ml/server/models/job_validation/validate_time_range.js rename to x-pack/plugins/ml/server/models/job_validation/validate_time_range.js index df14d37266496..e6a92b45649b0 100644 --- a/x-pack/legacy/plugins/ml/server/models/job_validation/validate_time_range.js +++ b/x-pack/plugins/ml/server/models/job_validation/validate_time_range.js @@ -6,8 +6,8 @@ import _ from 'lodash'; -import { ES_FIELD_TYPES } from '../../../../../../../src/plugins/data/server'; -import { parseInterval } from '../../../common/util/parse_interval'; +import { ES_FIELD_TYPES } from '../../../../../../src/plugins/data/server'; +import { parseInterval } from '../../../../../legacy/plugins/ml/common/util/parse_interval'; import { validateJobObject } from './validate_job_object'; const BUCKET_SPAN_COMPARE_FACTOR = 25; diff --git a/x-pack/legacy/plugins/ml/server/models/results_service/build_anomaly_table_items.d.ts b/x-pack/plugins/ml/server/models/results_service/build_anomaly_table_items.d.ts similarity index 89% rename from x-pack/legacy/plugins/ml/server/models/results_service/build_anomaly_table_items.d.ts rename to x-pack/plugins/ml/server/models/results_service/build_anomaly_table_items.d.ts index 2bd19985c8518..f2d74fb915299 100644 --- a/x-pack/legacy/plugins/ml/server/models/results_service/build_anomaly_table_items.d.ts +++ b/x-pack/plugins/ml/server/models/results_service/build_anomaly_table_items.d.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AnomalyRecordDoc } from '../../../common/types/anomalies'; +import { AnomalyRecordDoc } from '../../../../../legacy/plugins/ml/common/types/anomalies'; export interface AnomaliesTableRecord { time: number; diff --git a/x-pack/legacy/plugins/ml/server/models/results_service/build_anomaly_table_items.js b/x-pack/plugins/ml/server/models/results_service/build_anomaly_table_items.js similarity index 99% rename from x-pack/legacy/plugins/ml/server/models/results_service/build_anomaly_table_items.js rename to x-pack/plugins/ml/server/models/results_service/build_anomaly_table_items.js index 4934a0ba07081..fc4280c74994d 100644 --- a/x-pack/legacy/plugins/ml/server/models/results_service/build_anomaly_table_items.js +++ b/x-pack/plugins/ml/server/models/results_service/build_anomaly_table_items.js @@ -12,7 +12,7 @@ import { getEntityFieldValue, showActualForFunction, showTypicalForFunction, -} from '../../../common/util/anomaly_utils'; +} from '../../../../../legacy/plugins/ml/common/util/anomaly_utils'; // Builds the items for display in the anomalies table from the supplied list of anomaly records. // Provide the timezone to use for aggregating anomalies (by day or hour) as set in the diff --git a/x-pack/legacy/plugins/ml/server/models/results_service/get_partition_fields_values.ts b/x-pack/plugins/ml/server/models/results_service/get_partition_fields_values.ts similarity index 95% rename from x-pack/legacy/plugins/ml/server/models/results_service/get_partition_fields_values.ts rename to x-pack/plugins/ml/server/models/results_service/get_partition_fields_values.ts index 99eeaacc8de9c..5d536059cb0a2 100644 --- a/x-pack/legacy/plugins/ml/server/models/results_service/get_partition_fields_values.ts +++ b/x-pack/plugins/ml/server/models/results_service/get_partition_fields_values.ts @@ -5,8 +5,8 @@ */ import Boom from 'boom'; -import { ML_RESULTS_INDEX_PATTERN } from '../../../common/constants/index_patterns'; -import { callWithRequestType } from '../../../common/types/kibana'; +import { ML_RESULTS_INDEX_PATTERN } from '../../../../../legacy/plugins/ml/common/constants/index_patterns'; +import { callWithRequestType } from '../../../../../legacy/plugins/ml/common/types/kibana'; import { CriteriaField } from './results_service'; const PARTITION_FIELDS = ['partition_field', 'over_field', 'by_field'] as const; diff --git a/x-pack/legacy/plugins/ml/server/models/results_service/index.ts b/x-pack/plugins/ml/server/models/results_service/index.ts similarity index 100% rename from x-pack/legacy/plugins/ml/server/models/results_service/index.ts rename to x-pack/plugins/ml/server/models/results_service/index.ts diff --git a/x-pack/legacy/plugins/ml/server/models/results_service/results_service.ts b/x-pack/plugins/ml/server/models/results_service/results_service.ts similarity index 97% rename from x-pack/legacy/plugins/ml/server/models/results_service/results_service.ts rename to x-pack/plugins/ml/server/models/results_service/results_service.ts index 555a58fbb5333..324cbb91ca8c1 100644 --- a/x-pack/legacy/plugins/ml/server/models/results_service/results_service.ts +++ b/x-pack/plugins/ml/server/models/results_service/results_service.ts @@ -9,10 +9,10 @@ import moment from 'moment'; import { SearchResponse } from 'elasticsearch'; import { RequestHandlerContext } from 'kibana/server'; import { buildAnomalyTableItems, AnomaliesTableRecord } from './build_anomaly_table_items'; -import { ML_RESULTS_INDEX_PATTERN } from '../../../common/constants/index_patterns'; -import { ANOMALIES_TABLE_DEFAULT_QUERY_SIZE } from '../../../common/constants/search'; +import { ML_RESULTS_INDEX_PATTERN } from '../../../../../legacy/plugins/ml/common/constants/index_patterns'; +import { ANOMALIES_TABLE_DEFAULT_QUERY_SIZE } from '../../../../../legacy/plugins/ml/common/constants/search'; import { getPartitionFieldsValuesFactory } from './get_partition_fields_values'; -import { AnomalyRecordDoc } from '../../../common/types/anomalies'; +import { AnomalyRecordDoc } from '../../../../../legacy/plugins/ml/common/types/anomalies'; // Service for carrying out Elasticsearch queries to obtain data for the // ML Results dashboards. diff --git a/x-pack/plugins/ml/server/plugin.ts b/x-pack/plugins/ml/server/plugin.ts new file mode 100644 index 0000000000000..b5adf1fedec79 --- /dev/null +++ b/x-pack/plugins/ml/server/plugin.ts @@ -0,0 +1,168 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { CoreSetup, IScopedClusterClient, Logger, PluginInitializerContext } from 'src/core/server'; +import { LicenseCheckResult, PluginsSetup, RouteInitialization } from './types'; +import { PLUGIN_ID } from '../../../legacy/plugins/ml/common/constants/app'; +import { VALID_FULL_LICENSE_MODES } from '../../../legacy/plugins/ml/common/constants/license'; + +// @ts-ignore: could not find declaration file for module +import { elasticsearchJsPlugin } from './client/elasticsearch_ml'; +import { makeMlUsageCollector } from './lib/ml_telemetry'; +import { initMlServerLog } from './client/log'; +import { addLinksToSampleDatasets } from './lib/sample_data_sets'; + +import { annotationRoutes } from './routes/annotations'; +import { calendars } from './routes/calendars'; +import { dataFeedRoutes } from './routes/datafeeds'; +import { dataFrameAnalyticsRoutes } from './routes/data_frame_analytics'; +import { dataRecognizer } from './routes/modules'; +import { dataVisualizerRoutes } from './routes/data_visualizer'; +import { fieldsService } from './routes/fields_service'; +import { fileDataVisualizerRoutes } from './routes/file_data_visualizer'; +import { filtersRoutes } from './routes/filters'; +import { indicesRoutes } from './routes/indices'; +import { jobAuditMessagesRoutes } from './routes/job_audit_messages'; +import { jobRoutes } from './routes/anomaly_detectors'; +import { jobServiceRoutes } from './routes/job_service'; +import { jobValidationRoutes } from './routes/job_validation'; +import { notificationRoutes } from './routes/notification_settings'; +import { resultsServiceRoutes } from './routes/results_service'; +import { systemRoutes } from './routes/system'; + +declare module 'kibana/server' { + interface RequestHandlerContext { + ml?: { + mlClient: IScopedClusterClient; + }; + } +} + +export class MlServerPlugin { + private readonly pluginId: string = PLUGIN_ID; + private log: Logger; + private version: string; + + private licenseCheckResults: LicenseCheckResult = { + isAvailable: false, + isActive: false, + isEnabled: false, + isSecurityDisabled: false, + }; + + constructor(ctx: PluginInitializerContext) { + this.log = ctx.logger.get(); + this.version = ctx.env.packageInfo.branch; + } + + public setup(coreSetup: CoreSetup, plugins: PluginsSetup) { + let sampleLinksInitialized = false; + + plugins.features.registerFeature({ + id: PLUGIN_ID, + name: i18n.translate('xpack.ml.featureRegistry.mlFeatureName', { + defaultMessage: 'Machine Learning', + }), + icon: 'machineLearningApp', + navLinkId: PLUGIN_ID, + app: [PLUGIN_ID, 'kibana'], + catalogue: [PLUGIN_ID], + privileges: {}, + reserved: { + privilege: { + savedObject: { + all: [], + read: [], + }, + ui: [], + }, + description: i18n.translate('xpack.ml.feature.reserved.description', { + defaultMessage: + 'To grant users access, you should also assign either the machine_learning_user or machine_learning_admin role.', + }), + }, + }); + + // Can access via router's handler function 'context' parameter - context.ml.mlClient + const mlClient = coreSetup.elasticsearch.createClient(PLUGIN_ID, { + plugins: [elasticsearchJsPlugin], + }); + + coreSetup.http.registerRouteHandlerContext(PLUGIN_ID, (context, request) => { + return { + mlClient: mlClient.asScoped(request), + }; + }); + + const routeInit: RouteInitialization = { + router: coreSetup.http.createRouter(), + getLicenseCheckResults: () => this.licenseCheckResults, + }; + + annotationRoutes(routeInit, plugins.security); + calendars(routeInit); + dataFeedRoutes(routeInit); + dataFrameAnalyticsRoutes(routeInit); + dataRecognizer(routeInit); + dataVisualizerRoutes(routeInit); + fieldsService(routeInit); + fileDataVisualizerRoutes(routeInit); + filtersRoutes(routeInit); + indicesRoutes(routeInit); + jobAuditMessagesRoutes(routeInit); + jobRoutes(routeInit); + jobServiceRoutes(routeInit); + notificationRoutes(routeInit); + resultsServiceRoutes(routeInit); + jobValidationRoutes(routeInit, this.version); + systemRoutes(routeInit, { + spacesPlugin: plugins.spaces, + cloud: plugins.cloud, + }); + initMlServerLog({ log: this.log }); + coreSetup.getStartServices().then(([core]) => { + makeMlUsageCollector(plugins.usageCollection, core.savedObjects); + }); + + plugins.licensing.license$.subscribe(async license => { + const { isEnabled: securityIsEnabled } = license.getFeature('security'); + // @ts-ignore isAvailable is not read + const { isAvailable, isEnabled } = license.getFeature(this.pluginId); + + this.licenseCheckResults = { + isActive: license.isActive, + // This `isAvailable` check for the ml plugin returns false for a basic license + // ML should be available on basic with reduced functionality (only file data visualizer) + // TODO: This will need to be updated in the second step of this cutover to NP. + isAvailable: isEnabled, + isEnabled, + isSecurityDisabled: securityIsEnabled === false, + type: license.type, + }; + + if (sampleLinksInitialized === false) { + sampleLinksInitialized = true; + // Add links to the Kibana sample data sets if ml is enabled + // and license is trial or platinum. + if (isEnabled === true && plugins.home) { + if ( + this.licenseCheckResults.type && + VALID_FULL_LICENSE_MODES.includes(this.licenseCheckResults.type) + ) { + addLinksToSampleDatasets({ + addAppLinksToSampleDataset: plugins.home.sampleData.addAppLinksToSampleDataset, + }); + } + } + } + }); + } + + public start() {} + + public stop() {} +} diff --git a/x-pack/legacy/plugins/ml/server/routes/README.md b/x-pack/plugins/ml/server/routes/README.md similarity index 100% rename from x-pack/legacy/plugins/ml/server/routes/README.md rename to x-pack/plugins/ml/server/routes/README.md diff --git a/x-pack/legacy/plugins/ml/server/routes/annotations.ts b/x-pack/plugins/ml/server/routes/annotations.ts similarity index 83% rename from x-pack/legacy/plugins/ml/server/routes/annotations.ts rename to x-pack/plugins/ml/server/routes/annotations.ts index 20f52b4b051c4..bcc0238c366a3 100644 --- a/x-pack/legacy/plugins/ml/server/routes/annotations.ts +++ b/x-pack/plugins/ml/server/routes/annotations.ts @@ -9,18 +9,19 @@ import _ from 'lodash'; import { i18n } from '@kbn/i18n'; import { schema } from '@kbn/config-schema'; +import { SecurityPluginSetup } from '../../../security/server'; import { isAnnotationsFeatureAvailable } from '../lib/check_annotations'; import { annotationServiceProvider } from '../models/annotation_service'; import { wrapError } from '../client/error_wrapper'; -import { licensePreRoutingFactory } from '../new_platform/licence_check_pre_routing_factory'; -import { RouteInitialization } from '../new_platform/plugin'; +import { licensePreRoutingFactory } from './license_check_pre_routing_factory'; +import { RouteInitialization } from '../types'; import { deleteAnnotationSchema, getAnnotationsSchema, indexAnnotationSchema, -} from '../new_platform/annotations_schema'; +} from './schemas/annotations_schema'; -import { ANNOTATION_USER_UNKNOWN } from '../../common/constants/annotations'; +import { ANNOTATION_USER_UNKNOWN } from '../../../../legacy/plugins/ml/common/constants/annotations'; function getAnnotationsFeatureUnavailableErrorMessage() { return Boom.badRequest( @@ -34,7 +35,10 @@ function getAnnotationsFeatureUnavailableErrorMessage() { /** * Routes for annotations */ -export function annotationRoutes({ xpackMainPlugin, router, securityPlugin }: RouteInitialization) { +export function annotationRoutes( + { router, getLicenseCheckResults }: RouteInitialization, + securityPlugin: SecurityPluginSetup +) { /** * @apiGroup Annotations * @@ -57,7 +61,7 @@ export function annotationRoutes({ xpackMainPlugin, router, securityPlugin }: Ro body: schema.object(getAnnotationsSchema), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const { getAnnotations } = annotationServiceProvider(context); const resp = await getAnnotations(request.body); @@ -88,7 +92,7 @@ export function annotationRoutes({ xpackMainPlugin, router, securityPlugin }: Ro body: schema.object(indexAnnotationSchema), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const annotationsFeatureAvailable = await isAnnotationsFeatureAvailable( context.ml!.mlClient.callAsCurrentUser @@ -99,6 +103,7 @@ export function annotationRoutes({ xpackMainPlugin, router, securityPlugin }: Ro const { indexAnnotation } = annotationServiceProvider(context); const user = securityPlugin.authc.getCurrentUser(request) || {}; + // @ts-ignore username doesn't exist on {} const resp = await indexAnnotation(request.body, user.username || ANNOTATION_USER_UNKNOWN); return response.ok({ @@ -126,7 +131,7 @@ export function annotationRoutes({ xpackMainPlugin, router, securityPlugin }: Ro params: schema.object(deleteAnnotationSchema), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const annotationsFeatureAvailable = await isAnnotationsFeatureAvailable( context.ml!.mlClient.callAsCurrentUser diff --git a/x-pack/legacy/plugins/ml/server/routes/anomaly_detectors.ts b/x-pack/plugins/ml/server/routes/anomaly_detectors.ts similarity index 89% rename from x-pack/legacy/plugins/ml/server/routes/anomaly_detectors.ts rename to x-pack/plugins/ml/server/routes/anomaly_detectors.ts index 99dbdec9e945b..7bf2fb7bc6903 100644 --- a/x-pack/legacy/plugins/ml/server/routes/anomaly_detectors.ts +++ b/x-pack/plugins/ml/server/routes/anomaly_detectors.ts @@ -6,17 +6,17 @@ import { schema } from '@kbn/config-schema'; import { wrapError } from '../client/error_wrapper'; -import { licensePreRoutingFactory } from '../new_platform/licence_check_pre_routing_factory'; -import { RouteInitialization } from '../new_platform/plugin'; +import { licensePreRoutingFactory } from './license_check_pre_routing_factory'; +import { RouteInitialization } from '../types'; import { anomalyDetectionJobSchema, anomalyDetectionUpdateJobSchema, -} from '../new_platform/anomaly_detectors_schema'; +} from './schemas/anomaly_detectors_schema'; /** * Routes for the anomaly detectors */ -export function jobRoutes({ xpackMainPlugin, router }: RouteInitialization) { +export function jobRoutes({ router, getLicenseCheckResults }: RouteInitialization) { /** * @apiGroup AnomalyDetectors * @@ -32,7 +32,7 @@ export function jobRoutes({ xpackMainPlugin, router }: RouteInitialization) { path: '/api/ml/anomaly_detectors', validate: false, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const results = await context.ml!.mlClient.callAsCurrentUser('ml.jobs'); return response.ok({ @@ -62,7 +62,7 @@ export function jobRoutes({ xpackMainPlugin, router }: RouteInitialization) { }), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const { jobId } = request.params; const results = await context.ml!.mlClient.callAsCurrentUser('ml.jobs', { jobId }); @@ -90,7 +90,7 @@ export function jobRoutes({ xpackMainPlugin, router }: RouteInitialization) { path: '/api/ml/anomaly_detectors/_stats', validate: false, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const results = await context.ml!.mlClient.callAsCurrentUser('ml.jobStats'); return response.ok({ @@ -120,7 +120,7 @@ export function jobRoutes({ xpackMainPlugin, router }: RouteInitialization) { }), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const { jobId } = request.params; const results = await context.ml!.mlClient.callAsCurrentUser('ml.jobStats', { jobId }); @@ -152,7 +152,7 @@ export function jobRoutes({ xpackMainPlugin, router }: RouteInitialization) { body: schema.object({ ...anomalyDetectionJobSchema }), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const { jobId } = request.params; const results = await context.ml!.mlClient.callAsCurrentUser('ml.addJob', { @@ -187,7 +187,7 @@ export function jobRoutes({ xpackMainPlugin, router }: RouteInitialization) { body: schema.object({ ...anomalyDetectionUpdateJobSchema }), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const { jobId } = request.params; const results = await context.ml!.mlClient.callAsCurrentUser('ml.updateJob', { @@ -221,7 +221,7 @@ export function jobRoutes({ xpackMainPlugin, router }: RouteInitialization) { }), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const { jobId } = request.params; const results = await context.ml!.mlClient.callAsCurrentUser('ml.openJob', { @@ -254,7 +254,7 @@ export function jobRoutes({ xpackMainPlugin, router }: RouteInitialization) { }), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const options: { jobId: string; force?: boolean } = { jobId: request.params.jobId, @@ -291,7 +291,7 @@ export function jobRoutes({ xpackMainPlugin, router }: RouteInitialization) { }), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const options: { jobId: string; force?: boolean } = { jobId: request.params.jobId, @@ -326,7 +326,7 @@ export function jobRoutes({ xpackMainPlugin, router }: RouteInitialization) { body: schema.any(), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const results = await context.ml!.mlClient.callAsCurrentUser('ml.validateDetector', { body: request.body, @@ -359,7 +359,7 @@ export function jobRoutes({ xpackMainPlugin, router }: RouteInitialization) { body: schema.object({ duration: schema.any() }), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const jobId = request.params.jobId; const duration = request.body.duration; @@ -407,7 +407,7 @@ export function jobRoutes({ xpackMainPlugin, router }: RouteInitialization) { }), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const results = await context.ml!.mlClient.callAsCurrentUser('ml.records', { jobId: request.params.jobId, @@ -456,7 +456,7 @@ export function jobRoutes({ xpackMainPlugin, router }: RouteInitialization) { }), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const results = await context.ml!.mlClient.callAsCurrentUser('ml.buckets', { jobId: request.params.jobId, @@ -499,7 +499,7 @@ export function jobRoutes({ xpackMainPlugin, router }: RouteInitialization) { }), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const results = await context.ml!.mlClient.callAsCurrentUser('ml.overallBuckets', { jobId: request.params.jobId, @@ -537,7 +537,7 @@ export function jobRoutes({ xpackMainPlugin, router }: RouteInitialization) { }), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const options = { jobId: request.params.jobId, diff --git a/x-pack/legacy/plugins/ml/server/routes/apidoc.json b/x-pack/plugins/ml/server/routes/apidoc.json similarity index 100% rename from x-pack/legacy/plugins/ml/server/routes/apidoc.json rename to x-pack/plugins/ml/server/routes/apidoc.json diff --git a/x-pack/legacy/plugins/ml/server/routes/calendars.ts b/x-pack/plugins/ml/server/routes/calendars.ts similarity index 84% rename from x-pack/legacy/plugins/ml/server/routes/calendars.ts rename to x-pack/plugins/ml/server/routes/calendars.ts index 8e4e1c4c14751..ae494d3578890 100644 --- a/x-pack/legacy/plugins/ml/server/routes/calendars.ts +++ b/x-pack/plugins/ml/server/routes/calendars.ts @@ -6,10 +6,10 @@ import { RequestHandlerContext } from 'src/core/server'; import { schema } from '@kbn/config-schema'; -import { licensePreRoutingFactory } from '../new_platform/licence_check_pre_routing_factory'; +import { licensePreRoutingFactory } from './license_check_pre_routing_factory'; import { wrapError } from '../client/error_wrapper'; -import { RouteInitialization } from '../new_platform/plugin'; -import { calendarSchema } from '../new_platform/calendars_schema'; +import { RouteInitialization } from '../types'; +import { calendarSchema } from './schemas/calendars_schema'; import { CalendarManager, Calendar, FormCalendar } from '../models/calendar'; function getAllCalendars(context: RequestHandlerContext) { @@ -42,13 +42,13 @@ function getCalendarsByIds(context: RequestHandlerContext, calendarIds: string) return cal.getCalendarsByIds(calendarIds); } -export function calendars({ xpackMainPlugin, router }: RouteInitialization) { +export function calendars({ router, getLicenseCheckResults }: RouteInitialization) { router.get( { path: '/api/ml/calendars', validate: false, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const resp = await getAllCalendars(context); @@ -68,7 +68,7 @@ export function calendars({ xpackMainPlugin, router }: RouteInitialization) { params: schema.object({ calendarIds: schema.string() }), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { let returnValue; try { const calendarIds = request.params.calendarIds.split(','); @@ -95,7 +95,7 @@ export function calendars({ xpackMainPlugin, router }: RouteInitialization) { body: schema.object({ ...calendarSchema }), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const body = request.body; const resp = await newCalendar(context, body); @@ -117,7 +117,7 @@ export function calendars({ xpackMainPlugin, router }: RouteInitialization) { body: schema.object({ ...calendarSchema }), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const { calendarId } = request.params; const body = request.body; @@ -139,7 +139,7 @@ export function calendars({ xpackMainPlugin, router }: RouteInitialization) { params: schema.object({ calendarId: schema.string() }), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const { calendarId } = request.params; const resp = await deleteCalendar(context, calendarId); diff --git a/x-pack/legacy/plugins/ml/server/routes/data_frame_analytics.ts b/x-pack/plugins/ml/server/routes/data_frame_analytics.ts similarity index 89% rename from x-pack/legacy/plugins/ml/server/routes/data_frame_analytics.ts rename to x-pack/plugins/ml/server/routes/data_frame_analytics.ts index 6541fa541a59f..0a93320c05eb5 100644 --- a/x-pack/legacy/plugins/ml/server/routes/data_frame_analytics.ts +++ b/x-pack/plugins/ml/server/routes/data_frame_analytics.ts @@ -7,18 +7,18 @@ import { schema } from '@kbn/config-schema'; import { wrapError } from '../client/error_wrapper'; import { analyticsAuditMessagesProvider } from '../models/data_frame_analytics/analytics_audit_messages'; -import { licensePreRoutingFactory } from '../new_platform/licence_check_pre_routing_factory'; -import { RouteInitialization } from '../new_platform/plugin'; +import { licensePreRoutingFactory } from './license_check_pre_routing_factory'; +import { RouteInitialization } from '../types'; import { dataAnalyticsJobConfigSchema, dataAnalyticsEvaluateSchema, dataAnalyticsExplainSchema, -} from '../new_platform/data_analytics_schema'; +} from './schemas/data_analytics_schema'; /** * Routes for the data frame analytics */ -export function dataFrameAnalyticsRoutes({ xpackMainPlugin, router }: RouteInitialization) { +export function dataFrameAnalyticsRoutes({ router, getLicenseCheckResults }: RouteInitialization) { /** * @apiGroup DataFrameAnalytics * @@ -36,7 +36,7 @@ export function dataFrameAnalyticsRoutes({ xpackMainPlugin, router }: RouteIniti params: schema.object({ analyticsId: schema.maybe(schema.string()) }), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const results = await context.ml!.mlClient.callAsCurrentUser('ml.getDataFrameAnalytics'); return response.ok({ @@ -64,7 +64,7 @@ export function dataFrameAnalyticsRoutes({ xpackMainPlugin, router }: RouteIniti params: schema.object({ analyticsId: schema.string() }), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const { analyticsId } = request.params; const results = await context.ml!.mlClient.callAsCurrentUser('ml.getDataFrameAnalytics', { @@ -91,7 +91,7 @@ export function dataFrameAnalyticsRoutes({ xpackMainPlugin, router }: RouteIniti path: '/api/ml/data_frame/analytics/_stats', validate: false, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const results = await context.ml!.mlClient.callAsCurrentUser( 'ml.getDataFrameAnalyticsStats' @@ -121,7 +121,7 @@ export function dataFrameAnalyticsRoutes({ xpackMainPlugin, router }: RouteIniti params: schema.object({ analyticsId: schema.string() }), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const { analyticsId } = request.params; const results = await context.ml!.mlClient.callAsCurrentUser( @@ -159,7 +159,7 @@ export function dataFrameAnalyticsRoutes({ xpackMainPlugin, router }: RouteIniti body: schema.object(dataAnalyticsJobConfigSchema), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const { analyticsId } = request.params; const results = await context.ml!.mlClient.callAsCurrentUser( @@ -192,7 +192,7 @@ export function dataFrameAnalyticsRoutes({ xpackMainPlugin, router }: RouteIniti body: schema.object({ ...dataAnalyticsEvaluateSchema }), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const results = await context.ml!.mlClient.callAsCurrentUser( 'ml.evaluateDataFrameAnalytics', @@ -232,7 +232,7 @@ export function dataFrameAnalyticsRoutes({ xpackMainPlugin, router }: RouteIniti body: schema.object({ ...dataAnalyticsExplainSchema }), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const results = await context.ml!.mlClient.callAsCurrentUser( 'ml.explainDataFrameAnalytics', @@ -267,7 +267,7 @@ export function dataFrameAnalyticsRoutes({ xpackMainPlugin, router }: RouteIniti }), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const { analyticsId } = request.params; const results = await context.ml!.mlClient.callAsCurrentUser( @@ -303,7 +303,7 @@ export function dataFrameAnalyticsRoutes({ xpackMainPlugin, router }: RouteIniti }), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const { analyticsId } = request.params; const results = await context.ml!.mlClient.callAsCurrentUser('ml.startDataFrameAnalytics', { @@ -337,7 +337,7 @@ export function dataFrameAnalyticsRoutes({ xpackMainPlugin, router }: RouteIniti }), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const options: { analyticsId: string; force?: boolean | undefined } = { analyticsId: request.params.analyticsId, @@ -377,7 +377,7 @@ export function dataFrameAnalyticsRoutes({ xpackMainPlugin, router }: RouteIniti params: schema.object({ analyticsId: schema.string() }), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const { analyticsId } = request.params; const { getAnalyticsAuditMessages } = analyticsAuditMessagesProvider( diff --git a/x-pack/legacy/plugins/ml/server/routes/data_visualizer.ts b/x-pack/plugins/ml/server/routes/data_visualizer.ts similarity index 89% rename from x-pack/legacy/plugins/ml/server/routes/data_visualizer.ts rename to x-pack/plugins/ml/server/routes/data_visualizer.ts index df7e4b7010877..e4d068784def1 100644 --- a/x-pack/legacy/plugins/ml/server/routes/data_visualizer.ts +++ b/x-pack/plugins/ml/server/routes/data_visualizer.ts @@ -11,9 +11,9 @@ import { Field } from '../models/data_visualizer/data_visualizer'; import { dataVisualizerFieldStatsSchema, dataVisualizerOverallStatsSchema, -} from '../new_platform/data_visualizer_schema'; -import { licensePreRoutingFactory } from '../new_platform/licence_check_pre_routing_factory'; -import { RouteInitialization } from '../new_platform/plugin'; +} from './schemas/data_visualizer_schema'; +import { licensePreRoutingFactory } from './license_check_pre_routing_factory'; +import { RouteInitialization } from '../types'; function getOverallStats( context: RequestHandlerContext, @@ -68,7 +68,7 @@ function getStatsForFields( /** * Routes for the index data visualizer. */ -export function dataVisualizerRoutes({ xpackMainPlugin, router }: RouteInitialization) { +export function dataVisualizerRoutes({ router, getLicenseCheckResults }: RouteInitialization) { /** * @apiGroup DataVisualizer * @@ -83,7 +83,7 @@ export function dataVisualizerRoutes({ xpackMainPlugin, router }: RouteInitializ path: '/api/ml/data_visualizer/get_field_stats/{indexPatternTitle}', validate: dataVisualizerFieldStatsSchema, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const { params: { indexPatternTitle }, @@ -135,7 +135,7 @@ export function dataVisualizerRoutes({ xpackMainPlugin, router }: RouteInitializ path: '/api/ml/data_visualizer/get_overall_stats/{indexPatternTitle}', validate: dataVisualizerOverallStatsSchema, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const { params: { indexPatternTitle }, diff --git a/x-pack/legacy/plugins/ml/server/routes/datafeeds.ts b/x-pack/plugins/ml/server/routes/datafeeds.ts similarity index 86% rename from x-pack/legacy/plugins/ml/server/routes/datafeeds.ts rename to x-pack/plugins/ml/server/routes/datafeeds.ts index 9335403616cf7..e3bce4c1328e4 100644 --- a/x-pack/legacy/plugins/ml/server/routes/datafeeds.ts +++ b/x-pack/plugins/ml/server/routes/datafeeds.ts @@ -5,15 +5,15 @@ */ import { schema } from '@kbn/config-schema'; -import { licensePreRoutingFactory } from '../new_platform/licence_check_pre_routing_factory'; +import { licensePreRoutingFactory } from './license_check_pre_routing_factory'; import { wrapError } from '../client/error_wrapper'; -import { RouteInitialization } from '../new_platform/plugin'; -import { startDatafeedSchema, datafeedConfigSchema } from '../new_platform/datafeeds_schema'; +import { RouteInitialization } from '../types'; +import { startDatafeedSchema, datafeedConfigSchema } from './schemas/datafeeds_schema'; /** * Routes for datafeed service */ -export function dataFeedRoutes({ xpackMainPlugin, router }: RouteInitialization) { +export function dataFeedRoutes({ router, getLicenseCheckResults }: RouteInitialization) { /** * @apiGroup DatafeedService * @@ -26,7 +26,7 @@ export function dataFeedRoutes({ xpackMainPlugin, router }: RouteInitialization) path: '/api/ml/datafeeds', validate: false, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const resp = await context.ml!.mlClient.callAsCurrentUser('ml.datafeeds'); @@ -53,7 +53,7 @@ export function dataFeedRoutes({ xpackMainPlugin, router }: RouteInitialization) params: schema.object({ datafeedId: schema.string() }), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const datafeedId = request.params.datafeedId; const resp = await context.ml!.mlClient.callAsCurrentUser('ml.datafeeds', { datafeedId }); @@ -79,7 +79,7 @@ export function dataFeedRoutes({ xpackMainPlugin, router }: RouteInitialization) path: '/api/ml/datafeeds/_stats', validate: false, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const resp = await context.ml!.mlClient.callAsCurrentUser('ml.datafeedStats'); @@ -106,7 +106,7 @@ export function dataFeedRoutes({ xpackMainPlugin, router }: RouteInitialization) params: schema.object({ datafeedId: schema.string() }), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const datafeedId = request.params.datafeedId; const resp = await context.ml!.mlClient.callAsCurrentUser('ml.datafeedStats', { @@ -137,7 +137,7 @@ export function dataFeedRoutes({ xpackMainPlugin, router }: RouteInitialization) body: datafeedConfigSchema, }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const datafeedId = request.params.datafeedId; const resp = await context.ml!.mlClient.callAsCurrentUser('ml.addDatafeed', { @@ -169,7 +169,7 @@ export function dataFeedRoutes({ xpackMainPlugin, router }: RouteInitialization) body: datafeedConfigSchema, }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const datafeedId = request.params.datafeedId; const resp = await context.ml!.mlClient.callAsCurrentUser('ml.updateDatafeed', { @@ -201,7 +201,7 @@ export function dataFeedRoutes({ xpackMainPlugin, router }: RouteInitialization) query: schema.maybe(schema.object({ force: schema.maybe(schema.any()) })), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const options: { datafeedId: string; force?: boolean } = { datafeedId: request.params.jobId, @@ -237,7 +237,7 @@ export function dataFeedRoutes({ xpackMainPlugin, router }: RouteInitialization) body: startDatafeedSchema, }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const datafeedId = request.params.datafeedId; const { start, end } = request.body; @@ -271,7 +271,7 @@ export function dataFeedRoutes({ xpackMainPlugin, router }: RouteInitialization) params: schema.object({ datafeedId: schema.string() }), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const datafeedId = request.params.datafeedId; @@ -302,7 +302,7 @@ export function dataFeedRoutes({ xpackMainPlugin, router }: RouteInitialization) params: schema.object({ datafeedId: schema.string() }), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const datafeedId = request.params.datafeedId; const resp = await context.ml!.mlClient.callAsCurrentUser('ml.datafeedPreview', { diff --git a/x-pack/legacy/plugins/ml/server/routes/fields_service.ts b/x-pack/plugins/ml/server/routes/fields_service.ts similarity index 84% rename from x-pack/legacy/plugins/ml/server/routes/fields_service.ts rename to x-pack/plugins/ml/server/routes/fields_service.ts index 4827adf23d7b4..bc092190c2c62 100644 --- a/x-pack/legacy/plugins/ml/server/routes/fields_service.ts +++ b/x-pack/plugins/ml/server/routes/fields_service.ts @@ -5,13 +5,13 @@ */ import { RequestHandlerContext } from 'src/core/server'; -import { licensePreRoutingFactory } from '../new_platform/licence_check_pre_routing_factory'; +import { licensePreRoutingFactory } from './license_check_pre_routing_factory'; import { wrapError } from '../client/error_wrapper'; -import { RouteInitialization } from '../new_platform/plugin'; +import { RouteInitialization } from '../types'; import { getCardinalityOfFieldsSchema, getTimeFieldRangeSchema, -} from '../new_platform/fields_service_schema'; +} from './schemas/fields_service_schema'; import { fieldsServiceProvider } from '../models/fields_service'; function getCardinalityOfFields(context: RequestHandlerContext, payload: any) { @@ -29,7 +29,7 @@ function getTimeFieldRange(context: RequestHandlerContext, payload: any) { /** * Routes for fields service */ -export function fieldsService({ xpackMainPlugin, router }: RouteInitialization) { +export function fieldsService({ router, getLicenseCheckResults }: RouteInitialization) { /** * @apiGroup FieldsService * @@ -44,7 +44,7 @@ export function fieldsService({ xpackMainPlugin, router }: RouteInitialization) body: getCardinalityOfFieldsSchema, }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const resp = await getCardinalityOfFields(context, request.body); @@ -71,7 +71,7 @@ export function fieldsService({ xpackMainPlugin, router }: RouteInitialization) body: getTimeFieldRangeSchema, }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const resp = await getTimeFieldRange(context, request.body); diff --git a/x-pack/legacy/plugins/ml/server/routes/file_data_visualizer.ts b/x-pack/plugins/ml/server/routes/file_data_visualizer.ts similarity index 87% rename from x-pack/legacy/plugins/ml/server/routes/file_data_visualizer.ts rename to x-pack/plugins/ml/server/routes/file_data_visualizer.ts index d5a992c933293..1d724a8843350 100644 --- a/x-pack/legacy/plugins/ml/server/routes/file_data_visualizer.ts +++ b/x-pack/plugins/ml/server/routes/file_data_visualizer.ts @@ -6,7 +6,7 @@ import { schema } from '@kbn/config-schema'; import { RequestHandlerContext } from 'kibana/server'; -import { MAX_BYTES } from '../../common/constants/file_datavisualizer'; +import { MAX_BYTES } from '../../../../legacy/plugins/ml/common/constants/file_datavisualizer'; import { wrapError } from '../client/error_wrapper'; import { InputOverrides, @@ -18,8 +18,8 @@ import { Mappings, } from '../models/file_data_visualizer'; -import { licensePreRoutingFactory } from '../new_platform/licence_check_pre_routing_factory'; -import { RouteInitialization } from '../new_platform/plugin'; +import { licensePreRoutingFactory } from './license_check_pre_routing_factory'; +import { RouteInitialization } from '../types'; import { incrementFileDataVisualizerIndexCreationCount } from '../lib/ml_telemetry'; function analyzeFiles(context: RequestHandlerContext, data: InputData, overrides: InputOverrides) { @@ -43,12 +43,7 @@ function importData( /** * Routes for the file data visualizer. */ -export function fileDataVisualizerRoutes({ - router, - xpackMainPlugin, - savedObjects, - elasticsearchPlugin, -}: RouteInitialization) { +export function fileDataVisualizerRoutes({ router, getLicenseCheckResults }: RouteInitialization) { /** * @apiGroup FileDataVisualizer * @@ -87,7 +82,7 @@ export function fileDataVisualizerRoutes({ }, }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const result = await analyzeFiles(context, request.body, request.query); return response.ok({ body: result }); @@ -129,7 +124,7 @@ export function fileDataVisualizerRoutes({ }, }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const { id } = request.query; const { index, data, settings, mappings, ingestPipeline } = request.body; @@ -138,7 +133,8 @@ export function fileDataVisualizerRoutes({ // follow-up import calls to just add additional data will include the `id` of the created // index, we'll ignore those and don't increment the counter. if (id === undefined) { - await incrementFileDataVisualizerIndexCreationCount(savedObjects!); + // @ts-ignore + await incrementFileDataVisualizerIndexCreationCount(context.core.savedObjects.client); } const result = await importData( diff --git a/x-pack/legacy/plugins/ml/server/routes/filters.ts b/x-pack/plugins/ml/server/routes/filters.ts similarity index 87% rename from x-pack/legacy/plugins/ml/server/routes/filters.ts rename to x-pack/plugins/ml/server/routes/filters.ts index a06f8d4f8b727..d5530668b2606 100644 --- a/x-pack/legacy/plugins/ml/server/routes/filters.ts +++ b/x-pack/plugins/ml/server/routes/filters.ts @@ -6,10 +6,10 @@ import { RequestHandlerContext } from 'src/core/server'; import { schema } from '@kbn/config-schema'; -import { licensePreRoutingFactory } from '../new_platform/licence_check_pre_routing_factory'; +import { licensePreRoutingFactory } from './license_check_pre_routing_factory'; import { wrapError } from '../client/error_wrapper'; -import { RouteInitialization } from '../new_platform/plugin'; -import { createFilterSchema, updateFilterSchema } from '../new_platform/filters_schema'; +import { RouteInitialization } from '../types'; +import { createFilterSchema, updateFilterSchema } from './schemas/filters_schema'; import { FilterManager, FormFilter } from '../models/filter'; // TODO - add function for returning a list of just the filter IDs. @@ -44,7 +44,7 @@ function deleteFilter(context: RequestHandlerContext, filterId: string) { return mgr.deleteFilter(filterId); } -export function filtersRoutes({ xpackMainPlugin, router }: RouteInitialization) { +export function filtersRoutes({ router, getLicenseCheckResults }: RouteInitialization) { /** * @apiGroup Filters * @@ -60,7 +60,7 @@ export function filtersRoutes({ xpackMainPlugin, router }: RouteInitialization) path: '/api/ml/filters', validate: false, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const resp = await getAllFilters(context); @@ -90,7 +90,7 @@ export function filtersRoutes({ xpackMainPlugin, router }: RouteInitialization) params: schema.object({ filterId: schema.string() }), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const resp = await getFilter(context, request.params.filterId); return response.ok({ @@ -119,7 +119,7 @@ export function filtersRoutes({ xpackMainPlugin, router }: RouteInitialization) body: schema.object(createFilterSchema), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const body = request.body; const resp = await newFilter(context, body); @@ -151,7 +151,7 @@ export function filtersRoutes({ xpackMainPlugin, router }: RouteInitialization) body: schema.object(updateFilterSchema), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const { filterId } = request.params; const body = request.body; @@ -182,7 +182,7 @@ export function filtersRoutes({ xpackMainPlugin, router }: RouteInitialization) params: schema.object({ filterId: schema.string() }), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const { filterId } = request.params; const resp = await deleteFilter(context, filterId); @@ -212,7 +212,7 @@ export function filtersRoutes({ xpackMainPlugin, router }: RouteInitialization) path: '/api/ml/filters/_stats', validate: false, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const resp = await getAllFilterStats(context); diff --git a/x-pack/legacy/plugins/ml/server/routes/indices.ts b/x-pack/plugins/ml/server/routes/indices.ts similarity index 80% rename from x-pack/legacy/plugins/ml/server/routes/indices.ts rename to x-pack/plugins/ml/server/routes/indices.ts index 0ee15f1321e9c..e01a7a0cbad28 100644 --- a/x-pack/legacy/plugins/ml/server/routes/indices.ts +++ b/x-pack/plugins/ml/server/routes/indices.ts @@ -6,13 +6,13 @@ import { schema } from '@kbn/config-schema'; import { wrapError } from '../client/error_wrapper'; -import { licensePreRoutingFactory } from '../new_platform/licence_check_pre_routing_factory'; -import { RouteInitialization } from '../new_platform/plugin'; +import { licensePreRoutingFactory } from './license_check_pre_routing_factory'; +import { RouteInitialization } from '../types'; /** * Indices routes. */ -export function indicesRoutes({ xpackMainPlugin, router }: RouteInitialization) { +export function indicesRoutes({ router, getLicenseCheckResults }: RouteInitialization) { /** * @apiGroup Indices * @@ -30,7 +30,7 @@ export function indicesRoutes({ xpackMainPlugin, router }: RouteInitialization) }), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const { body: { index, fields: requestFields }, diff --git a/x-pack/legacy/plugins/ml/server/routes/job_audit_messages.ts b/x-pack/plugins/ml/server/routes/job_audit_messages.ts similarity index 84% rename from x-pack/legacy/plugins/ml/server/routes/job_audit_messages.ts rename to x-pack/plugins/ml/server/routes/job_audit_messages.ts index 76986b935b993..38df28e17ec0d 100644 --- a/x-pack/legacy/plugins/ml/server/routes/job_audit_messages.ts +++ b/x-pack/plugins/ml/server/routes/job_audit_messages.ts @@ -5,15 +5,15 @@ */ import { schema } from '@kbn/config-schema'; -import { licensePreRoutingFactory } from '../new_platform/licence_check_pre_routing_factory'; +import { licensePreRoutingFactory } from './license_check_pre_routing_factory'; import { wrapError } from '../client/error_wrapper'; -import { RouteInitialization } from '../new_platform/plugin'; +import { RouteInitialization } from '../types'; import { jobAuditMessagesProvider } from '../models/job_audit_messages'; /** * Routes for job audit message routes */ -export function jobAuditMessagesRoutes({ xpackMainPlugin, router }: RouteInitialization) { +export function jobAuditMessagesRoutes({ router, getLicenseCheckResults }: RouteInitialization) { /** * @apiGroup JobAuditMessages * @@ -29,7 +29,7 @@ export function jobAuditMessagesRoutes({ xpackMainPlugin, router }: RouteInitial query: schema.maybe(schema.object({ from: schema.maybe(schema.any()) })), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const { getJobAuditMessages } = jobAuditMessagesProvider( context.ml!.mlClient.callAsCurrentUser @@ -62,7 +62,7 @@ export function jobAuditMessagesRoutes({ xpackMainPlugin, router }: RouteInitial query: schema.maybe(schema.object({ from: schema.maybe(schema.any()) })), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const { getJobAuditMessages } = jobAuditMessagesProvider( context.ml!.mlClient.callAsCurrentUser diff --git a/x-pack/legacy/plugins/ml/server/routes/job_service.ts b/x-pack/plugins/ml/server/routes/job_service.ts similarity index 88% rename from x-pack/legacy/plugins/ml/server/routes/job_service.ts rename to x-pack/plugins/ml/server/routes/job_service.ts index 5ddbd4cdfd5a5..e15888088d3a1 100644 --- a/x-pack/legacy/plugins/ml/server/routes/job_service.ts +++ b/x-pack/plugins/ml/server/routes/job_service.ts @@ -7,10 +7,9 @@ import Boom from 'boom'; import { schema } from '@kbn/config-schema'; import { IScopedClusterClient } from 'src/core/server'; -import { licensePreRoutingFactory } from '../new_platform/licence_check_pre_routing_factory'; +import { licensePreRoutingFactory } from './license_check_pre_routing_factory'; import { wrapError } from '../client/error_wrapper'; -import { RouteInitialization } from '../new_platform/plugin'; -import { isSecurityDisabled } from '../lib/security_utils'; +import { RouteInitialization } from '../types'; import { categorizationFieldExamplesSchema, chartSchema, @@ -21,7 +20,7 @@ import { lookBackProgressSchema, topCategoriesSchema, updateGroupsSchema, -} from '../new_platform/job_service_schema'; +} from './schemas/job_service_schema'; // @ts-ignore no declaration module import { jobServiceProvider } from '../models/job_service'; import { categorizationExamplesProvider } from '../models/job_service/new_job'; @@ -29,11 +28,12 @@ import { categorizationExamplesProvider } from '../models/job_service/new_job'; /** * Routes for job service */ -export function jobServiceRoutes({ xpackMainPlugin, router }: RouteInitialization) { +export function jobServiceRoutes({ router, getLicenseCheckResults }: RouteInitialization) { async function hasPermissionToCreateJobs( callAsCurrentUser: IScopedClusterClient['callAsCurrentUser'] ) { - if (isSecurityDisabled(xpackMainPlugin) === true) { + const { isSecurityDisabled } = getLicenseCheckResults(); + if (isSecurityDisabled === true) { return true; } @@ -63,7 +63,7 @@ export function jobServiceRoutes({ xpackMainPlugin, router }: RouteInitializatio body: schema.object(forceStartDatafeedSchema), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const { forceStartDatafeeds } = jobServiceProvider(context.ml!.mlClient.callAsCurrentUser); const { datafeedIds, start, end } = request.body; @@ -92,7 +92,7 @@ export function jobServiceRoutes({ xpackMainPlugin, router }: RouteInitializatio body: schema.object(datafeedIdsSchema), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const { stopDatafeeds } = jobServiceProvider(context.ml!.mlClient.callAsCurrentUser); const { datafeedIds } = request.body; @@ -121,7 +121,7 @@ export function jobServiceRoutes({ xpackMainPlugin, router }: RouteInitializatio body: schema.object(jobIdsSchema), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const { deleteJobs } = jobServiceProvider(context.ml!.mlClient.callAsCurrentUser); const { jobIds } = request.body; @@ -150,7 +150,7 @@ export function jobServiceRoutes({ xpackMainPlugin, router }: RouteInitializatio body: schema.object(jobIdsSchema), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const { closeJobs } = jobServiceProvider(context.ml!.mlClient.callAsCurrentUser); const { jobIds } = request.body; @@ -179,7 +179,7 @@ export function jobServiceRoutes({ xpackMainPlugin, router }: RouteInitializatio body: schema.object(jobIdsSchema), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const { jobsSummary } = jobServiceProvider(context.ml!.mlClient.callAsCurrentUser); const { jobIds } = request.body; @@ -208,7 +208,7 @@ export function jobServiceRoutes({ xpackMainPlugin, router }: RouteInitializatio body: schema.object(jobsWithTimerangeSchema), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const { jobsWithTimerange } = jobServiceProvider(context.ml!.mlClient.callAsCurrentUser); const { dateFormatTz } = request.body; @@ -237,7 +237,7 @@ export function jobServiceRoutes({ xpackMainPlugin, router }: RouteInitializatio body: schema.object(jobIdsSchema), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const { createFullJobsList } = jobServiceProvider(context.ml!.mlClient.callAsCurrentUser); const { jobIds } = request.body; @@ -264,7 +264,7 @@ export function jobServiceRoutes({ xpackMainPlugin, router }: RouteInitializatio path: '/api/ml/jobs/groups', validate: false, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const { getAllGroups } = jobServiceProvider(context.ml!.mlClient.callAsCurrentUser); const resp = await getAllGroups(); @@ -292,7 +292,7 @@ export function jobServiceRoutes({ xpackMainPlugin, router }: RouteInitializatio body: schema.object(updateGroupsSchema), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const { updateGroups } = jobServiceProvider(context.ml!.mlClient.callAsCurrentUser); const { jobs } = request.body; @@ -319,7 +319,7 @@ export function jobServiceRoutes({ xpackMainPlugin, router }: RouteInitializatio path: '/api/ml/jobs/deleting_jobs_tasks', validate: false, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const { deletingJobTasks } = jobServiceProvider(context.ml!.mlClient.callAsCurrentUser); const resp = await deletingJobTasks(); @@ -347,7 +347,7 @@ export function jobServiceRoutes({ xpackMainPlugin, router }: RouteInitializatio body: schema.object(jobIdsSchema), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const { jobsExist } = jobServiceProvider(context.ml!.mlClient.callAsCurrentUser); const { jobIds } = request.body; @@ -377,7 +377,7 @@ export function jobServiceRoutes({ xpackMainPlugin, router }: RouteInitializatio query: schema.maybe(schema.object({ rollup: schema.maybe(schema.string()) })), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const { indexPattern } = request.params; const isRollup = request.query.rollup === 'true'; @@ -408,7 +408,7 @@ export function jobServiceRoutes({ xpackMainPlugin, router }: RouteInitializatio body: schema.object(chartSchema), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const { indexPatternTitle, @@ -461,7 +461,7 @@ export function jobServiceRoutes({ xpackMainPlugin, router }: RouteInitializatio body: schema.object(chartSchema), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const { indexPatternTitle, @@ -509,7 +509,7 @@ export function jobServiceRoutes({ xpackMainPlugin, router }: RouteInitializatio path: '/api/ml/jobs/all_jobs_and_group_ids', validate: false, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const { getAllJobAndGroupIds } = jobServiceProvider(context.ml!.mlClient.callAsCurrentUser); const resp = await getAllJobAndGroupIds(); @@ -537,7 +537,7 @@ export function jobServiceRoutes({ xpackMainPlugin, router }: RouteInitializatio body: schema.object(lookBackProgressSchema), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const { getLookBackProgress } = jobServiceProvider(context.ml!.mlClient.callAsCurrentUser); const { jobId, start, end } = request.body; @@ -566,7 +566,7 @@ export function jobServiceRoutes({ xpackMainPlugin, router }: RouteInitializatio body: schema.object(categorizationFieldExamplesSchema), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { // due to the use of the _analyze endpoint which is called by the kibana user, // basic job creation privileges are required to use this endpoint @@ -625,7 +625,7 @@ export function jobServiceRoutes({ xpackMainPlugin, router }: RouteInitializatio body: schema.object(topCategoriesSchema), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const { topCategories } = jobServiceProvider(context.ml!.mlClient.callAsCurrentUser); const { jobId, count } = request.body; diff --git a/x-pack/legacy/plugins/ml/server/routes/job_validation.ts b/x-pack/plugins/ml/server/routes/job_validation.ts similarity index 85% rename from x-pack/legacy/plugins/ml/server/routes/job_validation.ts rename to x-pack/plugins/ml/server/routes/job_validation.ts index 64c9ccd27720a..ae2e6885ba0f3 100644 --- a/x-pack/legacy/plugins/ml/server/routes/job_validation.ts +++ b/x-pack/plugins/ml/server/routes/job_validation.ts @@ -7,15 +7,15 @@ import Boom from 'boom'; import { RequestHandlerContext } from 'src/core/server'; import { schema, TypeOf } from '@kbn/config-schema'; -import { licensePreRoutingFactory } from '../new_platform/licence_check_pre_routing_factory'; +import { licensePreRoutingFactory } from './license_check_pre_routing_factory'; import { wrapError } from '../client/error_wrapper'; -import { RouteInitialization } from '../new_platform/plugin'; +import { RouteInitialization } from '../types'; import { estimateBucketSpanSchema, modelMemoryLimitSchema, validateCardinalitySchema, validateJobSchema, -} from '../new_platform/job_validation_schema'; +} from './schemas/job_validation_schema'; import { estimateBucketSpanFactory } from '../models/bucket_span_estimator'; import { calculateModelMemoryLimitProvider } from '../models/calculate_model_memory_limit'; import { validateJob, validateCardinality } from '../models/job_validation'; @@ -25,7 +25,10 @@ type CalculateModelMemoryLimitPayload = TypeOf; /** * Routes for job validation */ -export function jobValidationRoutes({ config, xpackMainPlugin, router }: RouteInitialization) { +export function jobValidationRoutes( + { getLicenseCheckResults, router }: RouteInitialization, + version: string +) { function calculateModelMemoryLimit( context: RequestHandlerContext, payload: CalculateModelMemoryLimitPayload @@ -67,13 +70,13 @@ export function jobValidationRoutes({ config, xpackMainPlugin, router }: RouteIn body: estimateBucketSpanSchema, }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { let errorResp; const resp = await estimateBucketSpanFactory( context.ml!.mlClient.callAsCurrentUser, context.core.elasticsearch.adminClient.callAsInternalUser, - xpackMainPlugin + getLicenseCheckResults().isSecurityDisabled )(request.body) // this catch gets triggered when the estimation code runs without error // but isn't able to come up with a bucket span estimation. @@ -114,7 +117,7 @@ export function jobValidationRoutes({ config, xpackMainPlugin, router }: RouteIn body: modelMemoryLimitSchema, }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const resp = await calculateModelMemoryLimit(context, request.body); @@ -141,7 +144,7 @@ export function jobValidationRoutes({ config, xpackMainPlugin, router }: RouteIn body: schema.object(validateCardinalitySchema), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const resp = await validateCardinality( context.ml!.mlClient.callAsCurrentUser, @@ -171,16 +174,15 @@ export function jobValidationRoutes({ config, xpackMainPlugin, router }: RouteIn body: validateJobSchema, }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { - // pkg.branch corresponds to the version used in documentation links. - const version = config.get('pkg.branch'); + // version corresponds to the version used in documentation links. const resp = await validateJob( context.ml!.mlClient.callAsCurrentUser, request.body, version, context.core.elasticsearch.adminClient.callAsInternalUser, - xpackMainPlugin + getLicenseCheckResults().isSecurityDisabled ); return response.ok({ diff --git a/x-pack/legacy/plugins/ml/server/new_platform/licence_check_pre_routing_factory.ts b/x-pack/plugins/ml/server/routes/license_check_pre_routing_factory.ts similarity index 71% rename from x-pack/legacy/plugins/ml/server/new_platform/licence_check_pre_routing_factory.ts rename to x-pack/plugins/ml/server/routes/license_check_pre_routing_factory.ts index cc77d2872fb90..a371af1abf2d1 100644 --- a/x-pack/legacy/plugins/ml/server/new_platform/licence_check_pre_routing_factory.ts +++ b/x-pack/plugins/ml/server/routes/license_check_pre_routing_factory.ts @@ -10,10 +10,10 @@ import { RequestHandler, RequestHandlerContext, } from 'src/core/server'; -import { PLUGIN_ID, MlXpackMainPlugin } from './plugin'; +import { LicenseCheckResult } from '../types'; export const licensePreRoutingFactory = ( - xpackMainPlugin: MlXpackMainPlugin, + getLicenseCheckResults: () => LicenseCheckResult, handler: RequestHandler ): RequestHandler => { // License checking and enable/disable logic @@ -22,14 +22,10 @@ export const licensePreRoutingFactory = ( request: KibanaRequest, response: KibanaResponseFactory ) { - const licenseCheckResults = xpackMainPlugin.info.feature(PLUGIN_ID).getLicenseCheckResults(); + const licenseCheckResults = getLicenseCheckResults(); if (!licenseCheckResults.isAvailable) { - return response.forbidden({ - body: { - message: licenseCheckResults.message, - }, - }); + return response.forbidden(); } return handler(ctx, request, response); diff --git a/x-pack/legacy/plugins/ml/server/routes/modules.ts b/x-pack/plugins/ml/server/routes/modules.ts similarity index 88% rename from x-pack/legacy/plugins/ml/server/routes/modules.ts rename to x-pack/plugins/ml/server/routes/modules.ts index a40fb1c9149ca..c9b005d4e43f9 100644 --- a/x-pack/legacy/plugins/ml/server/routes/modules.ts +++ b/x-pack/plugins/ml/server/routes/modules.ts @@ -6,12 +6,12 @@ import { schema } from '@kbn/config-schema'; import { RequestHandlerContext } from 'kibana/server'; -import { DatafeedOverride, JobOverride } from '../../common/types/modules'; +import { DatafeedOverride, JobOverride } from '../../../../legacy/plugins/ml/common/types/modules'; import { wrapError } from '../client/error_wrapper'; import { DataRecognizer } from '../models/data_recognizer'; -import { licensePreRoutingFactory } from '../new_platform/licence_check_pre_routing_factory'; -import { getModuleIdParamSchema, setupModuleBodySchema } from '../new_platform/modules'; -import { RouteInitialization } from '../new_platform/plugin'; +import { licensePreRoutingFactory } from './license_check_pre_routing_factory'; +import { getModuleIdParamSchema, setupModuleBodySchema } from './schemas/modules'; +import { RouteInitialization } from '../types'; function recognize(context: RequestHandlerContext, indexPatternTitle: string) { const dr = new DataRecognizer(context); @@ -65,7 +65,7 @@ function dataRecognizerJobsExist(context: RequestHandlerContext, moduleId: strin /** * Recognizer routes. */ -export function dataRecognizer({ xpackMainPlugin, router }: RouteInitialization) { +export function dataRecognizer({ router, getLicenseCheckResults }: RouteInitialization) { /** * @apiGroup DataRecognizer * @@ -84,7 +84,7 @@ export function dataRecognizer({ xpackMainPlugin, router }: RouteInitialization) }), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const { indexPatternTitle } = request.params; const results = await recognize(context, indexPatternTitle); @@ -114,7 +114,7 @@ export function dataRecognizer({ xpackMainPlugin, router }: RouteInitialization) }), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { let { moduleId } = request.params; if (moduleId === '') { @@ -150,7 +150,7 @@ export function dataRecognizer({ xpackMainPlugin, router }: RouteInitialization) body: setupModuleBodySchema, }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const { moduleId } = request.params; @@ -207,7 +207,7 @@ export function dataRecognizer({ xpackMainPlugin, router }: RouteInitialization) }), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const { moduleId } = request.params; const result = await dataRecognizerJobsExist(context, moduleId); diff --git a/x-pack/legacy/plugins/ml/server/routes/notification_settings.ts b/x-pack/plugins/ml/server/routes/notification_settings.ts similarity index 75% rename from x-pack/legacy/plugins/ml/server/routes/notification_settings.ts rename to x-pack/plugins/ml/server/routes/notification_settings.ts index c65627543b21d..b68d2441333f9 100644 --- a/x-pack/legacy/plugins/ml/server/routes/notification_settings.ts +++ b/x-pack/plugins/ml/server/routes/notification_settings.ts @@ -4,14 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import { licensePreRoutingFactory } from '../new_platform/licence_check_pre_routing_factory'; +import { licensePreRoutingFactory } from './license_check_pre_routing_factory'; import { wrapError } from '../client/error_wrapper'; -import { RouteInitialization } from '../new_platform/plugin'; +import { RouteInitialization } from '../types'; /** * Routes for notification settings */ -export function notificationRoutes({ xpackMainPlugin, router }: RouteInitialization) { +export function notificationRoutes({ router, getLicenseCheckResults }: RouteInitialization) { /** * @apiGroup NotificationSettings * @@ -24,7 +24,7 @@ export function notificationRoutes({ xpackMainPlugin, router }: RouteInitializat path: '/api/ml/notification_settings', validate: false, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const params = { includeDefaults: true, diff --git a/x-pack/legacy/plugins/ml/server/routes/results_service.ts b/x-pack/plugins/ml/server/routes/results_service.ts similarity index 88% rename from x-pack/legacy/plugins/ml/server/routes/results_service.ts rename to x-pack/plugins/ml/server/routes/results_service.ts index 5d107b2d97809..77c998acc9f27 100644 --- a/x-pack/legacy/plugins/ml/server/routes/results_service.ts +++ b/x-pack/plugins/ml/server/routes/results_service.ts @@ -6,16 +6,16 @@ import { RequestHandlerContext } from 'src/core/server'; import { schema } from '@kbn/config-schema'; -import { licensePreRoutingFactory } from '../new_platform/licence_check_pre_routing_factory'; +import { licensePreRoutingFactory } from './license_check_pre_routing_factory'; import { wrapError } from '../client/error_wrapper'; -import { RouteInitialization } from '../new_platform/plugin'; +import { RouteInitialization } from '../types'; import { anomaliesTableDataSchema, categoryDefinitionSchema, categoryExamplesSchema, maxAnomalyScoreSchema, partitionFieldValuesSchema, -} from '../new_platform/results_service_schema'; +} from './schemas/results_service_schema'; import { resultsServiceProvider } from '../models/results_service'; function getAnomaliesTableData(context: RequestHandlerContext, payload: any) { @@ -74,7 +74,7 @@ function getPartitionFieldsValues(context: RequestHandlerContext, payload: any) /** * Routes for results service */ -export function resultsServiceRoutes({ xpackMainPlugin, router }: RouteInitialization) { +export function resultsServiceRoutes({ router, getLicenseCheckResults }: RouteInitialization) { /** * @apiGroup ResultsService * @@ -89,7 +89,7 @@ export function resultsServiceRoutes({ xpackMainPlugin, router }: RouteInitializ body: schema.object(anomaliesTableDataSchema), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const resp = await getAnomaliesTableData(context, request.body); @@ -116,7 +116,7 @@ export function resultsServiceRoutes({ xpackMainPlugin, router }: RouteInitializ body: schema.object(categoryDefinitionSchema), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const resp = await getCategoryDefinition(context, request.body); @@ -143,7 +143,7 @@ export function resultsServiceRoutes({ xpackMainPlugin, router }: RouteInitializ body: schema.object(maxAnomalyScoreSchema), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const resp = await getMaxAnomalyScore(context, request.body); @@ -170,7 +170,7 @@ export function resultsServiceRoutes({ xpackMainPlugin, router }: RouteInitializ body: schema.object(categoryExamplesSchema), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const resp = await getCategoryExamples(context, request.body); @@ -197,7 +197,7 @@ export function resultsServiceRoutes({ xpackMainPlugin, router }: RouteInitializ body: schema.object(partitionFieldValuesSchema), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const resp = await getPartitionFieldsValues(context, request.body); diff --git a/x-pack/legacy/plugins/ml/server/new_platform/annotations_schema.ts b/x-pack/plugins/ml/server/routes/schemas/annotations_schema.ts similarity index 100% rename from x-pack/legacy/plugins/ml/server/new_platform/annotations_schema.ts rename to x-pack/plugins/ml/server/routes/schemas/annotations_schema.ts diff --git a/x-pack/legacy/plugins/ml/server/new_platform/anomaly_detectors_schema.ts b/x-pack/plugins/ml/server/routes/schemas/anomaly_detectors_schema.ts similarity index 100% rename from x-pack/legacy/plugins/ml/server/new_platform/anomaly_detectors_schema.ts rename to x-pack/plugins/ml/server/routes/schemas/anomaly_detectors_schema.ts diff --git a/x-pack/legacy/plugins/ml/server/new_platform/calendars_schema.ts b/x-pack/plugins/ml/server/routes/schemas/calendars_schema.ts similarity index 100% rename from x-pack/legacy/plugins/ml/server/new_platform/calendars_schema.ts rename to x-pack/plugins/ml/server/routes/schemas/calendars_schema.ts diff --git a/x-pack/legacy/plugins/ml/server/new_platform/data_analytics_schema.ts b/x-pack/plugins/ml/server/routes/schemas/data_analytics_schema.ts similarity index 100% rename from x-pack/legacy/plugins/ml/server/new_platform/data_analytics_schema.ts rename to x-pack/plugins/ml/server/routes/schemas/data_analytics_schema.ts diff --git a/x-pack/legacy/plugins/ml/server/new_platform/data_visualizer_schema.ts b/x-pack/plugins/ml/server/routes/schemas/data_visualizer_schema.ts similarity index 100% rename from x-pack/legacy/plugins/ml/server/new_platform/data_visualizer_schema.ts rename to x-pack/plugins/ml/server/routes/schemas/data_visualizer_schema.ts diff --git a/x-pack/legacy/plugins/ml/server/new_platform/datafeeds_schema.ts b/x-pack/plugins/ml/server/routes/schemas/datafeeds_schema.ts similarity index 100% rename from x-pack/legacy/plugins/ml/server/new_platform/datafeeds_schema.ts rename to x-pack/plugins/ml/server/routes/schemas/datafeeds_schema.ts diff --git a/x-pack/legacy/plugins/ml/server/new_platform/fields_service_schema.ts b/x-pack/plugins/ml/server/routes/schemas/fields_service_schema.ts similarity index 100% rename from x-pack/legacy/plugins/ml/server/new_platform/fields_service_schema.ts rename to x-pack/plugins/ml/server/routes/schemas/fields_service_schema.ts diff --git a/x-pack/legacy/plugins/ml/server/new_platform/filters_schema.ts b/x-pack/plugins/ml/server/routes/schemas/filters_schema.ts similarity index 100% rename from x-pack/legacy/plugins/ml/server/new_platform/filters_schema.ts rename to x-pack/plugins/ml/server/routes/schemas/filters_schema.ts diff --git a/x-pack/legacy/plugins/ml/server/new_platform/job_service_schema.ts b/x-pack/plugins/ml/server/routes/schemas/job_service_schema.ts similarity index 100% rename from x-pack/legacy/plugins/ml/server/new_platform/job_service_schema.ts rename to x-pack/plugins/ml/server/routes/schemas/job_service_schema.ts diff --git a/x-pack/legacy/plugins/ml/server/new_platform/job_validation_schema.ts b/x-pack/plugins/ml/server/routes/schemas/job_validation_schema.ts similarity index 100% rename from x-pack/legacy/plugins/ml/server/new_platform/job_validation_schema.ts rename to x-pack/plugins/ml/server/routes/schemas/job_validation_schema.ts diff --git a/x-pack/legacy/plugins/ml/server/new_platform/modules.ts b/x-pack/plugins/ml/server/routes/schemas/modules.ts similarity index 100% rename from x-pack/legacy/plugins/ml/server/new_platform/modules.ts rename to x-pack/plugins/ml/server/routes/schemas/modules.ts diff --git a/x-pack/legacy/plugins/ml/server/new_platform/results_service_schema.ts b/x-pack/plugins/ml/server/routes/schemas/results_service_schema.ts similarity index 100% rename from x-pack/legacy/plugins/ml/server/new_platform/results_service_schema.ts rename to x-pack/plugins/ml/server/routes/schemas/results_service_schema.ts diff --git a/x-pack/legacy/plugins/ml/server/routes/system.ts b/x-pack/plugins/ml/server/routes/system.ts similarity index 88% rename from x-pack/legacy/plugins/ml/server/routes/system.ts rename to x-pack/plugins/ml/server/routes/system.ts index 5861b53d74875..36a9ea1447f58 100644 --- a/x-pack/legacy/plugins/ml/server/routes/system.ts +++ b/x-pack/plugins/ml/server/routes/system.ts @@ -11,20 +11,17 @@ import { RequestHandlerContext } from 'kibana/server'; import { wrapError } from '../client/error_wrapper'; import { mlLog } from '../client/log'; import { privilegesProvider } from '../lib/check_privileges'; -import { isSecurityDisabled } from '../lib/security_utils'; import { spacesUtilsProvider } from '../lib/spaces_utils'; -import { licensePreRoutingFactory } from '../new_platform/licence_check_pre_routing_factory'; -import { RouteInitialization } from '../new_platform/plugin'; +import { licensePreRoutingFactory } from './license_check_pre_routing_factory'; +import { RouteInitialization, SystemRouteDeps } from '../types'; /** * System routes */ -export function systemRoutes({ - router, - xpackMainPlugin, - spacesPlugin, - cloud, -}: RouteInitialization) { +export function systemRoutes( + { getLicenseCheckResults, router }: RouteInitialization, + { spacesPlugin, cloud }: SystemRouteDeps +) { async function getNodeCount(context: RequestHandlerContext) { const filterPath = 'nodes.*.attributes'; const resp = await context.ml!.mlClient.callAsInternalUser('nodes.info', { @@ -59,7 +56,7 @@ export function systemRoutes({ body: schema.maybe(schema.any()), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { let upgradeInProgress = false; try { @@ -80,7 +77,7 @@ export function systemRoutes({ } } - if (isSecurityDisabled(xpackMainPlugin)) { + if (getLicenseCheckResults().isSecurityDisabled) { // if xpack.security.enabled has been explicitly set to false // return that security is disabled and don't call the privilegeCheck endpoint return response.ok({ @@ -119,7 +116,7 @@ export function systemRoutes({ }), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const ignoreSpaces = request.query && request.query.ignoreSpaces === 'true'; // if spaces is disabled force isMlEnabledInSpace to be true @@ -130,7 +127,7 @@ export function systemRoutes({ const { getPrivileges } = privilegesProvider( context.ml!.mlClient.callAsCurrentUser, - xpackMainPlugin, + getLicenseCheckResults(), isMlEnabledInSpace, ignoreSpaces ); @@ -155,11 +152,11 @@ export function systemRoutes({ path: '/api/ml/ml_node_count', validate: false, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { // check for basic license first for consistency with other // security disabled checks - if (isSecurityDisabled(xpackMainPlugin)) { + if (getLicenseCheckResults().isSecurityDisabled) { return response.ok({ body: await getNodeCount(context), }); @@ -206,7 +203,7 @@ export function systemRoutes({ path: '/api/ml/info', validate: false, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { const info = await context.ml!.mlClient.callAsCurrentUser('ml.info'); const cloudId = cloud && cloud.cloudId; @@ -234,7 +231,7 @@ export function systemRoutes({ body: schema.maybe(schema.any()), }, }, - licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { try { return response.ok({ body: await context.ml!.mlClient.callAsCurrentUser('search', request.body), diff --git a/x-pack/plugins/ml/server/types.ts b/x-pack/plugins/ml/server/types.ts new file mode 100644 index 0000000000000..550abadb3c06f --- /dev/null +++ b/x-pack/plugins/ml/server/types.ts @@ -0,0 +1,43 @@ +/* + * 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 { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; +import { HomeServerPluginSetup } from 'src/plugins/home/server'; +import { IRouter } from 'src/core/server'; +import { CloudSetup } from '../../cloud/server'; +import { SecurityPluginSetup } from '../../security/server'; +import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server'; +import { LicensingPluginSetup } from '../../licensing/server'; +import { SpacesPluginSetup } from '../../spaces/server'; + +export interface LicenseCheckResult { + isAvailable: boolean; + isActive: boolean; + isEnabled: boolean; + isSecurityDisabled: boolean; + status?: string; + type?: string; +} + +export interface SystemRouteDeps { + cloud: CloudSetup; + spacesPlugin: SpacesPluginSetup; +} + +export interface PluginsSetup { + cloud: CloudSetup; + features: FeaturesPluginSetup; + home: HomeServerPluginSetup; + licensing: LicensingPluginSetup; + security: SecurityPluginSetup; + spaces: SpacesPluginSetup; + usageCollection: UsageCollectionSetup; +} + +export interface RouteInitialization { + router: IRouter; + getLicenseCheckResults: () => LicenseCheckResult; +} diff --git a/x-pack/plugins/security/server/authentication/authenticator.test.ts b/x-pack/plugins/security/server/authentication/authenticator.test.ts index 65874ba3a461e..af019ff10dedc 100644 --- a/x-pack/plugins/security/server/authentication/authenticator.test.ts +++ b/x-pack/plugins/security/server/authentication/authenticator.test.ts @@ -5,6 +5,8 @@ */ jest.mock('./providers/basic'); +jest.mock('./providers/saml'); +jest.mock('./providers/http'); import Boom from 'boom'; import { duration, Duration } from 'moment'; @@ -23,15 +25,27 @@ import { Authenticator, AuthenticatorOptions, ProviderSession } from './authenti import { DeauthenticationResult } from './deauthentication_result'; import { BasicAuthenticationProvider } from './providers'; -function getMockOptions(config: Partial = {}) { +function getMockOptions({ + session, + providers, + http = {}, +}: { + session?: AuthenticatorOptions['config']['session']; + providers?: string[]; + http?: Partial; +} = {}) { return { clusterClient: elasticsearchServiceMock.createClusterClient(), basePath: httpServiceMock.createSetupContract().basePath, loggers: loggingServiceMock.create(), config: { - session: { idleTimeout: null, lifespan: null }, - authc: { providers: [], oidc: {}, saml: {} }, - ...config, + session: { idleTimeout: null, lifespan: null, ...(session || {}) }, + authc: { + providers: providers || [], + oidc: {}, + saml: {}, + http: { enabled: true, autoSchemesEnabled: true, schemes: ['apikey'], ...http }, + }, }, sessionStorageFactory: sessionStorageMock.createFactory(), }; @@ -44,8 +58,13 @@ describe('Authenticator', () => { login: jest.fn(), authenticate: jest.fn(), logout: jest.fn(), + getHTTPAuthenticationScheme: jest.fn(), }; + jest.requireMock('./providers/http').HTTPAuthenticationProvider.mockImplementation(() => ({ + authenticate: jest.fn().mockResolvedValue(AuthenticationResult.notHandled()), + })); + jest .requireMock('./providers/basic') .BasicAuthenticationProvider.mockImplementation(() => mockBasicAuthenticationProvider); @@ -55,23 +74,80 @@ describe('Authenticator', () => { describe('initialization', () => { it('fails if authentication providers are not configured.', () => { - const mockOptions = getMockOptions({ - authc: { providers: [], oidc: {}, saml: {} }, - }); - expect(() => new Authenticator(mockOptions)).toThrowError( + expect(() => new Authenticator(getMockOptions())).toThrowError( 'No authentication provider is configured. Verify `xpack.security.authc.providers` config value.' ); }); it('fails if configured authentication provider is not known.', () => { - const mockOptions = getMockOptions({ - authc: { providers: ['super-basic'], oidc: {}, saml: {} }, - }); - - expect(() => new Authenticator(mockOptions)).toThrowError( + expect(() => new Authenticator(getMockOptions({ providers: ['super-basic'] }))).toThrowError( 'Unsupported authentication provider name: super-basic.' ); }); + + describe('HTTP authentication provider', () => { + beforeEach(() => { + jest + .requireMock('./providers/basic') + .BasicAuthenticationProvider.mockImplementation(() => ({ + getHTTPAuthenticationScheme: jest.fn().mockReturnValue('basic'), + })); + }); + + afterEach(() => jest.resetAllMocks()); + + it('enabled by default', () => { + const authenticator = new Authenticator(getMockOptions({ providers: ['basic'] })); + expect(authenticator.isProviderEnabled('basic')).toBe(true); + expect(authenticator.isProviderEnabled('http')).toBe(true); + + expect( + jest.requireMock('./providers/http').HTTPAuthenticationProvider + ).toHaveBeenCalledWith(expect.anything(), { + supportedSchemes: new Set(['apikey', 'basic']), + }); + }); + + it('includes all required schemes if `autoSchemesEnabled` is enabled', () => { + const authenticator = new Authenticator( + getMockOptions({ providers: ['basic', 'kerberos'] }) + ); + expect(authenticator.isProviderEnabled('basic')).toBe(true); + expect(authenticator.isProviderEnabled('kerberos')).toBe(true); + expect(authenticator.isProviderEnabled('http')).toBe(true); + + expect( + jest.requireMock('./providers/http').HTTPAuthenticationProvider + ).toHaveBeenCalledWith(expect.anything(), { + supportedSchemes: new Set(['apikey', 'basic', 'bearer']), + }); + }); + + it('does not include additional schemes if `autoSchemesEnabled` is disabled', () => { + const authenticator = new Authenticator( + getMockOptions({ providers: ['basic', 'kerberos'], http: { autoSchemesEnabled: false } }) + ); + expect(authenticator.isProviderEnabled('basic')).toBe(true); + expect(authenticator.isProviderEnabled('kerberos')).toBe(true); + expect(authenticator.isProviderEnabled('http')).toBe(true); + + expect( + jest.requireMock('./providers/http').HTTPAuthenticationProvider + ).toHaveBeenCalledWith(expect.anything(), { supportedSchemes: new Set(['apikey']) }); + }); + + it('disabled if explicitly disabled', () => { + const authenticator = new Authenticator( + getMockOptions({ providers: ['basic'], http: { enabled: false } }) + ); + expect(authenticator.isProviderEnabled('basic')).toBe(true); + expect(authenticator.isProviderEnabled('http')).toBe(false); + + expect( + jest.requireMock('./providers/http').HTTPAuthenticationProvider + ).not.toHaveBeenCalled(); + }); + }); }); describe('`login` method', () => { @@ -80,9 +156,7 @@ describe('Authenticator', () => { let mockSessionStorage: jest.Mocked>; let mockSessVal: any; beforeEach(() => { - mockOptions = getMockOptions({ - authc: { providers: ['basic'], oidc: {}, saml: {} }, - }); + mockOptions = getMockOptions({ providers: ['basic'] }); mockSessionStorage = sessionStorageMock.create(); mockOptions.sessionStorageFactory.asScoped.mockReturnValue(mockSessionStorage); mockSessVal = { @@ -124,12 +198,9 @@ describe('Authenticator', () => { AuthenticationResult.failed(failureReason) ); - const authenticationResult = await authenticator.login(request, { - provider: 'basic', - value: {}, - }); - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.error).toBe(failureReason); + await expect(authenticator.login(request, { provider: 'basic', value: {} })).resolves.toEqual( + AuthenticationResult.failed(failureReason) + ); }); it('returns user that authentication provider returns.', async () => { @@ -140,13 +211,9 @@ describe('Authenticator', () => { AuthenticationResult.succeeded(user, { authHeaders: { authorization: 'Basic .....' } }) ); - const authenticationResult = await authenticator.login(request, { - provider: 'basic', - value: {}, - }); - expect(authenticationResult.succeeded()).toBe(true); - expect(authenticationResult.user).toEqual(user); - expect(authenticationResult.authHeaders).toEqual({ authorization: 'Basic .....' }); + await expect(authenticator.login(request, { provider: 'basic', value: {} })).resolves.toEqual( + AuthenticationResult.succeeded(user, { authHeaders: { authorization: 'Basic .....' } }) + ); }); it('creates session whenever authentication provider returns state', async () => { @@ -158,12 +225,9 @@ describe('Authenticator', () => { AuthenticationResult.succeeded(user, { state: { authorization } }) ); - const authenticationResult = await authenticator.login(request, { - provider: 'basic', - value: {}, - }); - expect(authenticationResult.succeeded()).toBe(true); - expect(authenticationResult.user).toEqual(user); + await expect(authenticator.login(request, { provider: 'basic', value: {} })).resolves.toEqual( + AuthenticationResult.succeeded(user, { state: { authorization } }) + ); expect(mockSessionStorage.set).toHaveBeenCalledTimes(1); expect(mockSessionStorage.set).toHaveBeenCalledWith({ @@ -174,11 +238,9 @@ describe('Authenticator', () => { it('returns `notHandled` if login attempt is targeted to not configured provider.', async () => { const request = httpServerMock.createKibanaRequest(); - const authenticationResult = await authenticator.login(request, { - provider: 'token', - value: {}, - }); - expect(authenticationResult.notHandled()).toBe(true); + await expect(authenticator.login(request, { provider: 'token', value: {} })).resolves.toEqual( + AuthenticationResult.notHandled() + ); }); it('clears session if it belongs to a different provider.', async () => { @@ -189,12 +251,9 @@ describe('Authenticator', () => { mockBasicAuthenticationProvider.login.mockResolvedValue(AuthenticationResult.succeeded(user)); mockSessionStorage.get.mockResolvedValue({ ...mockSessVal, provider: 'token' }); - const authenticationResult = await authenticator.login(request, { - provider: 'basic', - value: credentials, - }); - expect(authenticationResult.succeeded()).toBe(true); - expect(authenticationResult.user).toBe(user); + await expect( + authenticator.login(request, { provider: 'basic', value: credentials }) + ).resolves.toEqual(AuthenticationResult.succeeded(user)); expect(mockBasicAuthenticationProvider.login).toHaveBeenCalledWith( request, @@ -214,12 +273,9 @@ describe('Authenticator', () => { AuthenticationResult.succeeded(user, { state: null }) ); - const authenticationResult = await authenticator.login(request, { - provider: 'basic', - value: {}, - }); - expect(authenticationResult.succeeded()).toBe(true); - expect(authenticationResult.user).toEqual(user); + await expect(authenticator.login(request, { provider: 'basic', value: {} })).resolves.toEqual( + AuthenticationResult.succeeded(user, { state: null }) + ); expect(mockSessionStorage.set).not.toHaveBeenCalled(); expect(mockSessionStorage.clear).toHaveBeenCalled(); @@ -232,9 +288,7 @@ describe('Authenticator', () => { let mockSessionStorage: jest.Mocked>; let mockSessVal: any; beforeEach(() => { - mockOptions = getMockOptions({ - authc: { providers: ['basic'], oidc: {}, saml: {} }, - }); + mockOptions = getMockOptions({ providers: ['basic'] }); mockSessionStorage = sessionStorageMock.create(); mockOptions.sessionStorageFactory.asScoped.mockReturnValue(mockSessionStorage); mockSessVal = { @@ -277,10 +331,9 @@ describe('Authenticator', () => { AuthenticationResult.succeeded(user, { authHeaders: { authorization: 'Basic .....' } }) ); - const authenticationResult = await authenticator.authenticate(request); - expect(authenticationResult.succeeded()).toBe(true); - expect(authenticationResult.user).toEqual(user); - expect(authenticationResult.authHeaders).toEqual({ authorization: 'Basic .....' }); + await expect(authenticator.authenticate(request)).resolves.toEqual( + AuthenticationResult.succeeded(user, { authHeaders: { authorization: 'Basic .....' } }) + ); }); it('creates session whenever authentication provider returns state for system API requests', async () => { @@ -294,9 +347,9 @@ describe('Authenticator', () => { AuthenticationResult.succeeded(user, { state: { authorization } }) ); - const systemAPIAuthenticationResult = await authenticator.authenticate(request); - expect(systemAPIAuthenticationResult.succeeded()).toBe(true); - expect(systemAPIAuthenticationResult.user).toEqual(user); + await expect(authenticator.authenticate(request)).resolves.toEqual( + AuthenticationResult.succeeded(user, { state: { authorization } }) + ); expect(mockSessionStorage.set).toHaveBeenCalledTimes(1); expect(mockSessionStorage.set).toHaveBeenCalledWith({ @@ -316,9 +369,9 @@ describe('Authenticator', () => { AuthenticationResult.succeeded(user, { state: { authorization } }) ); - const systemAPIAuthenticationResult = await authenticator.authenticate(request); - expect(systemAPIAuthenticationResult.succeeded()).toBe(true); - expect(systemAPIAuthenticationResult.user).toEqual(user); + await expect(authenticator.authenticate(request)).resolves.toEqual( + AuthenticationResult.succeeded(user, { state: { authorization } }) + ); expect(mockSessionStorage.set).toHaveBeenCalledTimes(1); expect(mockSessionStorage.set).toHaveBeenCalledWith({ @@ -338,9 +391,9 @@ describe('Authenticator', () => { ); mockSessionStorage.get.mockResolvedValue(mockSessVal); - const authenticationResult = await authenticator.authenticate(request); - expect(authenticationResult.succeeded()).toBe(true); - expect(authenticationResult.user).toEqual(user); + await expect(authenticator.authenticate(request)).resolves.toEqual( + AuthenticationResult.succeeded(user) + ); expect(mockSessionStorage.set).not.toHaveBeenCalled(); expect(mockSessionStorage.clear).not.toHaveBeenCalled(); @@ -357,9 +410,9 @@ describe('Authenticator', () => { ); mockSessionStorage.get.mockResolvedValue(mockSessVal); - const authenticationResult = await authenticator.authenticate(request); - expect(authenticationResult.succeeded()).toBe(true); - expect(authenticationResult.user).toEqual(user); + await expect(authenticator.authenticate(request)).resolves.toEqual( + AuthenticationResult.succeeded(user) + ); expect(mockSessionStorage.set).toHaveBeenCalledTimes(1); expect(mockSessionStorage.set).toHaveBeenCalledWith(mockSessVal); @@ -377,7 +430,7 @@ describe('Authenticator', () => { idleTimeout: duration(3600 * 24), lifespan: null, }, - authc: { providers: ['basic'], oidc: {}, saml: {} }, + providers: ['basic'], }); mockSessionStorage = sessionStorageMock.create(); @@ -392,9 +445,9 @@ describe('Authenticator', () => { jest.spyOn(Date, 'now').mockImplementation(() => currentDate); - const authenticationResult = await authenticator.authenticate(request); - expect(authenticationResult.succeeded()).toBe(true); - expect(authenticationResult.user).toEqual(user); + await expect(authenticator.authenticate(request)).resolves.toEqual( + AuthenticationResult.succeeded(user) + ); expect(mockSessionStorage.set).toHaveBeenCalledTimes(1); expect(mockSessionStorage.set).toHaveBeenCalledWith({ @@ -416,7 +469,7 @@ describe('Authenticator', () => { idleTimeout: duration(hr * 2), lifespan: duration(hr * 8), }, - authc: { providers: ['basic'], oidc: {}, saml: {} }, + providers: ['basic'], }); mockSessionStorage = sessionStorageMock.create(); @@ -437,9 +490,9 @@ describe('Authenticator', () => { jest.spyOn(Date, 'now').mockImplementation(() => currentDate); - const authenticationResult = await authenticator.authenticate(request); - expect(authenticationResult.succeeded()).toBe(true); - expect(authenticationResult.user).toEqual(user); + await expect(authenticator.authenticate(request)).resolves.toEqual( + AuthenticationResult.succeeded(user) + ); expect(mockSessionStorage.set).toHaveBeenCalledTimes(1); expect(mockSessionStorage.set).toHaveBeenCalledWith({ @@ -468,7 +521,7 @@ describe('Authenticator', () => { idleTimeout: null, lifespan, }, - authc: { providers: ['basic'], oidc: {}, saml: {} }, + providers: ['basic'], }); mockSessionStorage = sessionStorageMock.create(); @@ -485,9 +538,9 @@ describe('Authenticator', () => { AuthenticationResult.succeeded(user) ); - const authenticationResult = await authenticator.authenticate(request); - expect(authenticationResult.succeeded()).toBe(true); - expect(authenticationResult.user).toEqual(user); + await expect(authenticator.authenticate(request)).resolves.toEqual( + AuthenticationResult.succeeded(user) + ); expect(mockSessionStorage.set).toHaveBeenCalledTimes(1); expect(mockSessionStorage.set).toHaveBeenCalledWith({ @@ -517,13 +570,15 @@ describe('Authenticator', () => { headers: { 'kbn-system-request': 'true' }, }); + const failureReason = new Error('some error'); mockBasicAuthenticationProvider.authenticate.mockResolvedValue( - AuthenticationResult.failed(new Error('some error')) + AuthenticationResult.failed(failureReason) ); mockSessionStorage.get.mockResolvedValue(mockSessVal); - const authenticationResult = await authenticator.authenticate(request); - expect(authenticationResult.failed()).toBe(true); + await expect(authenticator.authenticate(request)).resolves.toEqual( + AuthenticationResult.failed(failureReason) + ); expect(mockSessionStorage.set).not.toHaveBeenCalled(); expect(mockSessionStorage.clear).not.toHaveBeenCalled(); @@ -534,13 +589,15 @@ describe('Authenticator', () => { headers: { 'kbn-system-request': 'false' }, }); + const failureReason = new Error('some error'); mockBasicAuthenticationProvider.authenticate.mockResolvedValue( - AuthenticationResult.failed(new Error('some error')) + AuthenticationResult.failed(failureReason) ); mockSessionStorage.get.mockResolvedValue(mockSessVal); - const authenticationResult = await authenticator.authenticate(request); - expect(authenticationResult.failed()).toBe(true); + await expect(authenticator.authenticate(request)).resolves.toEqual( + AuthenticationResult.failed(failureReason) + ); expect(mockSessionStorage.set).not.toHaveBeenCalled(); expect(mockSessionStorage.clear).not.toHaveBeenCalled(); @@ -558,9 +615,9 @@ describe('Authenticator', () => { ); mockSessionStorage.get.mockResolvedValue(mockSessVal); - const authenticationResult = await authenticator.authenticate(request); - expect(authenticationResult.succeeded()).toBe(true); - expect(authenticationResult.user).toEqual(user); + await expect(authenticator.authenticate(request)).resolves.toEqual( + AuthenticationResult.succeeded(user, { state: newState }) + ); expect(mockSessionStorage.set).toHaveBeenCalledTimes(1); expect(mockSessionStorage.set).toHaveBeenCalledWith({ @@ -582,9 +639,9 @@ describe('Authenticator', () => { ); mockSessionStorage.get.mockResolvedValue(mockSessVal); - const authenticationResult = await authenticator.authenticate(request); - expect(authenticationResult.succeeded()).toBe(true); - expect(authenticationResult.user).toEqual(user); + await expect(authenticator.authenticate(request)).resolves.toEqual( + AuthenticationResult.succeeded(user, { state: newState }) + ); expect(mockSessionStorage.set).toHaveBeenCalledTimes(1); expect(mockSessionStorage.set).toHaveBeenCalledWith({ @@ -604,8 +661,9 @@ describe('Authenticator', () => { ); mockSessionStorage.get.mockResolvedValue(mockSessVal); - const authenticationResult = await authenticator.authenticate(request); - expect(authenticationResult.failed()).toBe(true); + await expect(authenticator.authenticate(request)).resolves.toEqual( + AuthenticationResult.failed(Boom.unauthorized()) + ); expect(mockSessionStorage.set).not.toHaveBeenCalled(); expect(mockSessionStorage.clear).toHaveBeenCalled(); @@ -621,8 +679,9 @@ describe('Authenticator', () => { ); mockSessionStorage.get.mockResolvedValue(mockSessVal); - const authenticationResult = await authenticator.authenticate(request); - expect(authenticationResult.failed()).toBe(true); + await expect(authenticator.authenticate(request)).resolves.toEqual( + AuthenticationResult.failed(Boom.unauthorized()) + ); expect(mockSessionStorage.set).not.toHaveBeenCalled(); expect(mockSessionStorage.clear).toHaveBeenCalled(); @@ -636,8 +695,9 @@ describe('Authenticator', () => { ); mockSessionStorage.get.mockResolvedValue(mockSessVal); - const authenticationResult = await authenticator.authenticate(request); - expect(authenticationResult.redirected()).toBe(true); + await expect(authenticator.authenticate(request)).resolves.toEqual( + AuthenticationResult.redirectTo('some-url', { state: null }) + ); expect(mockSessionStorage.set).not.toHaveBeenCalled(); expect(mockSessionStorage.clear).toHaveBeenCalled(); @@ -653,8 +713,9 @@ describe('Authenticator', () => { ); mockSessionStorage.get.mockResolvedValue(mockSessVal); - const authenticationResult = await authenticator.authenticate(request); - expect(authenticationResult.notHandled()).toBe(true); + await expect(authenticator.authenticate(request)).resolves.toEqual( + AuthenticationResult.notHandled() + ); expect(mockSessionStorage.set).not.toHaveBeenCalled(); expect(mockSessionStorage.clear).not.toHaveBeenCalled(); @@ -670,8 +731,9 @@ describe('Authenticator', () => { ); mockSessionStorage.get.mockResolvedValue(mockSessVal); - const authenticationResult = await authenticator.authenticate(request); - expect(authenticationResult.notHandled()).toBe(true); + await expect(authenticator.authenticate(request)).resolves.toEqual( + AuthenticationResult.notHandled() + ); expect(mockSessionStorage.set).not.toHaveBeenCalled(); expect(mockSessionStorage.clear).not.toHaveBeenCalled(); @@ -687,8 +749,9 @@ describe('Authenticator', () => { ); mockSessionStorage.get.mockResolvedValue({ ...mockSessVal, provider: 'token' }); - const authenticationResult = await authenticator.authenticate(request); - expect(authenticationResult.notHandled()).toBe(true); + await expect(authenticator.authenticate(request)).resolves.toEqual( + AuthenticationResult.notHandled() + ); expect(mockSessionStorage.set).not.toHaveBeenCalled(); expect(mockSessionStorage.clear).toHaveBeenCalled(); @@ -704,8 +767,9 @@ describe('Authenticator', () => { ); mockSessionStorage.get.mockResolvedValue({ ...mockSessVal, provider: 'token' }); - const authenticationResult = await authenticator.authenticate(request); - expect(authenticationResult.notHandled()).toBe(true); + await expect(authenticator.authenticate(request)).resolves.toEqual( + AuthenticationResult.notHandled() + ); expect(mockSessionStorage.set).not.toHaveBeenCalled(); expect(mockSessionStorage.clear).toHaveBeenCalled(); @@ -718,9 +782,7 @@ describe('Authenticator', () => { let mockSessionStorage: jest.Mocked>; let mockSessVal: any; beforeEach(() => { - mockOptions = getMockOptions({ - authc: { providers: ['basic'], oidc: {}, saml: {} }, - }); + mockOptions = getMockOptions({ providers: ['basic'] }); mockSessionStorage = sessionStorageMock.create(); mockOptions.sessionStorageFactory.asScoped.mockReturnValue(mockSessionStorage); mockSessVal = { @@ -744,9 +806,10 @@ describe('Authenticator', () => { const request = httpServerMock.createKibanaRequest(); mockSessionStorage.get.mockResolvedValue(null); - const deauthenticationResult = await authenticator.logout(request); + await expect(authenticator.logout(request)).resolves.toEqual( + DeauthenticationResult.notHandled() + ); - expect(deauthenticationResult.notHandled()).toBe(true); expect(mockSessionStorage.clear).not.toHaveBeenCalled(); }); @@ -757,12 +820,12 @@ describe('Authenticator', () => { ); mockSessionStorage.get.mockResolvedValue(mockSessVal); - const deauthenticationResult = await authenticator.logout(request); + await expect(authenticator.logout(request)).resolves.toEqual( + DeauthenticationResult.redirectTo('some-url') + ); expect(mockBasicAuthenticationProvider.logout).toHaveBeenCalledTimes(1); expect(mockSessionStorage.clear).toHaveBeenCalled(); - expect(deauthenticationResult.redirected()).toBe(true); - expect(deauthenticationResult.redirectURL).toBe('some-url'); }); it('if session does not exist but provider name is valid, returns whatever authentication provider returns.', async () => { @@ -773,21 +836,22 @@ describe('Authenticator', () => { DeauthenticationResult.redirectTo('some-url') ); - const deauthenticationResult = await authenticator.logout(request); + await expect(authenticator.logout(request)).resolves.toEqual( + DeauthenticationResult.redirectTo('some-url') + ); 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); + await expect(authenticator.logout(request)).resolves.toEqual( + DeauthenticationResult.notHandled() + ); - expect(deauthenticationResult.notHandled()).toBe(true); expect(mockSessionStorage.clear).not.toHaveBeenCalled(); }); @@ -796,11 +860,12 @@ describe('Authenticator', () => { const state = { authorization: 'Bearer xxx' }; mockSessionStorage.get.mockResolvedValue({ ...mockSessVal, state, provider: 'token' }); - const deauthenticationResult = await authenticator.logout(request); + await expect(authenticator.logout(request)).resolves.toEqual( + DeauthenticationResult.notHandled() + ); expect(mockBasicAuthenticationProvider.logout).not.toHaveBeenCalled(); expect(mockSessionStorage.clear).toHaveBeenCalled(); - expect(deauthenticationResult.notHandled()).toBe(true); }); }); @@ -809,9 +874,7 @@ describe('Authenticator', () => { let mockOptions: ReturnType; let mockSessionStorage: jest.Mocked>; beforeEach(() => { - mockOptions = getMockOptions({ - authc: { providers: ['basic'], oidc: {}, saml: {} }, - }); + mockOptions = getMockOptions({ providers: ['basic'] }); mockSessionStorage = sessionStorageMock.create(); mockOptions.sessionStorageFactory.asScoped.mockReturnValue(mockSessionStorage); @@ -851,4 +914,16 @@ describe('Authenticator', () => { expect(sessionInfo).toBe(null); }); }); + + describe('`isProviderEnabled` method', () => { + it('returns `true` only if specified provider is enabled', () => { + let authenticator = new Authenticator(getMockOptions({ providers: ['basic'] })); + expect(authenticator.isProviderEnabled('basic')).toBe(true); + expect(authenticator.isProviderEnabled('saml')).toBe(false); + + authenticator = new Authenticator(getMockOptions({ providers: ['basic', 'saml'] })); + expect(authenticator.isProviderEnabled('basic')).toBe(true); + expect(authenticator.isProviderEnabled('saml')).toBe(true); + }); + }); }); diff --git a/x-pack/plugins/security/server/authentication/authenticator.ts b/x-pack/plugins/security/server/authentication/authenticator.ts index 3ab49d3c5b124..4954e1b24216c 100644 --- a/x-pack/plugins/security/server/authentication/authenticator.ts +++ b/x-pack/plugins/security/server/authentication/authenticator.ts @@ -27,6 +27,7 @@ import { TokenAuthenticationProvider, OIDCAuthenticationProvider, PKIAuthenticationProvider, + HTTPAuthenticationProvider, isSAMLRequestQuery, } from './providers'; import { AuthenticationResult } from './authentication_result'; @@ -191,6 +192,7 @@ export class Authenticator { client: this.options.clusterClient, logger: this.options.loggers.get('tokens'), }), + isProviderEnabled: this.isProviderEnabled.bind(this), }; const authProviders = this.options.config.authc.providers; @@ -206,6 +208,8 @@ export class Authenticator { ? (this.options.config.authc as Record)[providerType] : undefined; + this.logger.debug(`Enabling "${providerType}" authentication provider.`); + return [ providerType, instantiateProvider( @@ -216,6 +220,17 @@ export class Authenticator { ] as [string, BaseAuthenticationProvider]; }) ); + + // For the BWC reasons we always include HTTP authentication provider unless it's explicitly disabled. + if (this.options.config.authc.http.enabled) { + this.setupHTTPAuthenticationProvider( + Object.freeze({ + ...providerCommonOptions, + logger: options.loggers.get(HTTPAuthenticationProvider.type), + }) + ); + } + this.serverBasePath = this.options.basePath.serverBasePath || '/'; this.idleTimeout = this.options.config.session.idleTimeout; @@ -385,6 +400,41 @@ export class Authenticator { return null; } + /** + * Checks whether specified provider type is currently enabled. + * @param providerType Type of the provider (`basic`, `saml`, `pki` etc.). + */ + isProviderEnabled(providerType: string) { + return this.providers.has(providerType); + } + + /** + * Initializes HTTP Authentication provider and appends it to the end of the list of enabled + * authentication providers. + * @param options Common provider options. + */ + private setupHTTPAuthenticationProvider(options: AuthenticationProviderOptions) { + const supportedSchemes = new Set( + this.options.config.authc.http.schemes.map(scheme => scheme.toLowerCase()) + ); + + // If `autoSchemesEnabled` is set we should allow schemes that other providers use to + // authenticate requests with Elasticsearch. + if (this.options.config.authc.http.autoSchemesEnabled) { + for (const provider of this.providers.values()) { + const supportedScheme = provider.getHTTPAuthenticationScheme(); + if (supportedScheme) { + supportedSchemes.add(supportedScheme.toLowerCase()); + } + } + } + + this.providers.set( + HTTPAuthenticationProvider.type, + new HTTPAuthenticationProvider(options, { supportedSchemes }) + ); + } + /** * Returns provider iterator where providers are sorted in the order of priority (based on the session ownership). * @param sessionValue Current session value. diff --git a/x-pack/plugins/security/server/authentication/get_http_authentication_scheme.test.ts b/x-pack/plugins/security/server/authentication/get_http_authentication_scheme.test.ts new file mode 100644 index 0000000000000..6a63634394ec0 --- /dev/null +++ b/x-pack/plugins/security/server/authentication/get_http_authentication_scheme.test.ts @@ -0,0 +1,58 @@ +/* + * 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 { httpServerMock } from '../../../../../src/core/server/http/http_server.mocks'; + +import { getHTTPAuthenticationScheme } from './get_http_authentication_scheme'; + +describe('getHTTPAuthenticationScheme', () => { + it('returns `null` if request does not have authorization header', () => { + expect(getHTTPAuthenticationScheme(httpServerMock.createKibanaRequest())).toBeNull(); + }); + + it('returns `null` if authorization header value isn not a string', () => { + expect( + getHTTPAuthenticationScheme( + httpServerMock.createKibanaRequest({ + headers: { authorization: ['Basic xxx', 'Bearer xxx'] as any }, + }) + ) + ).toBeNull(); + }); + + it('returns `null` if authorization header value is an empty string', () => { + expect( + getHTTPAuthenticationScheme( + httpServerMock.createKibanaRequest({ headers: { authorization: '' } }) + ) + ).toBeNull(); + }); + + it('returns only scheme portion of the authorization header value in lower case', () => { + const headerValueAndSchemeMap = [ + ['Basic xxx', 'basic'], + ['Basic xxx yyy', 'basic'], + ['basic xxx', 'basic'], + ['basic', 'basic'], + // We don't trim leading whitespaces in scheme. + [' Basic xxx', ''], + ['Negotiate xxx', 'negotiate'], + ['negotiate xxx', 'negotiate'], + ['negotiate', 'negotiate'], + ['ApiKey xxx', 'apikey'], + ['apikey xxx', 'apikey'], + ['Api Key xxx', 'api'], + ]; + + for (const [authorization, scheme] of headerValueAndSchemeMap) { + expect( + getHTTPAuthenticationScheme( + httpServerMock.createKibanaRequest({ headers: { authorization } }) + ) + ).toBe(scheme); + } + }); +}); diff --git a/x-pack/plugins/security/server/authentication/get_http_authentication_scheme.ts b/x-pack/plugins/security/server/authentication/get_http_authentication_scheme.ts new file mode 100644 index 0000000000000..b9c53f34dbcab --- /dev/null +++ b/x-pack/plugins/security/server/authentication/get_http_authentication_scheme.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 { KibanaRequest } from '../../../../../src/core/server'; + +/** + * Parses request's `Authorization` HTTP header if present and extracts authentication scheme. + * https://www.iana.org/assignments/http-authschemes/http-authschemes.xhtml#authschemes + * @param request Request instance to extract authentication scheme for. + */ +export function getHTTPAuthenticationScheme(request: KibanaRequest) { + const authorizationHeaderValue = request.headers.authorization; + if (!authorizationHeaderValue || typeof authorizationHeaderValue !== 'string') { + return null; + } + + return authorizationHeaderValue.split(/\s+/)[0].toLowerCase(); +} diff --git a/x-pack/plugins/security/server/authentication/index.mock.ts b/x-pack/plugins/security/server/authentication/index.mock.ts index 77f1f9e45aea7..c634e2c80c299 100644 --- a/x-pack/plugins/security/server/authentication/index.mock.ts +++ b/x-pack/plugins/security/server/authentication/index.mock.ts @@ -9,11 +9,12 @@ import { Authentication } from '.'; export const authenticationMock = { create: (): jest.Mocked => ({ login: jest.fn(), + logout: jest.fn(), + isProviderEnabled: jest.fn(), createAPIKey: jest.fn(), getCurrentUser: jest.fn(), invalidateAPIKey: jest.fn(), isAuthenticated: jest.fn(), - logout: jest.fn(), getSessionInfo: jest.fn(), }), }; diff --git a/x-pack/plugins/security/server/authentication/index.test.ts b/x-pack/plugins/security/server/authentication/index.test.ts index 3727b1fc13dac..aaf3fc357352e 100644 --- a/x-pack/plugins/security/server/authentication/index.test.ts +++ b/x-pack/plugins/security/server/authentication/index.test.ts @@ -61,7 +61,7 @@ describe('setupAuthentication()', () => { lifespan: null, }, cookieName: 'my-sid-cookie', - authc: { providers: ['basic'] }, + authc: { providers: ['basic'], http: { enabled: true } }, }), true ); diff --git a/x-pack/plugins/security/server/authentication/index.ts b/x-pack/plugins/security/server/authentication/index.ts index 467afe0034025..189babbc6bfe6 100644 --- a/x-pack/plugins/security/server/authentication/index.ts +++ b/x-pack/plugins/security/server/authentication/index.ts @@ -169,6 +169,7 @@ export async function setupAuthentication({ login: authenticator.login.bind(authenticator), logout: authenticator.logout.bind(authenticator), getSessionInfo: authenticator.getSessionInfo.bind(authenticator), + isProviderEnabled: authenticator.isProviderEnabled.bind(authenticator), getCurrentUser, createAPIKey: (request: KibanaRequest, params: CreateAPIKeyParams) => apiKeys.create(request, params), diff --git a/x-pack/plugins/security/server/authentication/providers/base.mock.ts b/x-pack/plugins/security/server/authentication/providers/base.mock.ts index a659786f4aeff..0781608f8bc4c 100644 --- a/x-pack/plugins/security/server/authentication/providers/base.mock.ts +++ b/x-pack/plugins/security/server/authentication/providers/base.mock.ts @@ -4,9 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import sinon from 'sinon'; -import { ScopedClusterClient } from '../../../../../../src/core/server'; -import { Tokens } from '../tokens'; import { loggingServiceMock, httpServiceMock, @@ -17,34 +14,7 @@ export type MockAuthenticationProviderOptions = ReturnType< typeof mockAuthenticationProviderOptions >; -export type MockAuthenticationProviderOptionsWithJest = ReturnType< - typeof mockAuthenticationProviderOptionsWithJest ->; - -export function mockScopedClusterClient( - client: MockAuthenticationProviderOptions['client'], - requestMatcher: sinon.SinonMatcher = sinon.match.any -) { - const scopedClusterClient = sinon.createStubInstance(ScopedClusterClient); - client.asScoped.withArgs(requestMatcher).returns(scopedClusterClient); - return scopedClusterClient; -} - export function mockAuthenticationProviderOptions() { - const logger = loggingServiceMock.create().get(); - const basePath = httpServiceMock.createSetupContract().basePath; - basePath.get.mockReturnValue('/base-path'); - - return { - client: { callAsInternalUser: sinon.stub(), asScoped: sinon.stub(), close: sinon.stub() }, - logger, - basePath, - tokens: sinon.createStubInstance(Tokens), - }; -} - -// Will be renamed to mockAuthenticationProviderOptions as soon as we migrate all providers tests to Jest. -export function mockAuthenticationProviderOptionsWithJest() { const basePath = httpServiceMock.createSetupContract().basePath; basePath.get.mockReturnValue('/base-path'); diff --git a/x-pack/plugins/security/server/authentication/providers/base.ts b/x-pack/plugins/security/server/authentication/providers/base.ts index a40732768810d..300e59d9ea3da 100644 --- a/x-pack/plugins/security/server/authentication/providers/base.ts +++ b/x-pack/plugins/security/server/authentication/providers/base.ts @@ -84,6 +84,13 @@ export abstract class BaseAuthenticationProvider { */ abstract logout(request: KibanaRequest, state?: unknown): Promise; + /** + * Returns HTTP authentication scheme that provider uses within `Authorization` HTTP header that + * it attaches to all successfully authenticated requests to Elasticsearch or `null` in case + * provider doesn't attach any additional `Authorization` HTTP headers. + */ + abstract getHTTPAuthenticationScheme(): string | null; + /** * Queries Elasticsearch `_authenticate` endpoint to authenticate request and retrieve the user * information of authenticated user. diff --git a/x-pack/plugins/security/server/authentication/providers/basic.test.ts b/x-pack/plugins/security/server/authentication/providers/basic.test.ts index f9c665d6cea48..b7bdff0531fc2 100644 --- a/x-pack/plugins/security/server/authentication/providers/basic.test.ts +++ b/x-pack/plugins/security/server/authentication/providers/basic.test.ts @@ -4,18 +4,31 @@ * you may not use this file except in compliance with the Elastic License. */ -import sinon from 'sinon'; - -import { httpServerMock } from '../../../../../../src/core/server/mocks'; +import { elasticsearchServiceMock, httpServerMock } from '../../../../../../src/core/server/mocks'; import { mockAuthenticatedUser } from '../../../common/model/authenticated_user.mock'; -import { mockAuthenticationProviderOptions, mockScopedClusterClient } from './base.mock'; +import { mockAuthenticationProviderOptions } from './base.mock'; +import { IClusterClient, ScopeableRequest } from '../../../../../../src/core/server'; +import { AuthenticationResult } from '../authentication_result'; +import { DeauthenticationResult } from '../deauthentication_result'; import { BasicAuthenticationProvider } from './basic'; function generateAuthorizationHeader(username: string, password: string) { return `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`; } +function expectAuthenticateCall( + mockClusterClient: jest.Mocked, + scopeableRequest: ScopeableRequest +) { + expect(mockClusterClient.asScoped).toHaveBeenCalledTimes(1); + expect(mockClusterClient.asScoped).toHaveBeenCalledWith(scopeableRequest); + + const mockScopedClusterClient = mockClusterClient.asScoped.mock.results[0].value; + expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledTimes(1); + expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith('shield.authenticate'); +} + describe('BasicAuthenticationProvider', () => { let provider: BasicAuthenticationProvider; let mockOptions: ReturnType; @@ -30,38 +43,39 @@ describe('BasicAuthenticationProvider', () => { const credentials = { username: 'user', password: 'password' }; const authorization = generateAuthorizationHeader(credentials.username, credentials.password); - mockScopedClusterClient(mockOptions.client, sinon.match({ headers: { authorization } })) - .callAsCurrentUser.withArgs('shield.authenticate') - .resolves(user); - - const authenticationResult = await provider.login( - httpServerMock.createKibanaRequest(), - credentials + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.callAsCurrentUser.mockResolvedValue(user); + mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); + + await expect( + provider.login(httpServerMock.createKibanaRequest({ headers: {} }), credentials) + ).resolves.toEqual( + AuthenticationResult.succeeded(user, { + authHeaders: { authorization }, + state: { authorization }, + }) ); - expect(authenticationResult.succeeded()).toBe(true); - expect(authenticationResult.user).toEqual(user); - expect(authenticationResult.state).toEqual({ authorization }); - expect(authenticationResult.authHeaders).toEqual({ authorization }); + expectAuthenticateCall(mockOptions.client, { headers: { authorization } }); }); it('fails if user cannot be retrieved during login attempt', async () => { - const request = httpServerMock.createKibanaRequest(); + const request = httpServerMock.createKibanaRequest({ headers: {} }); const credentials = { username: 'user', password: 'password' }; const authorization = generateAuthorizationHeader(credentials.username, credentials.password); const authenticationError = new Error('Some error'); - mockScopedClusterClient(mockOptions.client, sinon.match({ headers: { authorization } })) - .callAsCurrentUser.withArgs('shield.authenticate') - .rejects(authenticationError); + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.callAsCurrentUser.mockRejectedValue(authenticationError); + mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); - const authenticationResult = await provider.login(request, credentials); + await expect(provider.login(request, credentials)).resolves.toEqual( + AuthenticationResult.failed(authenticationError) + ); + + expectAuthenticateCall(mockOptions.client, { headers: { authorization } }); expect(request.headers).not.toHaveProperty('authorization'); - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.user).toBeUndefined(); - expect(authenticationResult.state).toBeUndefined(); - expect(authenticationResult.error).toEqual(authenticationError); }); }); @@ -69,142 +83,113 @@ describe('BasicAuthenticationProvider', () => { it('does not redirect AJAX requests that can not be authenticated to the login page.', async () => { // Add `kbn-xsrf` header to make `can_redirect_request` think that it's AJAX request and // avoid triggering of redirect logic. - const authenticationResult = await provider.authenticate( - httpServerMock.createKibanaRequest({ headers: { 'kbn-xsrf': 'xsrf' } }), - null - ); - - expect(authenticationResult.notHandled()).toBe(true); + await expect( + provider.authenticate( + httpServerMock.createKibanaRequest({ headers: { 'kbn-xsrf': 'xsrf' } }), + null + ) + ).resolves.toEqual(AuthenticationResult.notHandled()); }); it('redirects non-AJAX requests that can not be authenticated to the login page.', async () => { - const authenticationResult = await provider.authenticate( - httpServerMock.createKibanaRequest({ path: '/s/foo/some-path # that needs to be encoded' }), - null - ); - - expect(authenticationResult.redirected()).toBe(true); - expect(authenticationResult.redirectURL).toBe( - '/base-path/login?next=%2Fbase-path%2Fs%2Ffoo%2Fsome-path%20%23%20that%20needs%20to%20be%20encoded' + await expect( + provider.authenticate( + httpServerMock.createKibanaRequest({ + path: '/s/foo/some-path # that needs to be encoded', + }), + null + ) + ).resolves.toEqual( + AuthenticationResult.redirectTo( + '/base-path/login?next=%2Fbase-path%2Fs%2Ffoo%2Fsome-path%20%23%20that%20needs%20to%20be%20encoded' + ) ); }); it('does not handle authentication if state exists, but authorization property is missing.', async () => { - const authenticationResult = await provider.authenticate( - httpServerMock.createKibanaRequest(), - {} - ); - expect(authenticationResult.notHandled()).toBe(true); + await expect( + provider.authenticate(httpServerMock.createKibanaRequest(), {}) + ).resolves.toEqual(AuthenticationResult.notHandled()); }); - it('succeeds if only `authorization` header is available.', async () => { - const request = httpServerMock.createKibanaRequest({ - headers: { authorization: generateAuthorizationHeader('user', 'password') }, - }); - const user = mockAuthenticatedUser(); - - mockScopedClusterClient(mockOptions.client, sinon.match({ headers: request.headers })) - .callAsCurrentUser.withArgs('shield.authenticate') - .resolves(user); - - const authenticationResult = await provider.authenticate(request); + it('does not handle authentication via `authorization` header.', async () => { + const authorization = generateAuthorizationHeader('user', 'password'); + const request = httpServerMock.createKibanaRequest({ headers: { authorization } }); - expect(authenticationResult.succeeded()).toBe(true); - expect(authenticationResult.user).toEqual(user); + await expect(provider.authenticate(request)).resolves.toEqual( + AuthenticationResult.notHandled() + ); - // Session state and authHeaders aren't returned for header-based auth. - expect(authenticationResult.state).toBeUndefined(); - expect(authenticationResult.authHeaders).toBeUndefined(); + expect(mockOptions.client.asScoped).not.toHaveBeenCalled(); + expect(request.headers.authorization).toBe(authorization); }); - it('succeeds if only state is available.', async () => { - const request = httpServerMock.createKibanaRequest(); - const user = mockAuthenticatedUser(); + it('does not handle authentication via `authorization` header even if state contains valid credentials.', async () => { const authorization = generateAuthorizationHeader('user', 'password'); + const request = httpServerMock.createKibanaRequest({ headers: { authorization } }); - mockScopedClusterClient(mockOptions.client, sinon.match({ headers: { authorization } })) - .callAsCurrentUser.withArgs('shield.authenticate') - .resolves(user); - - const authenticationResult = await provider.authenticate(request, { authorization }); + await expect(provider.authenticate(request, { authorization })).resolves.toEqual( + AuthenticationResult.notHandled() + ); - expect(authenticationResult.succeeded()).toBe(true); - expect(authenticationResult.user).toEqual(user); - expect(authenticationResult.state).toBeUndefined(); - expect(authenticationResult.authHeaders).toEqual({ authorization }); + expect(mockOptions.client.asScoped).not.toHaveBeenCalled(); + expect(request.headers.authorization).toBe(authorization); }); - it('does not handle `authorization` header with unsupported schema even if state contains valid credentials.', async () => { - const request = httpServerMock.createKibanaRequest({ - headers: { authorization: 'Bearer ***' }, - }); + it('succeeds if only state is available.', async () => { + const request = httpServerMock.createKibanaRequest({ headers: {} }); + const user = mockAuthenticatedUser(); const authorization = generateAuthorizationHeader('user', 'password'); - const authenticationResult = await provider.authenticate(request, { authorization }); + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.callAsCurrentUser.mockResolvedValue(user); + mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); - sinon.assert.notCalled(mockOptions.client.asScoped); - expect(request.headers.authorization).toBe('Bearer ***'); - expect(authenticationResult.notHandled()).toBe(true); + await expect(provider.authenticate(request, { authorization })).resolves.toEqual( + AuthenticationResult.succeeded(user, { authHeaders: { authorization } }) + ); + + expectAuthenticateCall(mockOptions.client, { headers: { authorization } }); }); it('fails if state contains invalid credentials.', async () => { - const request = httpServerMock.createKibanaRequest(); + const request = httpServerMock.createKibanaRequest({ headers: {} }); const authorization = generateAuthorizationHeader('user', 'password'); const authenticationError = new Error('Forbidden'); - mockScopedClusterClient(mockOptions.client, sinon.match({ headers: { authorization } })) - .callAsCurrentUser.withArgs('shield.authenticate') - .rejects(authenticationError); - - const authenticationResult = await provider.authenticate(request, { authorization }); - - expect(request.headers).not.toHaveProperty('authorization'); - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.user).toBeUndefined(); - expect(authenticationResult.state).toBeUndefined(); - expect(authenticationResult.authHeaders).toBeUndefined(); - expect(authenticationResult.error).toBe(authenticationError); - }); + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.callAsCurrentUser.mockRejectedValue(authenticationError); + mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); - it('authenticates only via `authorization` header even if state is available.', async () => { - const request = httpServerMock.createKibanaRequest({ - headers: { authorization: generateAuthorizationHeader('user', 'password') }, - }); - const user = mockAuthenticatedUser(); - - mockScopedClusterClient(mockOptions.client, sinon.match({ headers: request.headers })) - .callAsCurrentUser.withArgs('shield.authenticate') - .resolves(user); + await expect(provider.authenticate(request, { authorization })).resolves.toEqual( + AuthenticationResult.failed(authenticationError) + ); - const authorizationInState = generateAuthorizationHeader('user1', 'password2'); - const authenticationResult = await provider.authenticate(request, { - authorization: authorizationInState, - }); + expectAuthenticateCall(mockOptions.client, { headers: { authorization } }); - expect(authenticationResult.succeeded()).toBe(true); - expect(authenticationResult.user).toEqual(user); - expect(authenticationResult.state).toBeUndefined(); - expect(authenticationResult.authHeaders).toBeUndefined(); + expect(request.headers).not.toHaveProperty('authorization'); }); }); describe('`logout` method', () => { it('always redirects to the login page.', async () => { - const request = httpServerMock.createKibanaRequest(); - const deauthenticateResult = await provider.logout(request); - expect(deauthenticateResult.redirected()).toBe(true); - expect(deauthenticateResult.redirectURL).toBe('/base-path/login?msg=LOGGED_OUT'); + await expect(provider.logout(httpServerMock.createKibanaRequest())).resolves.toEqual( + DeauthenticationResult.redirectTo('/base-path/login?msg=LOGGED_OUT') + ); }); it('passes query string parameters to the login page.', async () => { - const request = httpServerMock.createKibanaRequest({ - query: { next: '/app/ml', msg: 'SESSION_EXPIRED' }, - }); - const deauthenticateResult = await provider.logout(request); - expect(deauthenticateResult.redirected()).toBe(true); - expect(deauthenticateResult.redirectURL).toBe( - '/base-path/login?next=%2Fapp%2Fml&msg=SESSION_EXPIRED' + await expect( + provider.logout( + httpServerMock.createKibanaRequest({ query: { next: '/app/ml', msg: 'SESSION_EXPIRED' } }) + ) + ).resolves.toEqual( + DeauthenticationResult.redirectTo('/base-path/login?next=%2Fapp%2Fml&msg=SESSION_EXPIRED') ); }); }); + + it('`getHTTPAuthenticationScheme` method', () => { + expect(provider.getHTTPAuthenticationScheme()).toBe('basic'); + }); }); diff --git a/x-pack/plugins/security/server/authentication/providers/basic.ts b/x-pack/plugins/security/server/authentication/providers/basic.ts index a8e4e8705a7a8..ad46aff8afa51 100644 --- a/x-pack/plugins/security/server/authentication/providers/basic.ts +++ b/x-pack/plugins/security/server/authentication/providers/basic.ts @@ -8,6 +8,7 @@ import { KibanaRequest } from '../../../../../../src/core/server'; import { canRedirectRequest } from '../can_redirect_request'; import { AuthenticationResult } from '../authentication_result'; import { DeauthenticationResult } from '../deauthentication_result'; +import { getHTTPAuthenticationScheme } from '../get_http_authentication_scheme'; import { BaseAuthenticationProvider } from './base'; /** @@ -75,29 +76,25 @@ export class BasicAuthenticationProvider extends BaseAuthenticationProvider { public async authenticate(request: KibanaRequest, state?: ProviderState | null) { this.logger.debug(`Trying to authenticate user request to ${request.url.path}.`); - // try header-based auth - const { - authenticationResult: headerAuthResult, - headerNotRecognized, - } = await this.authenticateViaHeader(request); - if (headerNotRecognized) { - return headerAuthResult; + if (getHTTPAuthenticationScheme(request) != null) { + this.logger.debug('Cannot authenticate requests with `Authorization` header.'); + return AuthenticationResult.notHandled(); } - let authenticationResult = headerAuthResult; - if (authenticationResult.notHandled() && state) { - authenticationResult = await this.authenticateViaState(request, state); - } else if (authenticationResult.notHandled() && canRedirectRequest(request)) { - // If we couldn't handle authentication let's redirect user to the login page. - const nextURL = encodeURIComponent( - `${this.options.basePath.get(request)}${request.url.path}` - ); - authenticationResult = AuthenticationResult.redirectTo( - `${this.options.basePath.get(request)}/login?next=${nextURL}` + if (state) { + return await this.authenticateViaState(request, state); + } + + // If state isn't present let's redirect user to the login page. + if (canRedirectRequest(request)) { + this.logger.debug('Redirecting request to Login page.'); + const basePath = this.options.basePath.get(request); + return AuthenticationResult.redirectTo( + `${basePath}/login?next=${encodeURIComponent(`${basePath}${request.url.path}`)}` ); } - return authenticationResult; + return AuthenticationResult.notHandled(); } /** @@ -114,37 +111,11 @@ export class BasicAuthenticationProvider extends BaseAuthenticationProvider { } /** - * Validates whether request contains `Basic ***` Authorization header and just passes it - * forward to Elasticsearch backend. - * @param request Request instance. + * Returns HTTP authentication scheme (`Bearer`) that's used within `Authorization` HTTP header + * that provider attaches to all successfully authenticated requests to Elasticsearch. */ - private async authenticateViaHeader(request: KibanaRequest) { - this.logger.debug('Trying to authenticate via header.'); - - const authorization = request.headers.authorization; - if (!authorization || typeof authorization !== 'string') { - this.logger.debug('Authorization header is not presented.'); - return { authenticationResult: AuthenticationResult.notHandled() }; - } - - const authenticationSchema = authorization.split(/\s+/)[0]; - if (authenticationSchema.toLowerCase() !== 'basic') { - this.logger.debug(`Unsupported authentication schema: ${authenticationSchema}`); - return { - authenticationResult: AuthenticationResult.notHandled(), - headerNotRecognized: true, - }; - } - - try { - const user = await this.getUser(request); - - this.logger.debug('Request has been authenticated via header.'); - return { authenticationResult: AuthenticationResult.succeeded(user) }; - } catch (err) { - this.logger.debug(`Failed to authenticate request via header: ${err.message}`); - return { authenticationResult: AuthenticationResult.failed(err) }; - } + public getHTTPAuthenticationScheme() { + return 'basic'; } /** diff --git a/x-pack/plugins/security/server/authentication/providers/http.test.ts b/x-pack/plugins/security/server/authentication/providers/http.test.ts new file mode 100644 index 0000000000000..65fbd7cd9f4ad --- /dev/null +++ b/x-pack/plugins/security/server/authentication/providers/http.test.ts @@ -0,0 +1,198 @@ +/* + * 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 { elasticsearchServiceMock, httpServerMock } from '../../../../../../src/core/server/mocks'; +import { mockAuthenticatedUser } from '../../../common/model/authenticated_user.mock'; +import { MockAuthenticationProviderOptions, mockAuthenticationProviderOptions } from './base.mock'; + +import { + ElasticsearchErrorHelpers, + IClusterClient, + ScopeableRequest, +} from '../../../../../../src/core/server'; +import { AuthenticationResult } from '../authentication_result'; +import { DeauthenticationResult } from '../deauthentication_result'; +import { HTTPAuthenticationProvider } from './http'; + +function expectAuthenticateCall( + mockClusterClient: jest.Mocked, + scopeableRequest: ScopeableRequest +) { + expect(mockClusterClient.asScoped).toHaveBeenCalledTimes(1); + expect(mockClusterClient.asScoped).toHaveBeenCalledWith(scopeableRequest); + + const mockScopedClusterClient = mockClusterClient.asScoped.mock.results[0].value; + expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledTimes(1); + expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith('shield.authenticate'); +} + +describe('HTTPAuthenticationProvider', () => { + let mockOptions: MockAuthenticationProviderOptions; + beforeEach(() => { + mockOptions = mockAuthenticationProviderOptions(); + }); + + it('throws if `schemes` are not specified', () => { + const providerOptions = mockAuthenticationProviderOptions(); + + expect(() => new HTTPAuthenticationProvider(providerOptions, undefined as any)).toThrowError( + 'Supported schemes should be specified' + ); + expect(() => new HTTPAuthenticationProvider(providerOptions, {} as any)).toThrowError( + 'Supported schemes should be specified' + ); + expect( + () => new HTTPAuthenticationProvider(providerOptions, { supportedSchemes: new Set() }) + ).toThrowError('Supported schemes should be specified'); + }); + + describe('`login` method', () => { + it('does not handle login', async () => { + const provider = new HTTPAuthenticationProvider(mockOptions, { + supportedSchemes: new Set(['apikey']), + }); + + await expect(provider.login()).resolves.toEqual(AuthenticationResult.notHandled()); + + expect(mockOptions.client.asScoped).not.toHaveBeenCalled(); + expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled(); + }); + }); + + describe('`authenticate` method', () => { + it('does not handle authentication for requests without `authorization` header.', async () => { + const provider = new HTTPAuthenticationProvider(mockOptions, { + supportedSchemes: new Set(['apikey']), + }); + + await expect(provider.authenticate(httpServerMock.createKibanaRequest())).resolves.toEqual( + AuthenticationResult.notHandled() + ); + + expect(mockOptions.client.asScoped).not.toHaveBeenCalled(); + expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled(); + }); + + it('does not handle authentication for requests with empty scheme in `authorization` header.', async () => { + const provider = new HTTPAuthenticationProvider(mockOptions, { + supportedSchemes: new Set(['apikey']), + }); + + await expect( + provider.authenticate( + httpServerMock.createKibanaRequest({ headers: { authorization: '' } }) + ) + ).resolves.toEqual(AuthenticationResult.notHandled()); + + expect(mockOptions.client.asScoped).not.toHaveBeenCalled(); + expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled(); + }); + + it('does not handle authentication via `authorization` header if scheme is not supported.', async () => { + for (const { schemes, header } of [ + { schemes: ['basic'], header: 'Bearer xxx' }, + { schemes: ['bearer'], header: 'Basic xxx' }, + { schemes: ['basic', 'apikey'], header: 'Bearer xxx' }, + { schemes: ['basic', 'bearer'], header: 'ApiKey xxx' }, + ]) { + const request = httpServerMock.createKibanaRequest({ headers: { authorization: header } }); + + const provider = new HTTPAuthenticationProvider(mockOptions, { + supportedSchemes: new Set(schemes), + }); + + await expect(provider.authenticate(request)).resolves.toEqual( + AuthenticationResult.notHandled() + ); + + expect(request.headers.authorization).toBe(header); + } + + expect(mockOptions.client.asScoped).not.toHaveBeenCalled(); + expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled(); + }); + + it('succeeds if authentication via `authorization` header with supported scheme succeeds.', async () => { + const user = mockAuthenticatedUser(); + for (const { schemes, header } of [ + { schemes: ['basic'], header: 'Basic xxx' }, + { schemes: ['bearer'], header: 'Bearer xxx' }, + { schemes: ['basic', 'apikey'], header: 'ApiKey xxx' }, + { schemes: ['some-weird-scheme'], header: 'some-weird-scheme xxx' }, + { schemes: ['apikey', 'bearer'], header: 'Bearer xxx' }, + ]) { + const request = httpServerMock.createKibanaRequest({ headers: { authorization: header } }); + + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.callAsCurrentUser.mockResolvedValue(user); + mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); + mockOptions.client.asScoped.mockClear(); + + const provider = new HTTPAuthenticationProvider(mockOptions, { + supportedSchemes: new Set(schemes), + }); + + await expect(provider.authenticate(request)).resolves.toEqual( + AuthenticationResult.succeeded({ ...user, authentication_provider: 'http' }) + ); + + expectAuthenticateCall(mockOptions.client, { headers: { authorization: header } }); + + expect(request.headers.authorization).toBe(header); + } + }); + + it('fails if authentication via `authorization` header with supported scheme fails.', async () => { + const failureReason = ElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error()); + for (const { schemes, header } of [ + { schemes: ['basic'], header: 'Basic xxx' }, + { schemes: ['bearer'], header: 'Bearer xxx' }, + { schemes: ['basic', 'apikey'], header: 'ApiKey xxx' }, + { schemes: ['some-weird-scheme'], header: 'some-weird-scheme xxx' }, + { schemes: ['apikey', 'bearer'], header: 'Bearer xxx' }, + ]) { + const request = httpServerMock.createKibanaRequest({ headers: { authorization: header } }); + + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.callAsCurrentUser.mockRejectedValue(failureReason); + mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); + mockOptions.client.asScoped.mockClear(); + + const provider = new HTTPAuthenticationProvider(mockOptions, { + supportedSchemes: new Set(schemes), + }); + + await expect(provider.authenticate(request)).resolves.toEqual( + AuthenticationResult.failed(failureReason) + ); + + expectAuthenticateCall(mockOptions.client, { headers: { authorization: header } }); + + expect(request.headers.authorization).toBe(header); + } + }); + }); + + describe('`logout` method', () => { + it('does not handle logout', async () => { + const provider = new HTTPAuthenticationProvider(mockOptions, { + supportedSchemes: new Set(['apikey']), + }); + + await expect(provider.logout()).resolves.toEqual(DeauthenticationResult.notHandled()); + + expect(mockOptions.client.asScoped).not.toHaveBeenCalled(); + expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled(); + }); + }); + + it('`getHTTPAuthenticationScheme` method', () => { + const provider = new HTTPAuthenticationProvider(mockOptions, { + supportedSchemes: new Set(['apikey']), + }); + expect(provider.getHTTPAuthenticationScheme()).toBeNull(); + }); +}); diff --git a/x-pack/plugins/security/server/authentication/providers/http.ts b/x-pack/plugins/security/server/authentication/providers/http.ts new file mode 100644 index 0000000000000..57163bf8145b8 --- /dev/null +++ b/x-pack/plugins/security/server/authentication/providers/http.ts @@ -0,0 +1,99 @@ +/* + * 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 { KibanaRequest } from '../../../../../../src/core/server'; +import { AuthenticationResult } from '../authentication_result'; +import { DeauthenticationResult } from '../deauthentication_result'; +import { getHTTPAuthenticationScheme } from '../get_http_authentication_scheme'; +import { AuthenticationProviderOptions, BaseAuthenticationProvider } from './base'; + +interface HTTPAuthenticationProviderOptions { + supportedSchemes: Set; +} + +/** + * Provider that supports request authentication via forwarding `Authorization` HTTP header to Elasticsearch. + */ +export class HTTPAuthenticationProvider extends BaseAuthenticationProvider { + /** + * Type of the provider. + */ + static readonly type = 'http'; + + /** + * Set of the schemes (`Basic`, `Bearer` etc.) that provider expects to see within `Authorization` + * HTTP header while authenticating request. + */ + private readonly supportedSchemes: Set; + + constructor( + protected readonly options: Readonly, + httpOptions: Readonly + ) { + super(options); + + if ((httpOptions?.supportedSchemes?.size ?? 0) === 0) { + throw new Error('Supported schemes should be specified'); + } + this.supportedSchemes = httpOptions.supportedSchemes; + } + + /** + * NOT SUPPORTED. + */ + public async login() { + this.logger.debug('Login is not supported.'); + return AuthenticationResult.notHandled(); + } + + /** + * Performs request authentication using provided `Authorization` HTTP headers. + * @param request Request instance. + */ + public async authenticate(request: KibanaRequest) { + this.logger.debug(`Trying to authenticate user request to ${request.url.path}.`); + + const authenticationScheme = getHTTPAuthenticationScheme(request); + if (authenticationScheme == null) { + this.logger.debug('Authorization header is not presented.'); + return AuthenticationResult.notHandled(); + } + + if (!this.supportedSchemes.has(authenticationScheme)) { + this.logger.debug(`Unsupported authentication scheme: ${authenticationScheme}`); + return AuthenticationResult.notHandled(); + } + + try { + const user = await this.getUser(request); + this.logger.debug( + `Request to ${request.url.path} has been authenticated via authorization header with "${authenticationScheme}" scheme.` + ); + return AuthenticationResult.succeeded(user); + } catch (err) { + this.logger.debug( + `Failed to authenticate request to ${request.url.path} via authorization header with "${authenticationScheme}" scheme: ${err.message}` + ); + return AuthenticationResult.failed(err); + } + } + + /** + * NOT SUPPORTED. + */ + public async logout() { + this.logger.debug('Logout is not supported.'); + return DeauthenticationResult.notHandled(); + } + + /** + * Returns `null` since provider doesn't attach any additional `Authorization` HTTP headers to + * successfully authenticated requests to Elasticsearch. + */ + public getHTTPAuthenticationScheme() { + return null; + } +} diff --git a/x-pack/plugins/security/server/authentication/providers/index.ts b/x-pack/plugins/security/server/authentication/providers/index.ts index 1ec6dfb67a81d..cd8f5a70c64e3 100644 --- a/x-pack/plugins/security/server/authentication/providers/index.ts +++ b/x-pack/plugins/security/server/authentication/providers/index.ts @@ -15,3 +15,4 @@ export { SAMLAuthenticationProvider, isSAMLRequestQuery, SAMLLoginStep } from '. export { TokenAuthenticationProvider } from './token'; export { OIDCAuthenticationProvider, OIDCAuthenticationFlow } from './oidc'; export { PKIAuthenticationProvider } from './pki'; +export { HTTPAuthenticationProvider } from './http'; diff --git a/x-pack/plugins/security/server/authentication/providers/kerberos.test.ts b/x-pack/plugins/security/server/authentication/providers/kerberos.test.ts index e4b4df3feeae2..51fb961482e83 100644 --- a/x-pack/plugins/security/server/authentication/providers/kerberos.test.ts +++ b/x-pack/plugins/security/server/authentication/providers/kerberos.test.ts @@ -6,18 +6,31 @@ import Boom from 'boom'; import { errors } from 'elasticsearch'; -import sinon from 'sinon'; -import { httpServerMock } from '../../../../../../src/core/server/mocks'; +import { elasticsearchServiceMock, httpServerMock } from '../../../../../../src/core/server/mocks'; import { mockAuthenticatedUser } from '../../../common/model/authenticated_user.mock'; -import { - MockAuthenticationProviderOptions, - mockAuthenticationProviderOptions, - mockScopedClusterClient, -} from './base.mock'; +import { MockAuthenticationProviderOptions, mockAuthenticationProviderOptions } from './base.mock'; +import { + ElasticsearchErrorHelpers, + IClusterClient, + ScopeableRequest, +} from '../../../../../../src/core/server'; +import { AuthenticationResult } from '../authentication_result'; +import { DeauthenticationResult } from '../deauthentication_result'; import { KerberosAuthenticationProvider } from './kerberos'; -import { ElasticsearchErrorHelpers } from '../../../../../../src/core/server/elasticsearch'; + +function expectAuthenticateCall( + mockClusterClient: jest.Mocked, + scopeableRequest: ScopeableRequest +) { + expect(mockClusterClient.asScoped).toHaveBeenCalledTimes(1); + expect(mockClusterClient.asScoped).toHaveBeenCalledWith(scopeableRequest); + + const mockScopedClusterClient = mockClusterClient.asScoped.mock.results[0].value; + expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledTimes(1); + expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith('shield.authenticate'); +} describe('KerberosAuthenticationProvider', () => { let provider: KerberosAuthenticationProvider; @@ -28,104 +41,128 @@ describe('KerberosAuthenticationProvider', () => { }); describe('`authenticate` method', () => { - it('does not handle `authorization` header with unsupported schema even if state contains a valid token.', async () => { + it('does not handle authentication via `authorization` header with non-negotiate scheme.', async () => { + const request = httpServerMock.createKibanaRequest({ + headers: { authorization: 'Bearer some-token' }, + }); + + await expect(provider.authenticate(request)).resolves.toEqual( + AuthenticationResult.notHandled() + ); + + expect(mockOptions.client.asScoped).not.toHaveBeenCalled(); + expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled(); + expect(request.headers.authorization).toBe('Bearer some-token'); + }); + + it('does not handle authentication via `authorization` header with non-negotiate scheme even if state contains a valid token.', async () => { const request = httpServerMock.createKibanaRequest({ - headers: { authorization: 'Basic some:credentials' }, + headers: { authorization: 'Bearer some-token' }, }); const tokenPair = { accessToken: 'some-valid-token', refreshToken: 'some-valid-refresh-token', }; - const authenticationResult = await provider.authenticate(request, tokenPair); + await expect(provider.authenticate(request, tokenPair)).resolves.toEqual( + AuthenticationResult.notHandled() + ); - sinon.assert.notCalled(mockOptions.client.asScoped); - sinon.assert.notCalled(mockOptions.client.callAsInternalUser); - expect(request.headers.authorization).toBe('Basic some:credentials'); - expect(authenticationResult.notHandled()).toBe(true); + expect(mockOptions.client.asScoped).not.toHaveBeenCalled(); + expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled(); + expect(request.headers.authorization).toBe('Bearer some-token'); }); it('does not handle requests that can be authenticated without `Negotiate` header.', async () => { - const request = httpServerMock.createKibanaRequest(); - mockScopedClusterClient( - mockOptions.client, - sinon.match({ - headers: { authorization: `Negotiate ${Buffer.from('__fake__').toString('base64')}` }, - }) - ) - .callAsCurrentUser.withArgs('shield.authenticate') - .resolves({}); + const request = httpServerMock.createKibanaRequest({ headers: {} }); - const authenticationResult = await provider.authenticate(request, null); + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.callAsCurrentUser.mockResolvedValue({}); + mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); + + await expect(provider.authenticate(request, null)).resolves.toEqual( + AuthenticationResult.notHandled() + ); - expect(authenticationResult.notHandled()).toBe(true); + expectAuthenticateCall(mockOptions.client, { + headers: { authorization: `Negotiate ${Buffer.from('__fake__').toString('base64')}` }, + }); }); it('does not handle requests if backend does not support Kerberos.', async () => { - const request = httpServerMock.createKibanaRequest(); - mockScopedClusterClient( - mockOptions.client, - sinon.match({ - headers: { authorization: `Negotiate ${Buffer.from('__fake__').toString('base64')}` }, - }) - ) - .callAsCurrentUser.withArgs('shield.authenticate') - .rejects(ElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error())); + const request = httpServerMock.createKibanaRequest({ headers: {} }); + + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.callAsCurrentUser.mockRejectedValue( + ElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error()) + ); + mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); + + await expect(provider.authenticate(request, null)).resolves.toEqual( + AuthenticationResult.notHandled() + ); - const authenticationResult = await provider.authenticate(request, null); - expect(authenticationResult.notHandled()).toBe(true); + expectAuthenticateCall(mockOptions.client, { + headers: { authorization: `Negotiate ${Buffer.from('__fake__').toString('base64')}` }, + }); }); it('fails if state is present, but backend does not support Kerberos.', async () => { const request = httpServerMock.createKibanaRequest(); const tokenPair = { accessToken: 'token', refreshToken: 'refresh-token' }; - mockScopedClusterClient(mockOptions.client) - .callAsCurrentUser.withArgs('shield.authenticate') - .rejects(ElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error())); - mockOptions.tokens.refresh.withArgs(tokenPair.refreshToken).resolves(null); + const failureReason = ElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error()); + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.callAsCurrentUser.mockRejectedValue(failureReason); + mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); + mockOptions.tokens.refresh.mockResolvedValue(null); + + await expect(provider.authenticate(request, tokenPair)).resolves.toEqual( + AuthenticationResult.failed(failureReason) + ); - const authenticationResult = await provider.authenticate(request, tokenPair); - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.error).toHaveProperty('output.statusCode', 401); - expect(authenticationResult.authResponseHeaders).toBeUndefined(); + expect(mockOptions.tokens.refresh).toHaveBeenCalledTimes(1); + expect(mockOptions.tokens.refresh).toHaveBeenCalledWith(tokenPair.refreshToken); }); it('fails with `Negotiate` challenge if backend supports Kerberos.', async () => { - const request = httpServerMock.createKibanaRequest(); - mockScopedClusterClient( - mockOptions.client, - sinon.match({ - headers: { authorization: `Negotiate ${Buffer.from('__fake__').toString('base64')}` }, + const request = httpServerMock.createKibanaRequest({ headers: {} }); + + const failureReason = ElasticsearchErrorHelpers.decorateNotAuthorizedError( + new (errors.AuthenticationException as any)('Unauthorized', { + body: { error: { header: { 'WWW-Authenticate': 'Negotiate' } } }, }) - ) - .callAsCurrentUser.withArgs('shield.authenticate') - .rejects( - ElasticsearchErrorHelpers.decorateNotAuthorizedError( - new (errors.AuthenticationException as any)('Unauthorized', { - body: { error: { header: { 'WWW-Authenticate': 'Negotiate' } } }, - }) - ) - ); - - const authenticationResult = await provider.authenticate(request, null); - - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.error).toHaveProperty('output.statusCode', 401); - expect(authenticationResult.authResponseHeaders).toEqual({ 'WWW-Authenticate': 'Negotiate' }); + ); + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.callAsCurrentUser.mockRejectedValue(failureReason); + mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); + + await expect(provider.authenticate(request, null)).resolves.toEqual( + AuthenticationResult.failed(failureReason, { + authResponseHeaders: { 'WWW-Authenticate': 'Negotiate' }, + }) + ); + + expectAuthenticateCall(mockOptions.client, { + headers: { authorization: `Negotiate ${Buffer.from('__fake__').toString('base64')}` }, + }); }); it('fails if request authentication is failed with non-401 error.', async () => { - const request = httpServerMock.createKibanaRequest(); - mockScopedClusterClient(mockOptions.client) - .callAsCurrentUser.withArgs('shield.authenticate') - .rejects(new errors.ServiceUnavailable()); + const request = httpServerMock.createKibanaRequest({ headers: {} }); + + const failureReason = new errors.ServiceUnavailable(); + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.callAsCurrentUser.mockRejectedValue(failureReason); + mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); - const authenticationResult = await provider.authenticate(request, null); + await expect(provider.authenticate(request, null)).resolves.toEqual( + AuthenticationResult.failed(failureReason) + ); - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.error).toHaveProperty('status', 503); - expect(authenticationResult.authResponseHeaders).toBeUndefined(); + expectAuthenticateCall(mockOptions.client, { + headers: { authorization: `Negotiate ${Buffer.from('__fake__').toString('base64')}` }, + }); }); it('gets a token pair in exchange to SPNEGO one and stores it in the state.', async () => { @@ -134,34 +171,33 @@ describe('KerberosAuthenticationProvider', () => { headers: { authorization: 'negotiate spnego' }, }); - mockScopedClusterClient( - mockOptions.client, - sinon.match({ headers: { authorization: 'Bearer some-token' } }) - ) - .callAsCurrentUser.withArgs('shield.authenticate') - .resolves(user); + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.callAsCurrentUser.mockResolvedValue(user); + mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); + mockOptions.client.callAsInternalUser.mockResolvedValue({ + access_token: 'some-token', + refresh_token: 'some-refresh-token', + }); - mockOptions.client.callAsInternalUser - .withArgs('shield.getAccessToken') - .resolves({ access_token: 'some-token', refresh_token: 'some-refresh-token' }); + await expect(provider.authenticate(request)).resolves.toEqual( + AuthenticationResult.succeeded( + { ...user, authentication_provider: 'kerberos' }, + { + authHeaders: { authorization: 'Bearer some-token' }, + state: { accessToken: 'some-token', refreshToken: 'some-refresh-token' }, + } + ) + ); - const authenticationResult = await provider.authenticate(request); + expectAuthenticateCall(mockOptions.client, { + headers: { authorization: 'Bearer some-token' }, + }); - sinon.assert.calledWithExactly( - mockOptions.client.callAsInternalUser, - 'shield.getAccessToken', - { body: { grant_type: '_kerberos', kerberos_ticket: 'spnego' } } - ); + expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.getAccessToken', { + body: { grant_type: '_kerberos', kerberos_ticket: 'spnego' }, + }); expect(request.headers.authorization).toBe('negotiate spnego'); - expect(authenticationResult.succeeded()).toBe(true); - expect(authenticationResult.user).toEqual({ ...user, authentication_provider: 'kerberos' }); - expect(authenticationResult.authHeaders).toEqual({ authorization: 'Bearer some-token' }); - expect(authenticationResult.authResponseHeaders).toBeUndefined(); - expect(authenticationResult.state).toEqual({ - accessToken: 'some-token', - refreshToken: 'some-refresh-token', - }); }); it('requests auth response header if token pair is complemented with Kerberos response token.', async () => { @@ -170,38 +206,35 @@ describe('KerberosAuthenticationProvider', () => { headers: { authorization: 'negotiate spnego' }, }); - mockScopedClusterClient( - mockOptions.client, - sinon.match({ headers: { authorization: 'Bearer some-token' } }) - ) - .callAsCurrentUser.withArgs('shield.authenticate') - .resolves(user); - - mockOptions.client.callAsInternalUser.withArgs('shield.getAccessToken').resolves({ + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.callAsCurrentUser.mockResolvedValue(user); + mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); + mockOptions.client.callAsInternalUser.mockResolvedValue({ access_token: 'some-token', refresh_token: 'some-refresh-token', kerberos_authentication_response_token: 'response-token', }); - const authenticationResult = await provider.authenticate(request); - - sinon.assert.calledWithExactly( - mockOptions.client.callAsInternalUser, - 'shield.getAccessToken', - { body: { grant_type: '_kerberos', kerberos_ticket: 'spnego' } } + await expect(provider.authenticate(request)).resolves.toEqual( + AuthenticationResult.succeeded( + { ...user, authentication_provider: 'kerberos' }, + { + authHeaders: { authorization: 'Bearer some-token' }, + authResponseHeaders: { 'WWW-Authenticate': 'Negotiate response-token' }, + state: { accessToken: 'some-token', refreshToken: 'some-refresh-token' }, + } + ) ); - expect(request.headers.authorization).toBe('negotiate spnego'); - expect(authenticationResult.succeeded()).toBe(true); - expect(authenticationResult.user).toEqual({ ...user, authentication_provider: 'kerberos' }); - expect(authenticationResult.authHeaders).toEqual({ authorization: 'Bearer some-token' }); - expect(authenticationResult.authResponseHeaders).toEqual({ - 'WWW-Authenticate': 'Negotiate response-token', + expectAuthenticateCall(mockOptions.client, { + headers: { authorization: 'Bearer some-token' }, }); - expect(authenticationResult.state).toEqual({ - accessToken: 'some-token', - refreshToken: 'some-refresh-token', + + expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.getAccessToken', { + body: { grant_type: '_kerberos', kerberos_ticket: 'spnego' }, }); + + expect(request.headers.authorization).toBe('negotiate spnego'); }); it('fails with `Negotiate response-token` if cannot complete context with a response token.', async () => { @@ -214,24 +247,19 @@ describe('KerberosAuthenticationProvider', () => { body: { error: { header: { 'WWW-Authenticate': 'Negotiate response-token' } } }, }) ); - mockOptions.client.callAsInternalUser - .withArgs('shield.getAccessToken') - .rejects(failureReason); + mockOptions.client.callAsInternalUser.mockRejectedValue(failureReason); - const authenticationResult = await provider.authenticate(request); - - sinon.assert.calledWithExactly( - mockOptions.client.callAsInternalUser, - 'shield.getAccessToken', - { body: { grant_type: '_kerberos', kerberos_ticket: 'spnego' } } + await expect(provider.authenticate(request)).resolves.toEqual( + AuthenticationResult.failed(Boom.unauthorized(), { + authResponseHeaders: { 'WWW-Authenticate': 'Negotiate response-token' }, + }) ); - expect(request.headers.authorization).toBe('negotiate spnego'); - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.error).toEqual(Boom.unauthorized()); - expect(authenticationResult.authResponseHeaders).toEqual({ - 'WWW-Authenticate': 'Negotiate response-token', + expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.getAccessToken', { + body: { grant_type: '_kerberos', kerberos_ticket: 'spnego' }, }); + + expect(request.headers.authorization).toBe('negotiate spnego'); }); it('fails with `Negotiate` if cannot create context using provided SPNEGO token.', async () => { @@ -244,24 +272,19 @@ describe('KerberosAuthenticationProvider', () => { body: { error: { header: { 'WWW-Authenticate': 'Negotiate' } } }, }) ); - mockOptions.client.callAsInternalUser - .withArgs('shield.getAccessToken') - .rejects(failureReason); - - const authenticationResult = await provider.authenticate(request); + mockOptions.client.callAsInternalUser.mockRejectedValue(failureReason); - sinon.assert.calledWithExactly( - mockOptions.client.callAsInternalUser, - 'shield.getAccessToken', - { body: { grant_type: '_kerberos', kerberos_ticket: 'spnego' } } + await expect(provider.authenticate(request)).resolves.toEqual( + AuthenticationResult.failed(Boom.unauthorized(), { + authResponseHeaders: { 'WWW-Authenticate': 'Negotiate' }, + }) ); - expect(request.headers.authorization).toBe('negotiate spnego'); - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.error).toEqual(Boom.unauthorized()); - expect(authenticationResult.authResponseHeaders).toEqual({ - 'WWW-Authenticate': 'Negotiate', + expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.getAccessToken', { + body: { grant_type: '_kerberos', kerberos_ticket: 'spnego' }, }); + + expect(request.headers.authorization).toBe('negotiate spnego'); }); it('fails if could not retrieve an access token in exchange to SPNEGO one.', async () => { @@ -270,22 +293,17 @@ describe('KerberosAuthenticationProvider', () => { }); const failureReason = ElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error()); - mockOptions.client.callAsInternalUser - .withArgs('shield.getAccessToken') - .rejects(failureReason); - - const authenticationResult = await provider.authenticate(request); + mockOptions.client.callAsInternalUser.mockRejectedValue(failureReason); - sinon.assert.calledWithExactly( - mockOptions.client.callAsInternalUser, - 'shield.getAccessToken', - { body: { grant_type: '_kerberos', kerberos_ticket: 'spnego' } } + await expect(provider.authenticate(request)).resolves.toEqual( + AuthenticationResult.failed(failureReason) ); + expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.getAccessToken', { + body: { grant_type: '_kerberos', kerberos_ticket: 'spnego' }, + }); + expect(request.headers.authorization).toBe('negotiate spnego'); - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.error).toBe(failureReason); - expect(authenticationResult.authResponseHeaders).toBeUndefined(); }); it('fails if could not retrieve user using the new access token.', async () => { @@ -294,51 +312,52 @@ describe('KerberosAuthenticationProvider', () => { }); const failureReason = ElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error()); - mockScopedClusterClient( - mockOptions.client, - sinon.match({ headers: { authorization: 'Bearer some-token' } }) - ) - .callAsCurrentUser.withArgs('shield.authenticate') - .rejects(failureReason); - - mockOptions.client.callAsInternalUser - .withArgs('shield.getAccessToken') - .resolves({ access_token: 'some-token', refresh_token: 'some-refresh-token' }); - - const authenticationResult = await provider.authenticate(request); - - sinon.assert.calledWithExactly( - mockOptions.client.callAsInternalUser, - 'shield.getAccessToken', - { body: { grant_type: '_kerberos', kerberos_ticket: 'spnego' } } + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.callAsCurrentUser.mockRejectedValue(failureReason); + mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); + mockOptions.client.callAsInternalUser.mockResolvedValue({ + access_token: 'some-token', + refresh_token: 'some-refresh-token', + }); + + await expect(provider.authenticate(request)).resolves.toEqual( + AuthenticationResult.failed(failureReason) ); + expectAuthenticateCall(mockOptions.client, { + headers: { authorization: 'Bearer some-token' }, + }); + + expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.getAccessToken', { + body: { grant_type: '_kerberos', kerberos_ticket: 'spnego' }, + }); + expect(request.headers.authorization).toBe('negotiate spnego'); - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.error).toBe(failureReason); - expect(authenticationResult.authResponseHeaders).toBeUndefined(); }); it('succeeds if state contains a valid token.', async () => { const user = mockAuthenticatedUser(); - const request = httpServerMock.createKibanaRequest(); + const request = httpServerMock.createKibanaRequest({ headers: {} }); const tokenPair = { accessToken: 'some-valid-token', refreshToken: 'some-valid-refresh-token', }; const authorization = `Bearer ${tokenPair.accessToken}`; - mockScopedClusterClient(mockOptions.client, sinon.match({ headers: { authorization } })) - .callAsCurrentUser.withArgs('shield.authenticate') - .resolves(user); + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.callAsCurrentUser.mockResolvedValue(user); + mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); + + await expect(provider.authenticate(request, tokenPair)).resolves.toEqual( + AuthenticationResult.succeeded( + { ...user, authentication_provider: 'kerberos' }, + { authHeaders: { authorization } } + ) + ); - const authenticationResult = await provider.authenticate(request, tokenPair); + expectAuthenticateCall(mockOptions.client, { headers: { authorization } }); expect(request.headers).not.toHaveProperty('authorization'); - expect(authenticationResult.succeeded()).toBe(true); - expect(authenticationResult.authHeaders).toEqual({ authorization }); - expect(authenticationResult.user).toEqual({ ...user, authentication_provider: 'kerberos' }); - expect(authenticationResult.state).toBeUndefined(); }); it('succeeds with valid session even if requiring a token refresh', async () => { @@ -346,149 +365,94 @@ describe('KerberosAuthenticationProvider', () => { const request = httpServerMock.createKibanaRequest(); const tokenPair = { accessToken: 'foo', refreshToken: 'bar' }; - mockScopedClusterClient( - mockOptions.client, - sinon.match({ headers: { authorization: `Bearer ${tokenPair.accessToken}` } }) - ) - .callAsCurrentUser.withArgs('shield.authenticate') - .rejects(ElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error())); - - mockOptions.tokens.refresh - .withArgs(tokenPair.refreshToken) - .resolves({ accessToken: 'newfoo', refreshToken: 'newbar' }); + mockOptions.client.asScoped.mockImplementation(scopeableRequest => { + if (scopeableRequest?.headers.authorization === `Bearer ${tokenPair.accessToken}`) { + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.callAsCurrentUser.mockRejectedValue( + ElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error()) + ); + return mockScopedClusterClient; + } + + if (scopeableRequest?.headers.authorization === 'Bearer newfoo') { + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.callAsCurrentUser.mockResolvedValue(user); + return mockScopedClusterClient; + } + + throw new Error('Unexpected call'); + }); - mockScopedClusterClient( - mockOptions.client, - sinon.match({ headers: { authorization: 'Bearer newfoo' } }) - ) - .callAsCurrentUser.withArgs('shield.authenticate') - .resolves(user); + mockOptions.tokens.refresh.mockResolvedValue({ + accessToken: 'newfoo', + refreshToken: 'newbar', + }); - const authenticationResult = await provider.authenticate(request, tokenPair); + await expect(provider.authenticate(request, tokenPair)).resolves.toEqual( + AuthenticationResult.succeeded( + { ...user, authentication_provider: 'kerberos' }, + { + authHeaders: { authorization: 'Bearer newfoo' }, + state: { accessToken: 'newfoo', refreshToken: 'newbar' }, + } + ) + ); - sinon.assert.calledOnce(mockOptions.tokens.refresh); + expect(mockOptions.tokens.refresh).toHaveBeenCalledTimes(1); + expect(mockOptions.tokens.refresh).toHaveBeenCalledWith(tokenPair.refreshToken); - expect(authenticationResult.succeeded()).toBe(true); - expect(authenticationResult.authHeaders).toEqual({ authorization: 'Bearer newfoo' }); - expect(authenticationResult.user).toEqual({ ...user, authentication_provider: 'kerberos' }); - expect(authenticationResult.state).toEqual({ accessToken: 'newfoo', refreshToken: 'newbar' }); expect(request.headers).not.toHaveProperty('authorization'); }); it('fails if token from the state is rejected because of unknown reason.', async () => { - const request = httpServerMock.createKibanaRequest(); + const request = httpServerMock.createKibanaRequest({ headers: {} }); const tokenPair = { accessToken: 'some-valid-token', refreshToken: 'some-valid-refresh-token', }; const failureReason = new errors.InternalServerError('Token is not valid!'); - const scopedClusterClient = mockScopedClusterClient( - mockOptions.client, - sinon.match({ headers: { authorization: `Bearer ${tokenPair.accessToken}` } }) + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.callAsCurrentUser.mockRejectedValue(failureReason); + mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); + + await expect(provider.authenticate(request, tokenPair)).resolves.toEqual( + AuthenticationResult.failed(failureReason) ); - scopedClusterClient.callAsCurrentUser.withArgs('shield.authenticate').rejects(failureReason); - const authenticationResult = await provider.authenticate(request, tokenPair); + expectAuthenticateCall(mockOptions.client, { + headers: { authorization: `Bearer ${tokenPair.accessToken}` }, + }); + + expect(mockScopedClusterClient.callAsInternalUser).not.toHaveBeenCalled(); + expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled(); expect(request.headers).not.toHaveProperty('authorization'); - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.error).toBe(failureReason); - sinon.assert.neverCalledWith(scopedClusterClient.callAsCurrentUser, 'shield.getAccessToken'); }); it('fails with `Negotiate` challenge if both access and refresh tokens from the state are expired and backend supports Kerberos.', async () => { const request = httpServerMock.createKibanaRequest(); const tokenPair = { accessToken: 'expired-token', refreshToken: 'some-valid-refresh-token' }; - mockScopedClusterClient(mockOptions.client) - .callAsCurrentUser.withArgs('shield.authenticate') - .rejects( - ElasticsearchErrorHelpers.decorateNotAuthorizedError( - new (errors.AuthenticationException as any)('Unauthorized', { - body: { error: { header: { 'WWW-Authenticate': 'Negotiate' } } }, - }) - ) - ); - mockOptions.tokens.refresh.withArgs(tokenPair.refreshToken).resolves(null); - - const authenticationResult = await provider.authenticate(request, tokenPair); - - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.error).toHaveProperty('output.statusCode', 401); - expect(authenticationResult.authResponseHeaders).toEqual({ 'WWW-Authenticate': 'Negotiate' }); - }); - - it('succeeds if `authorization` contains a valid token.', async () => { - const user = mockAuthenticatedUser(); - const request = httpServerMock.createKibanaRequest({ - headers: { authorization: 'Bearer some-valid-token' }, - }); - - mockScopedClusterClient( - mockOptions.client, - sinon.match({ headers: { authorization: 'Bearer some-valid-token' } }) - ) - .callAsCurrentUser.withArgs('shield.authenticate') - .resolves(user); - - const authenticationResult = await provider.authenticate(request); - - expect(request.headers.authorization).toBe('Bearer some-valid-token'); - expect(authenticationResult.succeeded()).toBe(true); - expect(authenticationResult.authHeaders).toBeUndefined(); - expect(authenticationResult.user).toEqual({ ...user, authentication_provider: 'kerberos' }); - expect(authenticationResult.state).toBeUndefined(); - }); - - it('fails if token from `authorization` header is rejected.', async () => { - const request = httpServerMock.createKibanaRequest({ - headers: { authorization: 'Bearer some-invalid-token' }, - }); - - const failureReason = ElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error()); - mockScopedClusterClient( - mockOptions.client, - sinon.match({ headers: { authorization: 'Bearer some-invalid-token' } }) - ) - .callAsCurrentUser.withArgs('shield.authenticate') - .rejects(failureReason); - - const authenticationResult = await provider.authenticate(request); + const failureReason = ElasticsearchErrorHelpers.decorateNotAuthorizedError( + new (errors.AuthenticationException as any)('Unauthorized', { + body: { error: { header: { 'WWW-Authenticate': 'Negotiate' } } }, + }) + ); + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.callAsCurrentUser.mockRejectedValue(failureReason); + mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.error).toBe(failureReason); - }); + mockOptions.tokens.refresh.mockResolvedValue(null); - it('fails if token from `authorization` header is rejected even if state contains a valid one.', async () => { - const user = mockAuthenticatedUser(); - const request = httpServerMock.createKibanaRequest({ - headers: { authorization: 'Bearer some-invalid-token' }, - }); - const tokenPair = { - accessToken: 'some-valid-token', - refreshToken: 'some-valid-refresh-token', - }; + await expect(provider.authenticate(request, tokenPair)).resolves.toEqual( + AuthenticationResult.failed(failureReason, { + authResponseHeaders: { 'WWW-Authenticate': 'Negotiate' }, + }) + ); - const failureReason = ElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error()); - mockScopedClusterClient( - mockOptions.client, - sinon.match({ headers: { authorization: 'Bearer some-invalid-token' } }) - ) - .callAsCurrentUser.withArgs('shield.authenticate') - .rejects(failureReason); - - mockScopedClusterClient( - mockOptions.client, - sinon.match({ headers: { authorization: `Bearer ${tokenPair.accessToken}` } }) - ) - .callAsCurrentUser.withArgs('shield.authenticate') - .resolves(user); - - const authenticationResult = await provider.authenticate(request, tokenPair); - - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.error).toBe(failureReason); + expect(mockOptions.tokens.refresh).toHaveBeenCalledTimes(1); + expect(mockOptions.tokens.refresh).toHaveBeenCalledWith(tokenPair.refreshToken); }); }); @@ -496,13 +460,13 @@ describe('KerberosAuthenticationProvider', () => { it('returns `notHandled` if state is not presented.', async () => { const request = httpServerMock.createKibanaRequest(); - let deauthenticateResult = await provider.logout(request); - expect(deauthenticateResult.notHandled()).toBe(true); + await expect(provider.logout(request)).resolves.toEqual(DeauthenticationResult.notHandled()); - deauthenticateResult = await provider.logout(request, null); - expect(deauthenticateResult.notHandled()).toBe(true); + await expect(provider.logout(request, null)).resolves.toEqual( + DeauthenticationResult.notHandled() + ); - sinon.assert.notCalled(mockOptions.tokens.invalidate); + expect(mockOptions.tokens.invalidate).not.toHaveBeenCalled(); }); it('fails if `tokens.invalidate` fails', async () => { @@ -510,15 +474,14 @@ describe('KerberosAuthenticationProvider', () => { const tokenPair = { accessToken: 'foo', refreshToken: 'bar' }; const failureReason = new Error('failed to delete token'); - mockOptions.tokens.invalidate.withArgs(tokenPair).rejects(failureReason); - - const authenticationResult = await provider.logout(request, tokenPair); + mockOptions.tokens.invalidate.mockRejectedValue(failureReason); - sinon.assert.calledOnce(mockOptions.tokens.invalidate); - sinon.assert.calledWithExactly(mockOptions.tokens.invalidate, tokenPair); + await expect(provider.logout(request, tokenPair)).resolves.toEqual( + DeauthenticationResult.failed(failureReason) + ); - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.error).toBe(failureReason); + expect(mockOptions.tokens.invalidate).toHaveBeenCalledTimes(1); + expect(mockOptions.tokens.invalidate).toHaveBeenCalledWith(tokenPair); }); it('redirects to `/logged_out` page if tokens are invalidated successfully.', async () => { @@ -528,15 +491,18 @@ describe('KerberosAuthenticationProvider', () => { refreshToken: 'some-valid-refresh-token', }; - mockOptions.tokens.invalidate.withArgs(tokenPair).resolves(); - - const authenticationResult = await provider.logout(request, tokenPair); + mockOptions.tokens.invalidate.mockResolvedValue(undefined); - sinon.assert.calledOnce(mockOptions.tokens.invalidate); - sinon.assert.calledWithExactly(mockOptions.tokens.invalidate, tokenPair); + await expect(provider.logout(request, tokenPair)).resolves.toEqual( + DeauthenticationResult.redirectTo('/mock-server-basepath/logged_out') + ); - expect(authenticationResult.redirected()).toBe(true); - expect(authenticationResult.redirectURL).toBe('/mock-server-basepath/logged_out'); + expect(mockOptions.tokens.invalidate).toHaveBeenCalledTimes(1); + expect(mockOptions.tokens.invalidate).toHaveBeenCalledWith(tokenPair); }); }); + + it('`getHTTPAuthenticationScheme` method', () => { + expect(provider.getHTTPAuthenticationScheme()).toBe('bearer'); + }); }); diff --git a/x-pack/plugins/security/server/authentication/providers/kerberos.ts b/x-pack/plugins/security/server/authentication/providers/kerberos.ts index b8e3b7bc23790..b6474a5e1d471 100644 --- a/x-pack/plugins/security/server/authentication/providers/kerberos.ts +++ b/x-pack/plugins/security/server/authentication/providers/kerberos.ts @@ -12,27 +12,15 @@ import { } from '../../../../../../src/core/server'; import { AuthenticationResult } from '../authentication_result'; import { DeauthenticationResult } from '../deauthentication_result'; -import { BaseAuthenticationProvider } from './base'; +import { getHTTPAuthenticationScheme } from '../get_http_authentication_scheme'; import { Tokens, TokenPair } from '../tokens'; +import { BaseAuthenticationProvider } from './base'; /** * The state supported by the provider. */ type ProviderState = TokenPair; -/** - * Parses request's `Authorization` HTTP header if present and extracts authentication scheme. - * @param request Request instance to extract authentication scheme for. - */ -function getRequestAuthenticationScheme(request: KibanaRequest) { - const authorization = request.headers.authorization; - if (!authorization || typeof authorization !== 'string') { - return ''; - } - - return authorization.split(/\s+/)[0].toLowerCase(); -} - /** * Name of the `WWW-Authenticate` we parse out of Elasticsearch responses or/and return to the * client to initiate or continue negotiation. @@ -56,24 +44,15 @@ export class KerberosAuthenticationProvider extends BaseAuthenticationProvider { public async authenticate(request: KibanaRequest, state?: ProviderState | null) { this.logger.debug(`Trying to authenticate user request to ${request.url.path}.`); - const authenticationScheme = getRequestAuthenticationScheme(request); - if ( - authenticationScheme && - authenticationScheme !== 'negotiate' && - authenticationScheme !== 'bearer' - ) { + const authenticationScheme = getHTTPAuthenticationScheme(request); + if (authenticationScheme && authenticationScheme !== 'negotiate') { this.logger.debug(`Unsupported authentication scheme: ${authenticationScheme}`); return AuthenticationResult.notHandled(); } - let authenticationResult = AuthenticationResult.notHandled(); - if (authenticationScheme) { - // We should get rid of `Bearer` scheme support as soon as Reporting doesn't need it anymore. - authenticationResult = - authenticationScheme === 'bearer' - ? await this.authenticateWithBearerScheme(request) - : await this.authenticateWithNegotiateScheme(request); - } + let authenticationResult = authenticationScheme + ? await this.authenticateWithNegotiateScheme(request) + : AuthenticationResult.notHandled(); if (state && authenticationResult.notHandled()) { authenticationResult = await this.authenticateViaState(request, state); @@ -115,6 +94,14 @@ export class KerberosAuthenticationProvider extends BaseAuthenticationProvider { return DeauthenticationResult.redirectTo(`${this.options.basePath.serverBasePath}/logged_out`); } + /** + * Returns HTTP authentication scheme (`Bearer`) that's used within `Authorization` HTTP header + * that provider attaches to all successfully authenticated requests to Elasticsearch. + */ + public getHTTPAuthenticationScheme() { + return 'bearer'; + } + /** * Tries to authenticate request with `Negotiate ***` Authorization header by passing it to the Elasticsearch backend to * get an access token in exchange. @@ -201,26 +188,6 @@ export class KerberosAuthenticationProvider extends BaseAuthenticationProvider { } } - /** - * Tries to authenticate request with `Bearer ***` Authorization header by passing it to the Elasticsearch backend. - * @param request Request instance. - */ - private async authenticateWithBearerScheme(request: KibanaRequest) { - this.logger.debug('Trying to authenticate request using "Bearer" authentication scheme.'); - - try { - const user = await this.getUser(request); - - this.logger.debug('Request has been authenticated using "Bearer" authentication scheme.'); - return AuthenticationResult.succeeded(user); - } catch (err) { - this.logger.debug( - `Failed to authenticate request using "Bearer" authentication scheme: ${err.message}` - ); - return AuthenticationResult.failed(err); - } - } - /** * Tries to extract access token from state and adds it to the request before it's * forwarded to Elasticsearch backend. diff --git a/x-pack/plugins/security/server/authentication/providers/oidc.test.ts b/x-pack/plugins/security/server/authentication/providers/oidc.test.ts index dae3774955859..51a25825bf985 100644 --- a/x-pack/plugins/security/server/authentication/providers/oidc.test.ts +++ b/x-pack/plugins/security/server/authentication/providers/oidc.test.ts @@ -4,20 +4,34 @@ * you may not use this file except in compliance with the Elastic License. */ -import sinon from 'sinon'; import Boom from 'boom'; -import { httpServerMock } from '../../../../../../src/core/server/mocks'; +import { elasticsearchServiceMock, httpServerMock } from '../../../../../../src/core/server/mocks'; import { mockAuthenticatedUser } from '../../../common/model/authenticated_user.mock'; -import { - MockAuthenticationProviderOptions, - mockAuthenticationProviderOptions, - mockScopedClusterClient, -} from './base.mock'; +import { MockAuthenticationProviderOptions, mockAuthenticationProviderOptions } from './base.mock'; -import { KibanaRequest } from '../../../../../../src/core/server'; +import { + ElasticsearchErrorHelpers, + IClusterClient, + KibanaRequest, + ScopeableRequest, +} from '../../../../../../src/core/server'; +import { AuthenticationResult } from '../authentication_result'; +import { DeauthenticationResult } from '../deauthentication_result'; import { OIDCAuthenticationProvider, OIDCAuthenticationFlow, ProviderLoginAttempt } from './oidc'; +function expectAuthenticateCall( + mockClusterClient: jest.Mocked, + scopeableRequest: ScopeableRequest +) { + expect(mockClusterClient.asScoped).toHaveBeenCalledTimes(1); + expect(mockClusterClient.asScoped).toHaveBeenCalledWith(scopeableRequest); + + const mockScopedClusterClient = mockClusterClient.asScoped.mock.results[0].value; + expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledTimes(1); + expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith('shield.authenticate'); +} + describe('OIDCAuthenticationProvider', () => { let provider: OIDCAuthenticationProvider; let mockOptions: MockAuthenticationProviderOptions; @@ -44,7 +58,7 @@ describe('OIDCAuthenticationProvider', () => { it('redirects third party initiated login attempts to the OpenId Connect Provider.', async () => { const request = httpServerMock.createKibanaRequest({ path: '/api/security/oidc/callback' }); - mockOptions.client.callAsInternalUser.withArgs('shield.oidcPrepare').resolves({ + mockOptions.client.callAsInternalUser.mockResolvedValue({ state: 'statevalue', nonce: 'noncevalue', redirect: @@ -56,30 +70,27 @@ describe('OIDCAuthenticationProvider', () => { '&login_hint=loginhint', }); - const authenticationResult = await provider.login(request, { - flow: OIDCAuthenticationFlow.InitiatedBy3rdParty, - iss: 'theissuer', - loginHint: 'loginhint', - }); + await expect( + provider.login(request, { + flow: OIDCAuthenticationFlow.InitiatedBy3rdParty, + iss: 'theissuer', + loginHint: 'loginhint', + }) + ).resolves.toEqual( + AuthenticationResult.redirectTo( + 'https://op-host/path/login?response_type=code' + + '&scope=openid%20profile%20email' + + '&client_id=s6BhdRkqt3' + + '&state=statevalue' + + '&redirect_uri=https%3A%2F%2Ftest-hostname:1234%2Ftest-base-path%2Fapi%2Fsecurity%2Fv1%2F/oidc' + + '&login_hint=loginhint', + { state: { state: 'statevalue', nonce: 'noncevalue', nextURL: '/base-path/' } } + ) + ); - sinon.assert.calledWithExactly(mockOptions.client.callAsInternalUser, 'shield.oidcPrepare', { + expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.oidcPrepare', { body: { iss: 'theissuer', login_hint: 'loginhint' }, }); - - expect(authenticationResult.redirected()).toBe(true); - expect(authenticationResult.redirectURL).toBe( - 'https://op-host/path/login?response_type=code' + - '&scope=openid%20profile%20email' + - '&client_id=s6BhdRkqt3' + - '&state=statevalue' + - '&redirect_uri=https%3A%2F%2Ftest-hostname:1234%2Ftest-base-path%2Fapi%2Fsecurity%2Fv1%2F/oidc' + - '&login_hint=loginhint' - ); - expect(authenticationResult.state).toEqual({ - state: 'statevalue', - nonce: 'noncevalue', - nextURL: '/base-path/', - }); }); function defineAuthenticationFlowTests( @@ -92,18 +103,24 @@ describe('OIDCAuthenticationProvider', () => { it('gets token and redirects user to requested URL if OIDC authentication response is valid.', async () => { const { request, attempt, expectedRedirectURI } = getMocks(); - mockOptions.client.callAsInternalUser - .withArgs('shield.oidcAuthenticate') - .resolves({ access_token: 'some-token', refresh_token: 'some-refresh-token' }); - - const authenticationResult = await provider.login(request, attempt, { - state: 'statevalue', - nonce: 'noncevalue', - nextURL: '/base-path/some-path', + mockOptions.client.callAsInternalUser.mockResolvedValue({ + access_token: 'some-token', + refresh_token: 'some-refresh-token', }); - sinon.assert.calledWithExactly( - mockOptions.client.callAsInternalUser, + await expect( + provider.login(request, attempt, { + state: 'statevalue', + nonce: 'noncevalue', + nextURL: '/base-path/some-path', + }) + ).resolves.toEqual( + AuthenticationResult.redirectTo('/base-path/some-path', { + state: { accessToken: 'some-token', refreshToken: 'some-refresh-token' }, + }) + ); + + expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith( 'shield.oidcAuthenticate', { body: { @@ -114,58 +131,52 @@ describe('OIDCAuthenticationProvider', () => { }, } ); - - expect(authenticationResult.redirected()).toBe(true); - expect(authenticationResult.redirectURL).toBe('/base-path/some-path'); - expect(authenticationResult.state).toEqual({ - accessToken: 'some-token', - refreshToken: 'some-refresh-token', - }); }); it('fails if authentication response is presented but session state does not contain the state parameter.', async () => { const { request, attempt } = getMocks(); - const authenticationResult = await provider.login(request, attempt, { - nextURL: '/base-path/some-path', - }); - - sinon.assert.notCalled(mockOptions.client.callAsInternalUser); - - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.error).toEqual( - Boom.badRequest( - 'Response session state does not have corresponding state or nonce parameters or redirect URL.' + await expect( + provider.login(request, attempt, { nextURL: '/base-path/some-path' }) + ).resolves.toEqual( + AuthenticationResult.failed( + Boom.badRequest( + 'Response session state does not have corresponding state or nonce parameters or redirect URL.' + ) ) ); + + expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled(); }); it('fails if authentication response is presented but session state does not contain redirect URL.', async () => { const { request, attempt } = getMocks(); - const authenticationResult = await provider.login(request, attempt, { - state: 'statevalue', - nonce: 'noncevalue', - }); - - sinon.assert.notCalled(mockOptions.client.callAsInternalUser); - - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.error).toEqual( - Boom.badRequest( - 'Response session state does not have corresponding state or nonce parameters or redirect URL.' + await expect( + provider.login(request, attempt, { state: 'statevalue', nonce: 'noncevalue' }) + ).resolves.toEqual( + AuthenticationResult.failed( + Boom.badRequest( + 'Response session state does not have corresponding state or nonce parameters or redirect URL.' + ) ) ); + + expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled(); }); it('fails if session state is not presented.', async () => { const { request, attempt } = getMocks(); - const authenticationResult = await provider.login(request, attempt, {}); - - sinon.assert.notCalled(mockOptions.client.callAsInternalUser); + await expect(provider.login(request, attempt, {})).resolves.toEqual( + AuthenticationResult.failed( + Boom.badRequest( + 'Response session state does not have corresponding state or nonce parameters or redirect URL.' + ) + ) + ); - expect(authenticationResult.failed()).toBe(true); + expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled(); }); it('fails if authentication response is not valid.', async () => { @@ -174,18 +185,17 @@ describe('OIDCAuthenticationProvider', () => { const failureReason = new Error( 'Failed to exchange code for Id Token using the Token Endpoint.' ); - mockOptions.client.callAsInternalUser - .withArgs('shield.oidcAuthenticate') - .returns(Promise.reject(failureReason)); - - const authenticationResult = await provider.login(request, attempt, { - state: 'statevalue', - nonce: 'noncevalue', - nextURL: '/base-path/some-path', - }); + mockOptions.client.callAsInternalUser.mockRejectedValue(failureReason); - sinon.assert.calledWithExactly( - mockOptions.client.callAsInternalUser, + await expect( + provider.login(request, attempt, { + state: 'statevalue', + nonce: 'noncevalue', + nextURL: '/base-path/some-path', + }) + ).resolves.toEqual(AuthenticationResult.failed(failureReason)); + + expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith( 'shield.oidcAuthenticate', { body: { @@ -196,9 +206,6 @@ describe('OIDCAuthenticationProvider', () => { }, } ); - - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.error).toBe(failureReason); }); } @@ -234,16 +241,15 @@ describe('OIDCAuthenticationProvider', () => { describe('`authenticate` method', () => { it('does not handle AJAX request that can not be authenticated.', async () => { const request = httpServerMock.createKibanaRequest({ headers: { 'kbn-xsrf': 'xsrf' } }); - - const authenticationResult = await provider.authenticate(request, null); - - expect(authenticationResult.notHandled()).toBe(true); + await expect(provider.authenticate(request, null)).resolves.toEqual( + AuthenticationResult.notHandled() + ); }); it('redirects non-AJAX request that can not be authenticated to the OpenId Connect Provider.', async () => { const request = httpServerMock.createKibanaRequest({ path: '/s/foo/some-path' }); - mockOptions.client.callAsInternalUser.withArgs('shield.oidcPrepare').resolves({ + mockOptions.client.callAsInternalUser.mockResolvedValue({ state: 'statevalue', nonce: 'noncevalue', redirect: @@ -254,84 +260,99 @@ describe('OIDCAuthenticationProvider', () => { '&redirect_uri=https%3A%2F%2Ftest-hostname:1234%2Ftest-base-path%2Fapi%2Fsecurity%2Fv1%2F/oidc', }); - const authenticationResult = await provider.authenticate(request, null); + await expect(provider.authenticate(request, null)).resolves.toEqual( + AuthenticationResult.redirectTo( + 'https://op-host/path/login?response_type=code' + + '&scope=openid%20profile%20email' + + '&client_id=s6BhdRkqt3' + + '&state=statevalue' + + '&redirect_uri=https%3A%2F%2Ftest-hostname:1234%2Ftest-base-path%2Fapi%2Fsecurity%2Fv1%2F/oidc', + { + state: { + state: 'statevalue', + nonce: 'noncevalue', + nextURL: '/base-path/s/foo/some-path', + }, + } + ) + ); - sinon.assert.calledWithExactly(mockOptions.client.callAsInternalUser, 'shield.oidcPrepare', { + expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.oidcPrepare', { body: { realm: `oidc1` }, }); - - expect(authenticationResult.redirected()).toBe(true); - expect(authenticationResult.redirectURL).toBe( - 'https://op-host/path/login?response_type=code' + - '&scope=openid%20profile%20email' + - '&client_id=s6BhdRkqt3' + - '&state=statevalue' + - '&redirect_uri=https%3A%2F%2Ftest-hostname:1234%2Ftest-base-path%2Fapi%2Fsecurity%2Fv1%2F/oidc' - ); - expect(authenticationResult.state).toEqual({ - state: 'statevalue', - nonce: 'noncevalue', - nextURL: '/base-path/s/foo/some-path', - }); }); it('fails if OpenID Connect authentication request preparation fails.', async () => { const request = httpServerMock.createKibanaRequest({ path: '/some-path' }); const failureReason = new Error('Realm is misconfigured!'); - mockOptions.client.callAsInternalUser - .withArgs('shield.oidcPrepare') - .returns(Promise.reject(failureReason)); + mockOptions.client.callAsInternalUser.mockRejectedValue(failureReason); - const authenticationResult = await provider.authenticate(request, null); + await expect(provider.authenticate(request, null)).resolves.toEqual( + AuthenticationResult.failed(failureReason) + ); - sinon.assert.calledWithExactly(mockOptions.client.callAsInternalUser, 'shield.oidcPrepare', { + expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.oidcPrepare', { body: { realm: `oidc1` }, }); - - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.error).toBe(failureReason); }); it('succeeds if state contains a valid token.', async () => { const user = mockAuthenticatedUser(); - const request = httpServerMock.createKibanaRequest(); + const request = httpServerMock.createKibanaRequest({ headers: {} }); const tokenPair = { accessToken: 'some-valid-token', refreshToken: 'some-valid-refresh-token', }; const authorization = `Bearer ${tokenPair.accessToken}`; - mockScopedClusterClient(mockOptions.client, sinon.match({ headers: { authorization } })) - .callAsCurrentUser.withArgs('shield.authenticate') - .resolves(user); + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.callAsCurrentUser.mockResolvedValue(user); + mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); + + await expect(provider.authenticate(request, tokenPair)).resolves.toEqual( + AuthenticationResult.succeeded( + { ...user, authentication_provider: 'oidc' }, + { authHeaders: { authorization } } + ) + ); - const authenticationResult = await provider.authenticate(request, tokenPair); + expectAuthenticateCall(mockOptions.client, { headers: { authorization } }); expect(request.headers).not.toHaveProperty('authorization'); - expect(authenticationResult.succeeded()).toBe(true); - expect(authenticationResult.authHeaders).toEqual({ authorization }); - expect(authenticationResult.user).toEqual({ ...user, authentication_provider: 'oidc' }); - expect(authenticationResult.state).toBeUndefined(); }); - it('does not handle `authorization` header with unsupported schema even if state contains a valid token.', async () => { + it('does not handle authentication via `authorization` header.', async () => { const request = httpServerMock.createKibanaRequest({ - headers: { authorization: 'Basic some:credentials' }, + headers: { authorization: 'Bearer some-token' }, }); - const authenticationResult = await provider.authenticate(request, { - accessToken: 'some-valid-token', - refreshToken: 'some-valid-refresh-token', + await expect(provider.authenticate(request)).resolves.toEqual( + AuthenticationResult.notHandled() + ); + + expect(mockOptions.client.asScoped).not.toHaveBeenCalled(); + expect(request.headers.authorization).toBe('Bearer some-token'); + }); + + it('does not handle authentication via `authorization` header even if state contains a valid token.', async () => { + const request = httpServerMock.createKibanaRequest({ + headers: { authorization: 'Bearer some-token' }, }); - sinon.assert.notCalled(mockOptions.client.asScoped); - expect(request.headers.authorization).toBe('Basic some:credentials'); - expect(authenticationResult.notHandled()).toBe(true); + await expect( + provider.authenticate(request, { + accessToken: 'some-valid-token', + refreshToken: 'some-valid-refresh-token', + }) + ).resolves.toEqual(AuthenticationResult.notHandled()); + + expect(mockOptions.client.asScoped).not.toHaveBeenCalled(); + expect(request.headers.authorization).toBe('Bearer some-token'); }); it('fails if token from the state is rejected because of unknown reason.', async () => { - const request = httpServerMock.createKibanaRequest(); + const request = httpServerMock.createKibanaRequest({ headers: {} }); const tokenPair = { accessToken: 'some-invalid-token', refreshToken: 'some-invalid-refresh-token', @@ -339,15 +360,17 @@ describe('OIDCAuthenticationProvider', () => { const authorization = `Bearer ${tokenPair.accessToken}`; const failureReason = new Error('Token is not valid!'); - mockScopedClusterClient(mockOptions.client, sinon.match({ headers: { authorization } })) - .callAsCurrentUser.withArgs('shield.authenticate') - .rejects(failureReason); + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.callAsCurrentUser.mockRejectedValue(failureReason); + mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); + + await expect(provider.authenticate(request, tokenPair)).resolves.toEqual( + AuthenticationResult.failed(failureReason) + ); - const authenticationResult = await provider.authenticate(request, tokenPair); + expectAuthenticateCall(mockOptions.client, { headers: { authorization } }); expect(request.headers).not.toHaveProperty('authorization'); - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.error).toBe(failureReason); }); it('succeeds if token from the state is expired, but has been successfully refreshed.', async () => { @@ -355,67 +378,80 @@ describe('OIDCAuthenticationProvider', () => { const request = httpServerMock.createKibanaRequest(); const tokenPair = { accessToken: 'expired-token', refreshToken: 'valid-refresh-token' }; - mockScopedClusterClient( - mockOptions.client, - sinon.match({ headers: { authorization: `Bearer ${tokenPair.accessToken}` } }) - ) - .callAsCurrentUser.withArgs('shield.authenticate') - .rejects({ statusCode: 401 }); + mockOptions.client.asScoped.mockImplementation(scopeableRequest => { + if (scopeableRequest?.headers.authorization === `Bearer ${tokenPair.accessToken}`) { + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.callAsCurrentUser.mockRejectedValue( + ElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error()) + ); + return mockScopedClusterClient; + } + + if (scopeableRequest?.headers.authorization === 'Bearer new-access-token') { + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.callAsCurrentUser.mockResolvedValue(user); + return mockScopedClusterClient; + } + + throw new Error('Unexpected call'); + }); - mockScopedClusterClient( - mockOptions.client, - sinon.match({ headers: { authorization: 'Bearer new-access-token' } }) - ) - .callAsCurrentUser.withArgs('shield.authenticate') - .resolves(user); + mockOptions.tokens.refresh.mockResolvedValue({ + accessToken: 'new-access-token', + refreshToken: 'new-refresh-token', + }); - mockOptions.tokens.refresh - .withArgs(tokenPair.refreshToken) - .resolves({ accessToken: 'new-access-token', refreshToken: 'new-refresh-token' }); + await expect(provider.authenticate(request, tokenPair)).resolves.toEqual( + AuthenticationResult.succeeded( + { ...user, authentication_provider: 'oidc' }, + { + authHeaders: { authorization: 'Bearer new-access-token' }, + state: { accessToken: 'new-access-token', refreshToken: 'new-refresh-token' }, + } + ) + ); - const authenticationResult = await provider.authenticate(request, tokenPair); + expect(mockOptions.tokens.refresh).toHaveBeenCalledTimes(1); + expect(mockOptions.tokens.refresh).toHaveBeenCalledWith(tokenPair.refreshToken); expect(request.headers).not.toHaveProperty('authorization'); - expect(authenticationResult.succeeded()).toBe(true); - expect(authenticationResult.authHeaders).toEqual({ - authorization: 'Bearer new-access-token', - }); - expect(authenticationResult.user).toEqual({ ...user, authentication_provider: 'oidc' }); - expect(authenticationResult.state).toEqual({ - accessToken: 'new-access-token', - refreshToken: 'new-refresh-token', - }); }); it('fails if token from the state is expired and refresh attempt failed too.', async () => { - const request = httpServerMock.createKibanaRequest(); + const request = httpServerMock.createKibanaRequest({ headers: {} }); const tokenPair = { accessToken: 'expired-token', refreshToken: 'invalid-refresh-token' }; + const authorization = `Bearer ${tokenPair.accessToken}`; - mockScopedClusterClient( - mockOptions.client, - sinon.match({ headers: { authorization: `Bearer ${tokenPair.accessToken}` } }) - ) - .callAsCurrentUser.withArgs('shield.authenticate') - .rejects({ statusCode: 401 }); + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.callAsCurrentUser.mockRejectedValue( + ElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error()) + ); + mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); const refreshFailureReason = { statusCode: 500, message: 'Something is wrong with refresh token.', }; - mockOptions.tokens.refresh.withArgs(tokenPair.refreshToken).rejects(refreshFailureReason); + mockOptions.tokens.refresh.mockRejectedValue(refreshFailureReason); - const authenticationResult = await provider.authenticate(request, tokenPair); + await expect(provider.authenticate(request, tokenPair)).resolves.toEqual( + AuthenticationResult.failed(refreshFailureReason as any) + ); + + expect(mockOptions.tokens.refresh).toHaveBeenCalledTimes(1); + expect(mockOptions.tokens.refresh).toHaveBeenCalledWith(tokenPair.refreshToken); + + expectAuthenticateCall(mockOptions.client, { headers: { authorization } }); expect(request.headers).not.toHaveProperty('authorization'); - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.error).toBe(refreshFailureReason); }); it('redirects to OpenID Connect Provider for non-AJAX requests if refresh token is expired or already refreshed.', async () => { - const request = httpServerMock.createKibanaRequest({ path: '/s/foo/some-path' }); + const request = httpServerMock.createKibanaRequest({ path: '/s/foo/some-path', headers: {} }); const tokenPair = { accessToken: 'expired-token', refreshToken: 'expired-refresh-token' }; + const authorization = `Bearer ${tokenPair.accessToken}`; - mockOptions.client.callAsInternalUser.withArgs('shield.oidcPrepare').resolves({ + mockOptions.client.callAsInternalUser.mockResolvedValue({ state: 'statevalue', nonce: 'noncevalue', redirect: @@ -426,115 +462,71 @@ describe('OIDCAuthenticationProvider', () => { '&redirect_uri=https%3A%2F%2Ftest-hostname:1234%2Ftest-base-path%2Fapi%2Fsecurity%2Fv1%2F/oidc', }); - mockScopedClusterClient( - mockOptions.client, - sinon.match({ headers: { authorization: `Bearer ${tokenPair.accessToken}` } }) - ) - .callAsCurrentUser.withArgs('shield.authenticate') - .rejects({ statusCode: 401 }); + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.callAsCurrentUser.mockRejectedValue( + ElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error()) + ); + mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); - mockOptions.tokens.refresh.withArgs(tokenPair.refreshToken).resolves(null); + mockOptions.tokens.refresh.mockResolvedValue(null); - const authenticationResult = await provider.authenticate(request, tokenPair); + await expect(provider.authenticate(request, tokenPair)).resolves.toEqual( + AuthenticationResult.redirectTo( + 'https://op-host/path/login?response_type=code' + + '&scope=openid%20profile%20email' + + '&client_id=s6BhdRkqt3' + + '&state=statevalue' + + '&redirect_uri=https%3A%2F%2Ftest-hostname:1234%2Ftest-base-path%2Fapi%2Fsecurity%2Fv1%2F/oidc', + { + state: { + state: 'statevalue', + nonce: 'noncevalue', + nextURL: '/base-path/s/foo/some-path', + }, + } + ) + ); - sinon.assert.calledWithExactly(mockOptions.client.callAsInternalUser, 'shield.oidcPrepare', { - body: { realm: `oidc1` }, + expect(mockOptions.tokens.refresh).toHaveBeenCalledTimes(1); + expect(mockOptions.tokens.refresh).toHaveBeenCalledWith(tokenPair.refreshToken); + + expect(mockOptions.client.asScoped).toHaveBeenCalledTimes(1); + expect(mockOptions.client.asScoped).toHaveBeenCalledWith({ + headers: { authorization }, }); + expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledTimes(1); + expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith('shield.authenticate'); - expect(authenticationResult.redirected()).toBe(true); - expect(authenticationResult.redirectURL).toBe( - 'https://op-host/path/login?response_type=code' + - '&scope=openid%20profile%20email' + - '&client_id=s6BhdRkqt3' + - '&state=statevalue' + - '&redirect_uri=https%3A%2F%2Ftest-hostname:1234%2Ftest-base-path%2Fapi%2Fsecurity%2Fv1%2F/oidc' - ); - expect(authenticationResult.state).toEqual({ - state: 'statevalue', - nonce: 'noncevalue', - nextURL: '/base-path/s/foo/some-path', + expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.oidcPrepare', { + body: { realm: `oidc1` }, }); }); it('fails for AJAX requests with user friendly message if refresh token is expired.', async () => { const request = httpServerMock.createKibanaRequest({ headers: { 'kbn-xsrf': 'xsrf' } }); const tokenPair = { accessToken: 'expired-token', refreshToken: 'expired-refresh-token' }; + const authorization = `Bearer ${tokenPair.accessToken}`; - mockScopedClusterClient( - mockOptions.client, - sinon.match({ headers: { authorization: `Bearer ${tokenPair.accessToken}` } }) - ) - .callAsCurrentUser.withArgs('shield.authenticate') - .rejects({ statusCode: 401 }); - - mockOptions.tokens.refresh.withArgs(tokenPair.refreshToken).resolves(null); - - const authenticationResult = await provider.authenticate(request, tokenPair); - - expect(request.headers).not.toHaveProperty('authorization'); - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.error).toEqual( - Boom.badRequest('Both access and refresh tokens are expired.') + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.callAsCurrentUser.mockRejectedValue( + ElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error()) ); - }); - - it('succeeds if `authorization` contains a valid token.', async () => { - const user = mockAuthenticatedUser(); - const authorization = 'Bearer some-valid-token'; - const request = httpServerMock.createKibanaRequest({ headers: { authorization } }); - - mockScopedClusterClient(mockOptions.client, sinon.match({ headers: { authorization } })) - .callAsCurrentUser.withArgs('shield.authenticate') - .resolves(user); + mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); - const authenticationResult = await provider.authenticate(request); + mockOptions.tokens.refresh.mockResolvedValue(null); - expect(request.headers.authorization).toBe('Bearer some-valid-token'); - expect(authenticationResult.succeeded()).toBe(true); - expect(authenticationResult.authHeaders).toBeUndefined(); - expect(authenticationResult.user).toEqual({ ...user, authentication_provider: 'oidc' }); - expect(authenticationResult.state).toBeUndefined(); - }); - - it('fails if token from `authorization` header is rejected.', async () => { - const authorization = 'Bearer some-invalid-token'; - const request = httpServerMock.createKibanaRequest({ headers: { authorization } }); - - const failureReason = { statusCode: 401 }; - mockScopedClusterClient(mockOptions.client, sinon.match({ headers: { authorization } })) - .callAsCurrentUser.withArgs('shield.authenticate') - .rejects(failureReason); + await expect(provider.authenticate(request, tokenPair)).resolves.toEqual( + AuthenticationResult.failed(Boom.badRequest('Both access and refresh tokens are expired.')) + ); - const authenticationResult = await provider.authenticate(request); + expect(mockOptions.tokens.refresh).toHaveBeenCalledTimes(1); + expect(mockOptions.tokens.refresh).toHaveBeenCalledWith(tokenPair.refreshToken); - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.error).toBe(failureReason); - }); - - it('fails if token from `authorization` header is rejected even if state contains a valid one.', async () => { - const user = mockAuthenticatedUser(); - const authorization = 'Bearer some-invalid-token'; - const request = httpServerMock.createKibanaRequest({ headers: { authorization } }); - - const failureReason = { statusCode: 401 }; - mockScopedClusterClient(mockOptions.client, sinon.match({ headers: { authorization } })) - .callAsCurrentUser.withArgs('shield.authenticate') - .rejects(failureReason); - - mockScopedClusterClient( - mockOptions.client, - sinon.match({ headers: { authorization: 'Bearer some-valid-token' } }) - ) - .callAsCurrentUser.withArgs('shield.authenticate') - .resolves(user); - - const authenticationResult = await provider.authenticate(request, { - accessToken: 'some-valid-token', - refreshToken: 'some-valid-refresh-token', + expectAuthenticateCall(mockOptions.client, { + headers: { 'kbn-xsrf': 'xsrf', authorization }, }); - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.error).toBe(failureReason); + expect(request.headers).not.toHaveProperty('authorization'); }); }); @@ -542,16 +534,19 @@ describe('OIDCAuthenticationProvider', () => { it('returns `notHandled` if state is not presented or does not include access token.', async () => { const request = httpServerMock.createKibanaRequest(); - let deauthenticateResult = await provider.logout(request, {}); - expect(deauthenticateResult.notHandled()).toBe(true); + await expect(provider.logout(request, undefined as any)).resolves.toEqual( + DeauthenticationResult.notHandled() + ); - deauthenticateResult = await provider.logout(request, {}); - expect(deauthenticateResult.notHandled()).toBe(true); + await expect(provider.logout(request, {})).resolves.toEqual( + DeauthenticationResult.notHandled() + ); - deauthenticateResult = await provider.logout(request, { nonce: 'x' }); - expect(deauthenticateResult.notHandled()).toBe(true); + await expect(provider.logout(request, { nonce: 'x' })).resolves.toEqual( + DeauthenticationResult.notHandled() + ); - sinon.assert.notCalled(mockOptions.client.callAsInternalUser); + expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled(); }); it('fails if OpenID Connect logout call fails.', async () => { @@ -560,22 +555,16 @@ describe('OIDCAuthenticationProvider', () => { const refreshToken = 'x-oidc-refresh-token'; const failureReason = new Error('Realm is misconfigured!'); - mockOptions.client.callAsInternalUser - .withArgs('shield.oidcLogout') - .returns(Promise.reject(failureReason)); + mockOptions.client.callAsInternalUser.mockRejectedValue(failureReason); - const authenticationResult = await provider.logout(request, { - accessToken, - refreshToken, - }); + await expect(provider.logout(request, { accessToken, refreshToken })).resolves.toEqual( + DeauthenticationResult.failed(failureReason) + ); - sinon.assert.calledOnce(mockOptions.client.callAsInternalUser); - sinon.assert.calledWithExactly(mockOptions.client.callAsInternalUser, 'shield.oidcLogout', { + expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledTimes(1); + expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.oidcLogout', { body: { token: accessToken, refresh_token: refreshToken }, }); - - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.error).toBe(failureReason); }); it('redirects to /logged_out if `redirect` field in OpenID Connect logout response is null.', async () => { @@ -583,22 +572,16 @@ describe('OIDCAuthenticationProvider', () => { const accessToken = 'x-oidc-token'; const refreshToken = 'x-oidc-refresh-token'; - mockOptions.client.callAsInternalUser - .withArgs('shield.oidcLogout') - .resolves({ redirect: null }); + mockOptions.client.callAsInternalUser.mockResolvedValue({ redirect: null }); - const authenticationResult = await provider.logout(request, { - accessToken, - refreshToken, - }); + await expect(provider.logout(request, { accessToken, refreshToken })).resolves.toEqual( + DeauthenticationResult.redirectTo('/mock-server-basepath/logged_out') + ); - sinon.assert.calledOnce(mockOptions.client.callAsInternalUser); - sinon.assert.calledWithExactly(mockOptions.client.callAsInternalUser, 'shield.oidcLogout', { + expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledTimes(1); + expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.oidcLogout', { body: { token: accessToken, refresh_token: refreshToken }, }); - - expect(authenticationResult.redirected()).toBe(true); - expect(authenticationResult.redirectURL).toBe('/mock-server-basepath/logged_out'); }); it('redirects user to the OpenID Connect Provider if RP initiated SLO is supported.', async () => { @@ -606,18 +589,22 @@ describe('OIDCAuthenticationProvider', () => { const accessToken = 'x-oidc-token'; const refreshToken = 'x-oidc-refresh-token'; - mockOptions.client.callAsInternalUser - .withArgs('shield.oidcLogout') - .resolves({ redirect: 'http://fake-idp/logout&id_token_hint=thehint' }); - - const authenticationResult = await provider.logout(request, { - accessToken, - refreshToken, + mockOptions.client.callAsInternalUser.mockResolvedValue({ + redirect: 'http://fake-idp/logout&id_token_hint=thehint', }); - sinon.assert.calledOnce(mockOptions.client.callAsInternalUser); - expect(authenticationResult.redirected()).toBe(true); - expect(authenticationResult.redirectURL).toBe('http://fake-idp/logout&id_token_hint=thehint'); + await expect(provider.logout(request, { accessToken, refreshToken })).resolves.toEqual( + DeauthenticationResult.redirectTo('http://fake-idp/logout&id_token_hint=thehint') + ); + + expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledTimes(1); + expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.oidcLogout', { + body: { token: accessToken, refresh_token: refreshToken }, + }); }); }); + + it('`getHTTPAuthenticationScheme` method', () => { + expect(provider.getHTTPAuthenticationScheme()).toBe('bearer'); + }); }); diff --git a/x-pack/plugins/security/server/authentication/providers/oidc.ts b/x-pack/plugins/security/server/authentication/providers/oidc.ts index f13a2ec05231a..c6b504e722adf 100644 --- a/x-pack/plugins/security/server/authentication/providers/oidc.ts +++ b/x-pack/plugins/security/server/authentication/providers/oidc.ts @@ -10,6 +10,7 @@ import { KibanaRequest } from '../../../../../../src/core/server'; import { AuthenticationResult } from '../authentication_result'; import { canRedirectRequest } from '../can_redirect_request'; import { DeauthenticationResult } from '../deauthentication_result'; +import { getHTTPAuthenticationScheme } from '../get_http_authentication_scheme'; import { Tokens, TokenPair } from '../tokens'; import { AuthenticationProviderOptions, @@ -130,16 +131,13 @@ export class OIDCAuthenticationProvider extends BaseAuthenticationProvider { public async authenticate(request: KibanaRequest, state?: ProviderState | null) { this.logger.debug(`Trying to authenticate user request to ${request.url.path}.`); - // We should get rid of `Bearer` scheme support as soon as Reporting doesn't need it anymore. - let { - authenticationResult, - headerNotRecognized, // eslint-disable-line prefer-const - } = await this.authenticateViaHeader(request); - if (headerNotRecognized) { - return authenticationResult; + if (getHTTPAuthenticationScheme(request) != null) { + this.logger.debug('Cannot authenticate requests with `Authorization` header.'); + return AuthenticationResult.notHandled(); } - if (state && authenticationResult.notHandled()) { + let authenticationResult = AuthenticationResult.notHandled(); + if (state) { authenticationResult = await this.authenticateViaState(request, state); if ( authenticationResult.failed() && @@ -276,46 +274,6 @@ export class OIDCAuthenticationProvider extends BaseAuthenticationProvider { } } - /** - * Validates whether request contains `Bearer ***` Authorization header and just passes it - * forward to Elasticsearch backend. - * @param request Request instance. - */ - private async authenticateViaHeader(request: KibanaRequest) { - this.logger.debug('Trying to authenticate via header.'); - - const authorization = request.headers.authorization; - if (!authorization || typeof authorization !== 'string') { - this.logger.debug('Authorization header is not presented.'); - return { - authenticationResult: AuthenticationResult.notHandled(), - }; - } - - const authenticationSchema = authorization.split(/\s+/)[0]; - if (authenticationSchema.toLowerCase() !== 'bearer') { - this.logger.debug(`Unsupported authentication schema: ${authenticationSchema}`); - return { - authenticationResult: AuthenticationResult.notHandled(), - headerNotRecognized: true, - }; - } - - try { - const user = await this.getUser(request); - - this.logger.debug('Request has been authenticated via header.'); - return { - authenticationResult: AuthenticationResult.succeeded(user), - }; - } catch (err) { - this.logger.debug(`Failed to authenticate request via header: ${err.message}`); - return { - authenticationResult: AuthenticationResult.failed(err), - }; - } - } - /** * Tries to extract an elasticsearch access token from state and adds it to the request before it's * forwarded to Elasticsearch backend. @@ -444,4 +402,12 @@ export class OIDCAuthenticationProvider extends BaseAuthenticationProvider { return DeauthenticationResult.failed(err); } } + + /** + * Returns HTTP authentication scheme (`Bearer`) that's used within `Authorization` HTTP header + * that provider attaches to all successfully authenticated requests to Elasticsearch. + */ + public getHTTPAuthenticationScheme() { + return 'bearer'; + } } diff --git a/x-pack/plugins/security/server/authentication/providers/pki.test.ts b/x-pack/plugins/security/server/authentication/providers/pki.test.ts index a2dda88c4680c..efc286c6c895f 100644 --- a/x-pack/plugins/security/server/authentication/providers/pki.test.ts +++ b/x-pack/plugins/security/server/authentication/providers/pki.test.ts @@ -7,23 +7,23 @@ jest.mock('net'); jest.mock('tls'); +import { Socket } from 'net'; import { PeerCertificate, TLSSocket } from 'tls'; +import Boom from 'boom'; import { errors } from 'elasticsearch'; import { elasticsearchServiceMock, httpServerMock } from '../../../../../../src/core/server/mocks'; import { mockAuthenticatedUser } from '../../../common/model/authenticated_user.mock'; -import { - MockAuthenticationProviderOptionsWithJest, - mockAuthenticationProviderOptionsWithJest, -} from './base.mock'; +import { MockAuthenticationProviderOptions, mockAuthenticationProviderOptions } from './base.mock'; -import { PKIAuthenticationProvider } from './pki'; import { ElasticsearchErrorHelpers, - ScopedClusterClient, -} from '../../../../../../src/core/server/elasticsearch'; -import { Socket } from 'net'; -import { getErrorStatusCode } from '../../errors'; + IClusterClient, + ScopeableRequest, +} from '../../../../../../src/core/server'; +import { AuthenticationResult } from '../authentication_result'; +import { DeauthenticationResult } from '../deauthentication_result'; +import { PKIAuthenticationProvider } from './pki'; interface MockPeerCertificate extends Partial { issuerCertificate: MockPeerCertificate; @@ -62,32 +62,59 @@ function getMockSocket({ return socket; } +function expectAuthenticateCall( + mockClusterClient: jest.Mocked, + scopeableRequest: ScopeableRequest +) { + expect(mockClusterClient.asScoped).toHaveBeenCalledTimes(1); + expect(mockClusterClient.asScoped).toHaveBeenCalledWith(scopeableRequest); + + const mockScopedClusterClient = mockClusterClient.asScoped.mock.results[0].value; + expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledTimes(1); + expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith('shield.authenticate'); +} + describe('PKIAuthenticationProvider', () => { let provider: PKIAuthenticationProvider; - let mockOptions: MockAuthenticationProviderOptionsWithJest; + let mockOptions: MockAuthenticationProviderOptions; beforeEach(() => { - mockOptions = mockAuthenticationProviderOptionsWithJest(); + mockOptions = mockAuthenticationProviderOptions(); provider = new PKIAuthenticationProvider(mockOptions); }); afterEach(() => jest.clearAllMocks()); describe('`authenticate` method', () => { - it('does not handle `authorization` header with unsupported schema even if state contains a valid token.', async () => { + it('does not handle authentication via `authorization` header.', async () => { + const request = httpServerMock.createKibanaRequest({ + headers: { authorization: 'Bearer some-token' }, + }); + + await expect(provider.authenticate(request)).resolves.toEqual( + AuthenticationResult.notHandled() + ); + + expect(mockOptions.client.asScoped).not.toHaveBeenCalled(); + expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled(); + expect(request.headers.authorization).toBe('Bearer some-token'); + }); + + it('does not handle authentication via `authorization` header even if state contains a valid token.', async () => { const request = httpServerMock.createKibanaRequest({ - headers: { authorization: 'Basic some:credentials' }, + headers: { authorization: 'Bearer some-token' }, }); const state = { accessToken: 'some-valid-token', peerCertificateFingerprint256: '2A:7A:C2:DD', }; - const authenticationResult = await provider.authenticate(request, state); + await expect(provider.authenticate(request, state)).resolves.toEqual( + AuthenticationResult.notHandled() + ); expect(mockOptions.client.asScoped).not.toHaveBeenCalled(); expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled(); - expect(request.headers.authorization).toBe('Basic some:credentials'); - expect(authenticationResult.notHandled()).toBe(true); + expect(request.headers.authorization).toBe('Bearer some-token'); }); it('does not handle requests without certificate.', async () => { @@ -95,9 +122,10 @@ describe('PKIAuthenticationProvider', () => { socket: getMockSocket({ authorized: true }), }); - const authenticationResult = await provider.authenticate(request, null); + await expect(provider.authenticate(request, null)).resolves.toEqual( + AuthenticationResult.notHandled() + ); - expect(authenticationResult.notHandled()).toBe(true); expect(mockOptions.client.asScoped).not.toHaveBeenCalled(); expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled(); }); @@ -107,9 +135,10 @@ describe('PKIAuthenticationProvider', () => { socket: getMockSocket({ peerCertificate: getMockPeerCertificate('2A:7A:C2:DD') }), }); - const authenticationResult = await provider.authenticate(request, null); + await expect(provider.authenticate(request, null)).resolves.toEqual( + AuthenticationResult.notHandled() + ); - expect(authenticationResult.notHandled()).toBe(true); expect(mockOptions.client.asScoped).not.toHaveBeenCalled(); expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled(); }); @@ -121,12 +150,10 @@ describe('PKIAuthenticationProvider', () => { const state = { accessToken: 'token', peerCertificateFingerprint256: '2A:7A:C2:DD' }; - const authenticationResult = await provider.authenticate(request, state); - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.error).toMatchInlineSnapshot( - `[Error: Peer certificate is not available]` + await expect(provider.authenticate(request, state)).resolves.toEqual( + AuthenticationResult.failed(new Error('Peer certificate is not available')) ); - expect(authenticationResult.authResponseHeaders).toBeUndefined(); + expect(mockOptions.tokens.invalidate).not.toHaveBeenCalled(); }); @@ -134,10 +161,9 @@ describe('PKIAuthenticationProvider', () => { const request = httpServerMock.createKibanaRequest({ socket: getMockSocket() }); const state = { accessToken: 'token', peerCertificateFingerprint256: '2A:7A:C2:DD' }; - const authenticationResult = await provider.authenticate(request, state); - - expect(authenticationResult.failed()).toBe(true); - expect(getErrorStatusCode(authenticationResult.error)).toBe(401); + await expect(provider.authenticate(request, state)).resolves.toEqual( + AuthenticationResult.failed(Boom.unauthorized()) + ); expect(mockOptions.tokens.invalidate).toHaveBeenCalledTimes(1); expect(mockOptions.tokens.invalidate).toHaveBeenCalledWith({ @@ -151,10 +177,9 @@ describe('PKIAuthenticationProvider', () => { }); const state = { accessToken: 'token', peerCertificateFingerprint256: '2A:7A:C2:DD' }; - const authenticationResult = await provider.authenticate(request, state); - - expect(authenticationResult.failed()).toBe(true); - expect(getErrorStatusCode(authenticationResult.error)).toBe(401); + await expect(provider.authenticate(request, state)).resolves.toEqual( + AuthenticationResult.failed(Boom.unauthorized()) + ); expect(mockOptions.tokens.invalidate).toHaveBeenCalledTimes(1); expect(mockOptions.tokens.invalidate).toHaveBeenCalledWith({ @@ -174,12 +199,18 @@ describe('PKIAuthenticationProvider', () => { const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); mockScopedClusterClient.callAsCurrentUser.mockResolvedValue(user); - mockOptions.client.asScoped.mockReturnValue( - (mockScopedClusterClient as unknown) as jest.Mocked - ); + mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); mockOptions.client.callAsInternalUser.mockResolvedValue({ access_token: 'access-token' }); - const authenticationResult = await provider.authenticate(request); + await expect(provider.authenticate(request)).resolves.toEqual( + AuthenticationResult.succeeded( + { ...user, authentication_provider: 'pki' }, + { + authHeaders: { authorization: 'Bearer access-token' }, + state: { accessToken: 'access-token', peerCertificateFingerprint256: '2A:7A:C2:DD' }, + } + ) + ); expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledTimes(1); expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.delegatePKI', { @@ -191,22 +222,11 @@ describe('PKIAuthenticationProvider', () => { }, }); - expect(mockOptions.client.asScoped).toHaveBeenCalledTimes(1); - expect(mockOptions.client.asScoped).toHaveBeenCalledWith({ - headers: { authorization: `Bearer access-token` }, + expectAuthenticateCall(mockOptions.client, { + headers: { authorization: 'Bearer access-token' }, }); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledTimes(1); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith('shield.authenticate'); expect(request.headers).not.toHaveProperty('authorization'); - expect(authenticationResult.succeeded()).toBe(true); - expect(authenticationResult.user).toEqual({ ...user, authentication_provider: 'pki' }); - expect(authenticationResult.authHeaders).toEqual({ authorization: 'Bearer access-token' }); - expect(authenticationResult.authResponseHeaders).toBeUndefined(); - expect(authenticationResult.state).toEqual({ - accessToken: 'access-token', - peerCertificateFingerprint256: '2A:7A:C2:DD', - }); }); it('gets an access token in exchange to a self-signed certificate and stores it in the state.', async () => { @@ -221,34 +241,29 @@ describe('PKIAuthenticationProvider', () => { const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); mockScopedClusterClient.callAsCurrentUser.mockResolvedValue(user); - mockOptions.client.asScoped.mockReturnValue( - (mockScopedClusterClient as unknown) as jest.Mocked - ); + mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); mockOptions.client.callAsInternalUser.mockResolvedValue({ access_token: 'access-token' }); - const authenticationResult = await provider.authenticate(request); + await expect(provider.authenticate(request)).resolves.toEqual( + AuthenticationResult.succeeded( + { ...user, authentication_provider: 'pki' }, + { + authHeaders: { authorization: 'Bearer access-token' }, + state: { accessToken: 'access-token', peerCertificateFingerprint256: '2A:7A:C2:DD' }, + } + ) + ); expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledTimes(1); expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.delegatePKI', { body: { x509_certificate_chain: ['fingerprint:2A:7A:C2:DD:base64'] }, }); - expect(mockOptions.client.asScoped).toHaveBeenCalledTimes(1); - expect(mockOptions.client.asScoped).toHaveBeenCalledWith({ - headers: { authorization: `Bearer access-token` }, + expectAuthenticateCall(mockOptions.client, { + headers: { authorization: 'Bearer access-token' }, }); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledTimes(1); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith('shield.authenticate'); expect(request.headers).not.toHaveProperty('authorization'); - expect(authenticationResult.succeeded()).toBe(true); - expect(authenticationResult.user).toEqual({ ...user, authentication_provider: 'pki' }); - expect(authenticationResult.authHeaders).toEqual({ authorization: 'Bearer access-token' }); - expect(authenticationResult.authResponseHeaders).toBeUndefined(); - expect(authenticationResult.state).toEqual({ - accessToken: 'access-token', - peerCertificateFingerprint256: '2A:7A:C2:DD', - }); }); it('invalidates existing token and gets a new one if fingerprints do not match.', async () => { @@ -263,12 +278,18 @@ describe('PKIAuthenticationProvider', () => { const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); mockScopedClusterClient.callAsCurrentUser.mockResolvedValue(user); - mockOptions.client.asScoped.mockReturnValue( - (mockScopedClusterClient as unknown) as jest.Mocked - ); + mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); mockOptions.client.callAsInternalUser.mockResolvedValue({ access_token: 'access-token' }); - const authenticationResult = await provider.authenticate(request, state); + await expect(provider.authenticate(request, state)).resolves.toEqual( + AuthenticationResult.succeeded( + { ...user, authentication_provider: 'pki' }, + { + authHeaders: { authorization: 'Bearer access-token' }, + state: { accessToken: 'access-token', peerCertificateFingerprint256: '2A:7A:C2:DD' }, + } + ) + ); expect(mockOptions.tokens.invalidate).toHaveBeenCalledTimes(1); expect(mockOptions.tokens.invalidate).toHaveBeenCalledWith({ @@ -286,14 +307,6 @@ describe('PKIAuthenticationProvider', () => { }); expect(request.headers).not.toHaveProperty('authorization'); - expect(authenticationResult.succeeded()).toBe(true); - expect(authenticationResult.user).toEqual({ ...user, authentication_provider: 'pki' }); - expect(authenticationResult.authHeaders).toEqual({ authorization: 'Bearer access-token' }); - expect(authenticationResult.authResponseHeaders).toBeUndefined(); - expect(authenticationResult.state).toEqual({ - accessToken: 'access-token', - peerCertificateFingerprint256: '2A:7A:C2:DD', - }); }); it('gets a new access token even if existing token is expired.', async () => { @@ -312,12 +325,18 @@ describe('PKIAuthenticationProvider', () => { .mockRejectedValueOnce(ElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error())) // In response to a call with a new token. .mockResolvedValueOnce(user); - mockOptions.client.asScoped.mockReturnValue( - (mockScopedClusterClient as unknown) as jest.Mocked - ); + mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); mockOptions.client.callAsInternalUser.mockResolvedValue({ access_token: 'access-token' }); - const authenticationResult = await provider.authenticate(request, state); + await expect(provider.authenticate(request, state)).resolves.toEqual( + AuthenticationResult.succeeded( + { ...user, authentication_provider: 'pki' }, + { + authHeaders: { authorization: 'Bearer access-token' }, + state: { accessToken: 'access-token', peerCertificateFingerprint256: '2A:7A:C2:DD' }, + } + ) + ); expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledTimes(1); expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.delegatePKI', { @@ -330,14 +349,6 @@ describe('PKIAuthenticationProvider', () => { }); expect(request.headers).not.toHaveProperty('authorization'); - expect(authenticationResult.succeeded()).toBe(true); - expect(authenticationResult.user).toEqual({ ...user, authentication_provider: 'pki' }); - expect(authenticationResult.authHeaders).toEqual({ authorization: 'Bearer access-token' }); - expect(authenticationResult.authResponseHeaders).toBeUndefined(); - expect(authenticationResult.state).toEqual({ - accessToken: 'access-token', - peerCertificateFingerprint256: '2A:7A:C2:DD', - }); }); it('fails with 401 if existing token is expired, but certificate is not present.', async () => { @@ -348,18 +359,15 @@ describe('PKIAuthenticationProvider', () => { mockScopedClusterClient.callAsCurrentUser.mockRejectedValue( ElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error()) ); - mockOptions.client.asScoped.mockReturnValue( - (mockScopedClusterClient as unknown) as jest.Mocked - ); + mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); - const authenticationResult = await provider.authenticate(request, state); + await expect(provider.authenticate(request, state)).resolves.toEqual( + AuthenticationResult.failed(Boom.unauthorized()) + ); expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled(); expect(request.headers).not.toHaveProperty('authorization'); - expect(authenticationResult.failed()).toBe(true); - expect(getErrorStatusCode(authenticationResult.error)).toBe(401); - expect(authenticationResult.authResponseHeaders).toBeUndefined(); }); it('fails if could not retrieve an access token in exchange to peer certificate chain.', async () => { @@ -373,7 +381,9 @@ describe('PKIAuthenticationProvider', () => { const failureReason = ElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error()); mockOptions.client.callAsInternalUser.mockRejectedValue(failureReason); - const authenticationResult = await provider.authenticate(request); + await expect(provider.authenticate(request)).resolves.toEqual( + AuthenticationResult.failed(failureReason) + ); expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledTimes(1); expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.delegatePKI', { @@ -381,9 +391,6 @@ describe('PKIAuthenticationProvider', () => { }); expect(request.headers).not.toHaveProperty('authorization'); - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.error).toBe(failureReason); - expect(authenticationResult.authResponseHeaders).toBeUndefined(); }); it('fails if could not retrieve user using the new access token.', async () => { @@ -398,35 +405,30 @@ describe('PKIAuthenticationProvider', () => { const failureReason = ElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error()); const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); mockScopedClusterClient.callAsCurrentUser.mockRejectedValue(failureReason); - mockOptions.client.asScoped.mockReturnValue( - (mockScopedClusterClient as unknown) as jest.Mocked - ); + mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); mockOptions.client.callAsInternalUser.mockResolvedValue({ access_token: 'access-token' }); - const authenticationResult = await provider.authenticate(request); + await expect(provider.authenticate(request)).resolves.toEqual( + AuthenticationResult.failed(failureReason) + ); expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledTimes(1); expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.delegatePKI', { body: { x509_certificate_chain: ['fingerprint:2A:7A:C2:DD:base64'] }, }); - expect(mockOptions.client.asScoped).toHaveBeenCalledTimes(1); - expect(mockOptions.client.asScoped).toHaveBeenCalledWith({ - headers: { authorization: `Bearer access-token` }, + expectAuthenticateCall(mockOptions.client, { + headers: { authorization: 'Bearer access-token' }, }); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledTimes(1); - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith('shield.authenticate'); expect(request.headers).not.toHaveProperty('authorization'); - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.error).toBe(failureReason); - expect(authenticationResult.authResponseHeaders).toBeUndefined(); }); it('succeeds if state contains a valid token.', async () => { const user = mockAuthenticatedUser(); const state = { accessToken: 'token', peerCertificateFingerprint256: '2A:7A:C2:DD' }; const request = httpServerMock.createKibanaRequest({ + headers: {}, socket: getMockSocket({ authorized: true, peerCertificate: getMockPeerCertificate(state.peerCertificateFingerprint256), @@ -435,110 +437,42 @@ describe('PKIAuthenticationProvider', () => { const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); mockScopedClusterClient.callAsCurrentUser.mockResolvedValue(user); - mockOptions.client.asScoped.mockReturnValue( - (mockScopedClusterClient as unknown) as jest.Mocked + mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); + + await expect(provider.authenticate(request, state)).resolves.toEqual( + AuthenticationResult.succeeded( + { ...user, authentication_provider: 'pki' }, + { authHeaders: { authorization: `Bearer ${state.accessToken}` } } + ) ); - const authenticationResult = await provider.authenticate(request, state); + expectAuthenticateCall(mockOptions.client, { headers: { authorization: 'Bearer token' } }); expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled(); expect(request.headers).not.toHaveProperty('authorization'); - expect(authenticationResult.succeeded()).toBe(true); - expect(authenticationResult.authHeaders).toEqual({ - authorization: `Bearer ${state.accessToken}`, - }); - expect(authenticationResult.user).toEqual({ ...user, authentication_provider: 'pki' }); - expect(authenticationResult.state).toBeUndefined(); }); it('fails if token from the state is rejected because of unknown reason.', async () => { const state = { accessToken: 'token', peerCertificateFingerprint256: '2A:7A:C2:DD' }; const request = httpServerMock.createKibanaRequest({ + headers: {}, socket: getMockSocket({ authorized: true, peerCertificate: getMockPeerCertificate(state.peerCertificateFingerprint256), }), }); - const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); - mockScopedClusterClient.callAsCurrentUser.mockRejectedValue(new errors.ServiceUnavailable()); - mockOptions.client.asScoped.mockReturnValue( - (mockScopedClusterClient as unknown) as jest.Mocked - ); - - const authenticationResult = await provider.authenticate(request, state); - - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.error).toHaveProperty('status', 503); - expect(authenticationResult.authResponseHeaders).toBeUndefined(); - }); - - it('succeeds if `authorization` contains a valid token.', async () => { - const user = mockAuthenticatedUser(); - const request = httpServerMock.createKibanaRequest({ - headers: { authorization: 'Bearer some-valid-token' }, - }); - - const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); - mockScopedClusterClient.callAsCurrentUser.mockResolvedValue(user); - mockOptions.client.asScoped.mockReturnValue( - (mockScopedClusterClient as unknown) as jest.Mocked - ); - - const authenticationResult = await provider.authenticate(request); - - expect(request.headers.authorization).toBe('Bearer some-valid-token'); - expect(authenticationResult.succeeded()).toBe(true); - expect(authenticationResult.authHeaders).toBeUndefined(); - expect(authenticationResult.user).toEqual({ ...user, authentication_provider: 'pki' }); - expect(authenticationResult.state).toBeUndefined(); - }); - - it('fails if token from `authorization` header is rejected.', async () => { - const request = httpServerMock.createKibanaRequest({ - headers: { authorization: 'Bearer some-invalid-token' }, - }); - - const failureReason = ElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error()); + const failureReason = new errors.ServiceUnavailable(); const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); mockScopedClusterClient.callAsCurrentUser.mockRejectedValue(failureReason); - mockOptions.client.asScoped.mockReturnValue( - (mockScopedClusterClient as unknown) as jest.Mocked - ); + mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); - const authenticationResult = await provider.authenticate(request); - - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.error).toBe(failureReason); - }); - - it('fails if token from `authorization` header is rejected even if state contains a valid one.', async () => { - const user = mockAuthenticatedUser(); - const state = { accessToken: 'token', peerCertificateFingerprint256: '2A:7A:C2:DD' }; - const request = httpServerMock.createKibanaRequest({ - headers: { authorization: 'Bearer some-invalid-token' }, - socket: getMockSocket({ - authorized: true, - peerCertificate: getMockPeerCertificate(state.peerCertificateFingerprint256), - }), - }); - - const failureReason = ElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error()); - const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); - mockScopedClusterClient.callAsCurrentUser - // In response to call with a token from header. - .mockRejectedValueOnce(failureReason) - // In response to a call with a token from session (not expected to be called). - .mockResolvedValueOnce(user); - mockOptions.client.asScoped.mockReturnValue( - (mockScopedClusterClient as unknown) as jest.Mocked + await expect(provider.authenticate(request, state)).resolves.toEqual( + AuthenticationResult.failed(failureReason) ); - const authenticationResult = await provider.authenticate(request, state); - - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.error).toBe(failureReason); + expectAuthenticateCall(mockOptions.client, { headers: { authorization: 'Bearer token' } }); }); }); @@ -546,11 +480,11 @@ describe('PKIAuthenticationProvider', () => { it('returns `notHandled` if state is not presented.', async () => { const request = httpServerMock.createKibanaRequest(); - let deauthenticateResult = await provider.logout(request); - expect(deauthenticateResult.notHandled()).toBe(true); + await expect(provider.logout(request)).resolves.toEqual(DeauthenticationResult.notHandled()); - deauthenticateResult = await provider.logout(request, null); - expect(deauthenticateResult.notHandled()).toBe(true); + await expect(provider.logout(request, null)).resolves.toEqual( + DeauthenticationResult.notHandled() + ); expect(mockOptions.tokens.invalidate).not.toHaveBeenCalled(); }); @@ -562,13 +496,12 @@ describe('PKIAuthenticationProvider', () => { const failureReason = new Error('failed to delete token'); mockOptions.tokens.invalidate.mockRejectedValue(failureReason); - const authenticationResult = await provider.logout(request, state); + await expect(provider.logout(request, state)).resolves.toEqual( + DeauthenticationResult.failed(failureReason) + ); expect(mockOptions.tokens.invalidate).toHaveBeenCalledTimes(1); expect(mockOptions.tokens.invalidate).toHaveBeenCalledWith({ accessToken: 'foo' }); - - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.error).toBe(failureReason); }); it('redirects to `/logged_out` page if access token is invalidated successfully.', async () => { @@ -577,13 +510,16 @@ describe('PKIAuthenticationProvider', () => { mockOptions.tokens.invalidate.mockResolvedValue(undefined); - const authenticationResult = await provider.logout(request, state); + await expect(provider.logout(request, state)).resolves.toEqual( + DeauthenticationResult.redirectTo('/mock-server-basepath/logged_out') + ); expect(mockOptions.tokens.invalidate).toHaveBeenCalledTimes(1); expect(mockOptions.tokens.invalidate).toHaveBeenCalledWith({ accessToken: 'foo' }); - - expect(authenticationResult.redirected()).toBe(true); - expect(authenticationResult.redirectURL).toBe('/mock-server-basepath/logged_out'); }); }); + + it('`getHTTPAuthenticationScheme` method', () => { + expect(provider.getHTTPAuthenticationScheme()).toBe('bearer'); + }); }); diff --git a/x-pack/plugins/security/server/authentication/providers/pki.ts b/x-pack/plugins/security/server/authentication/providers/pki.ts index 6d5aa9f01f2ea..854f92a50fa9d 100644 --- a/x-pack/plugins/security/server/authentication/providers/pki.ts +++ b/x-pack/plugins/security/server/authentication/providers/pki.ts @@ -9,8 +9,9 @@ import { DetailedPeerCertificate } from 'tls'; import { KibanaRequest } from '../../../../../../src/core/server'; import { AuthenticationResult } from '../authentication_result'; import { DeauthenticationResult } from '../deauthentication_result'; -import { BaseAuthenticationProvider } from './base'; +import { getHTTPAuthenticationScheme } from '../get_http_authentication_scheme'; import { Tokens } from '../tokens'; +import { BaseAuthenticationProvider } from './base'; /** * The state supported by the provider. @@ -27,19 +28,6 @@ interface ProviderState { peerCertificateFingerprint256: string; } -/** - * Parses request's `Authorization` HTTP header if present and extracts authentication scheme. - * @param request Request instance to extract authentication scheme for. - */ -function getRequestAuthenticationScheme(request: KibanaRequest) { - const authorization = request.headers.authorization; - if (!authorization || typeof authorization !== 'string') { - return ''; - } - - return authorization.split(/\s+/)[0].toLowerCase(); -} - /** * Provider that supports PKI request authentication. */ @@ -57,19 +45,13 @@ export class PKIAuthenticationProvider extends BaseAuthenticationProvider { public async authenticate(request: KibanaRequest, state?: ProviderState | null) { this.logger.debug(`Trying to authenticate user request to ${request.url.path}.`); - const authenticationScheme = getRequestAuthenticationScheme(request); - if (authenticationScheme && authenticationScheme !== 'bearer') { - this.logger.debug(`Unsupported authentication scheme: ${authenticationScheme}`); + if (getHTTPAuthenticationScheme(request) != null) { + this.logger.debug('Cannot authenticate requests with `Authorization` header.'); return AuthenticationResult.notHandled(); } let authenticationResult = AuthenticationResult.notHandled(); - if (authenticationScheme) { - // We should get rid of `Bearer` scheme support as soon as Reporting doesn't need it anymore. - authenticationResult = await this.authenticateWithBearerScheme(request); - } - - if (state && authenticationResult.notHandled()) { + if (state) { authenticationResult = await this.authenticateViaState(request, state); // If access token expired or doesn't match to the certificate fingerprint we should try to get @@ -120,23 +102,11 @@ export class PKIAuthenticationProvider extends BaseAuthenticationProvider { } /** - * Tries to authenticate request with `Bearer ***` Authorization header by passing it to the Elasticsearch backend. - * @param request Request instance. + * Returns HTTP authentication scheme (`Bearer`) that's used within `Authorization` HTTP header + * that provider attaches to all successfully authenticated requests to Elasticsearch. */ - private async authenticateWithBearerScheme(request: KibanaRequest) { - this.logger.debug('Trying to authenticate request using "Bearer" authentication scheme.'); - - try { - const user = await this.getUser(request); - - this.logger.debug('Request has been authenticated using "Bearer" authentication scheme.'); - return AuthenticationResult.succeeded(user); - } catch (err) { - this.logger.debug( - `Failed to authenticate request using "Bearer" authentication scheme: ${err.message}` - ); - return AuthenticationResult.failed(err); - } + public getHTTPAuthenticationScheme() { + return 'bearer'; } /** diff --git a/x-pack/plugins/security/server/authentication/providers/saml.test.ts b/x-pack/plugins/security/server/authentication/providers/saml.test.ts index c4fdf0b25061b..d97a6c0838b86 100644 --- a/x-pack/plugins/security/server/authentication/providers/saml.test.ts +++ b/x-pack/plugins/security/server/authentication/providers/saml.test.ts @@ -5,19 +5,33 @@ */ import Boom from 'boom'; -import sinon from 'sinon'; import { ByteSizeValue } from '@kbn/config-schema'; -import { httpServerMock } from '../../../../../../src/core/server/mocks'; +import { elasticsearchServiceMock, httpServerMock } from '../../../../../../src/core/server/mocks'; import { mockAuthenticatedUser } from '../../../common/model/authenticated_user.mock'; -import { - MockAuthenticationProviderOptions, - mockAuthenticationProviderOptions, - mockScopedClusterClient, -} from './base.mock'; +import { MockAuthenticationProviderOptions, mockAuthenticationProviderOptions } from './base.mock'; +import { + ElasticsearchErrorHelpers, + IClusterClient, + ScopeableRequest, +} from '../../../../../../src/core/server'; +import { AuthenticationResult } from '../authentication_result'; +import { DeauthenticationResult } from '../deauthentication_result'; import { SAMLAuthenticationProvider, SAMLLoginStep } from './saml'; +function expectAuthenticateCall( + mockClusterClient: jest.Mocked, + scopeableRequest: ScopeableRequest +) { + expect(mockClusterClient.asScoped).toHaveBeenCalledTimes(1); + expect(mockClusterClient.asScoped).toHaveBeenCalledWith(scopeableRequest); + + const mockScopedClusterClient = mockClusterClient.asScoped.mock.results[0].value; + expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledTimes(1); + expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith('shield.authenticate'); +} + describe('SAMLAuthenticationProvider', () => { let provider: SAMLAuthenticationProvider; let mockOptions: MockAuthenticationProviderOptions; @@ -63,294 +77,317 @@ describe('SAMLAuthenticationProvider', () => { it('gets token and redirects user to requested URL if SAML Response is valid.', async () => { const request = httpServerMock.createKibanaRequest(); - mockOptions.client.callAsInternalUser.withArgs('shield.samlAuthenticate').resolves({ + mockOptions.client.callAsInternalUser.mockResolvedValue({ username: 'user', access_token: 'some-token', refresh_token: 'some-refresh-token', }); - const authenticationResult = await provider.login( - request, - { step: SAMLLoginStep.SAMLResponseReceived, samlResponse: 'saml-response-xml' }, - { requestId: 'some-request-id', redirectURL: '/test-base-path/some-path#some-app' } + await expect( + provider.login( + request, + { step: SAMLLoginStep.SAMLResponseReceived, samlResponse: 'saml-response-xml' }, + { requestId: 'some-request-id', redirectURL: '/test-base-path/some-path#some-app' } + ) + ).resolves.toEqual( + AuthenticationResult.redirectTo('/test-base-path/some-path#some-app', { + state: { + username: 'user', + accessToken: 'some-token', + refreshToken: 'some-refresh-token', + }, + }) ); - sinon.assert.calledWithExactly( - mockOptions.client.callAsInternalUser, + expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith( 'shield.samlAuthenticate', { body: { ids: ['some-request-id'], content: 'saml-response-xml', realm: 'test-realm' } } ); - - expect(authenticationResult.redirected()).toBe(true); - expect(authenticationResult.redirectURL).toBe('/test-base-path/some-path#some-app'); - expect(authenticationResult.state).toEqual({ - username: 'user', - accessToken: 'some-token', - refreshToken: 'some-refresh-token', - }); }); it('fails if SAML Response payload is presented but state does not contain SAML Request token.', async () => { const request = httpServerMock.createKibanaRequest(); - const authenticationResult = await provider.login( - request, - { step: SAMLLoginStep.SAMLResponseReceived, samlResponse: 'saml-response-xml' }, - {} + await expect( + provider.login( + request, + { step: SAMLLoginStep.SAMLResponseReceived, samlResponse: 'saml-response-xml' }, + {} + ) + ).resolves.toEqual( + AuthenticationResult.failed( + Boom.badRequest('SAML response state does not have corresponding request id.') + ) ); - sinon.assert.notCalled(mockOptions.client.callAsInternalUser); - - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.error).toEqual( - Boom.badRequest('SAML response state does not have corresponding request id.') - ); + expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled(); }); it('redirects to the default location if state contains empty redirect URL.', async () => { const request = httpServerMock.createKibanaRequest(); - mockOptions.client.callAsInternalUser.withArgs('shield.samlAuthenticate').resolves({ + mockOptions.client.callAsInternalUser.mockResolvedValue({ access_token: 'user-initiated-login-token', refresh_token: 'user-initiated-login-refresh-token', }); - const authenticationResult = await provider.login( - request, - { step: SAMLLoginStep.SAMLResponseReceived, samlResponse: 'saml-response-xml' }, - { requestId: 'some-request-id', redirectURL: '' } + await expect( + provider.login( + request, + { step: SAMLLoginStep.SAMLResponseReceived, samlResponse: 'saml-response-xml' }, + { requestId: 'some-request-id', redirectURL: '' } + ) + ).resolves.toEqual( + AuthenticationResult.redirectTo('/base-path/', { + state: { + accessToken: 'user-initiated-login-token', + refreshToken: 'user-initiated-login-refresh-token', + }, + }) ); - sinon.assert.calledWithExactly( - mockOptions.client.callAsInternalUser, + expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith( 'shield.samlAuthenticate', { body: { ids: ['some-request-id'], content: 'saml-response-xml', realm: 'test-realm' } } ); - - expect(authenticationResult.redirected()).toBe(true); - expect(authenticationResult.redirectURL).toBe('/base-path/'); - expect(authenticationResult.state).toEqual({ - accessToken: 'user-initiated-login-token', - refreshToken: 'user-initiated-login-refresh-token', - }); }); it('redirects to the default location if state is not presented.', async () => { const request = httpServerMock.createKibanaRequest(); - mockOptions.client.callAsInternalUser.withArgs('shield.samlAuthenticate').resolves({ + mockOptions.client.callAsInternalUser.mockResolvedValue({ access_token: 'idp-initiated-login-token', refresh_token: 'idp-initiated-login-refresh-token', }); - const authenticationResult = await provider.login(request, { - step: SAMLLoginStep.SAMLResponseReceived, - samlResponse: 'saml-response-xml', - }); + await expect( + provider.login(request, { + step: SAMLLoginStep.SAMLResponseReceived, + samlResponse: 'saml-response-xml', + }) + ).resolves.toEqual( + AuthenticationResult.redirectTo('/base-path/', { + state: { + accessToken: 'idp-initiated-login-token', + refreshToken: 'idp-initiated-login-refresh-token', + }, + }) + ); - sinon.assert.calledWithExactly( - mockOptions.client.callAsInternalUser, + expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith( 'shield.samlAuthenticate', { body: { ids: [], content: 'saml-response-xml', realm: 'test-realm' } } ); - - expect(authenticationResult.redirected()).toBe(true); - expect(authenticationResult.redirectURL).toBe('/base-path/'); - expect(authenticationResult.state).toEqual({ - accessToken: 'idp-initiated-login-token', - refreshToken: 'idp-initiated-login-refresh-token', - }); }); it('fails if SAML Response is rejected.', async () => { const request = httpServerMock.createKibanaRequest(); const failureReason = new Error('SAML response is stale!'); - mockOptions.client.callAsInternalUser - .withArgs('shield.samlAuthenticate') - .rejects(failureReason); - - const authenticationResult = await provider.login( - request, - { step: SAMLLoginStep.SAMLResponseReceived, samlResponse: 'saml-response-xml' }, - { requestId: 'some-request-id', redirectURL: '/test-base-path/some-path' } - ); + mockOptions.client.callAsInternalUser.mockRejectedValue(failureReason); + + await expect( + provider.login( + request, + { step: SAMLLoginStep.SAMLResponseReceived, samlResponse: 'saml-response-xml' }, + { requestId: 'some-request-id', redirectURL: '/test-base-path/some-path' } + ) + ).resolves.toEqual(AuthenticationResult.failed(failureReason)); - sinon.assert.calledWithExactly( - mockOptions.client.callAsInternalUser, + expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith( 'shield.samlAuthenticate', { body: { ids: ['some-request-id'], content: 'saml-response-xml', realm: 'test-realm' } } ); - - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.error).toBe(failureReason); }); describe('IdP initiated login with existing session', () => { it('fails if new SAML Response is rejected.', async () => { - const request = httpServerMock.createKibanaRequest(); + const request = httpServerMock.createKibanaRequest({ headers: {} }); + const authorization = 'Bearer some-valid-token'; const user = mockAuthenticatedUser(); - mockScopedClusterClient(mockOptions.client) - .callAsCurrentUser.withArgs('shield.authenticate') - .resolves(user); + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.callAsCurrentUser.mockResolvedValue(user); + mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); const failureReason = new Error('SAML response is invalid!'); - mockOptions.client.callAsInternalUser - .withArgs('shield.samlAuthenticate') - .rejects(failureReason); - - const authenticationResult = await provider.login( - request, - { step: SAMLLoginStep.SAMLResponseReceived, samlResponse: 'saml-response-xml' }, + mockOptions.client.callAsInternalUser.mockRejectedValue(failureReason); + + await expect( + provider.login( + request, + { step: SAMLLoginStep.SAMLResponseReceived, samlResponse: 'saml-response-xml' }, + { + username: 'user', + accessToken: 'some-valid-token', + refreshToken: 'some-valid-refresh-token', + } + ) + ).resolves.toEqual(AuthenticationResult.failed(failureReason)); + + expectAuthenticateCall(mockOptions.client, { headers: { authorization } }); + + expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith( + 'shield.samlAuthenticate', { - username: 'user', - accessToken: 'some-valid-token', - refreshToken: 'some-valid-refresh-token', + body: { ids: [], content: 'saml-response-xml', realm: 'test-realm' }, } ); - - sinon.assert.calledWithExactly( - mockOptions.client.callAsInternalUser, - 'shield.samlAuthenticate', - { body: { ids: [], content: 'saml-response-xml', realm: 'test-realm' } } - ); - - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.error).toBe(failureReason); }); it('fails if fails to invalidate existing access/refresh tokens.', async () => { - const request = httpServerMock.createKibanaRequest(); + const request = httpServerMock.createKibanaRequest({ headers: {} }); const state = { username: 'user', accessToken: 'existing-valid-token', refreshToken: 'existing-valid-refresh-token', }; + const authorization = `Bearer ${state.accessToken}`; const user = mockAuthenticatedUser(); - mockScopedClusterClient(mockOptions.client) - .callAsCurrentUser.withArgs('shield.authenticate') - .resolves(user); + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.callAsCurrentUser.mockResolvedValue(user); + mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); - mockOptions.client.callAsInternalUser.withArgs('shield.samlAuthenticate').resolves({ + mockOptions.client.callAsInternalUser.mockResolvedValue({ username: 'user', access_token: 'new-valid-token', refresh_token: 'new-valid-refresh-token', }); const failureReason = new Error('Failed to invalidate token!'); - mockOptions.tokens.invalidate.rejects(failureReason); + mockOptions.tokens.invalidate.mockRejectedValue(failureReason); - const authenticationResult = await provider.login( - request, - { step: SAMLLoginStep.SAMLResponseReceived, samlResponse: 'saml-response-xml' }, - state - ); + await expect( + provider.login( + request, + { step: SAMLLoginStep.SAMLResponseReceived, samlResponse: 'saml-response-xml' }, + state + ) + ).resolves.toEqual(AuthenticationResult.failed(failureReason)); - sinon.assert.calledWithExactly( - mockOptions.client.callAsInternalUser, + expectAuthenticateCall(mockOptions.client, { headers: { authorization } }); + + expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith( 'shield.samlAuthenticate', - { body: { ids: [], content: 'saml-response-xml', realm: 'test-realm' } } + { + body: { ids: [], content: 'saml-response-xml', realm: 'test-realm' }, + } ); - sinon.assert.calledOnce(mockOptions.tokens.invalidate); - sinon.assert.calledWithExactly(mockOptions.tokens.invalidate, { + expect(mockOptions.tokens.invalidate).toHaveBeenCalledTimes(1); + expect(mockOptions.tokens.invalidate).toHaveBeenCalledWith({ accessToken: state.accessToken, refreshToken: state.refreshToken, }); - - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.error).toBe(failureReason); }); it('redirects to the home page if new SAML Response is for the same user.', async () => { - const request = httpServerMock.createKibanaRequest(); + const request = httpServerMock.createKibanaRequest({ headers: {} }); const state = { username: 'user', accessToken: 'existing-valid-token', refreshToken: 'existing-valid-refresh-token', }; + const authorization = `Bearer ${state.accessToken}`; const user = { username: 'user' }; - mockScopedClusterClient(mockOptions.client) - .callAsCurrentUser.withArgs('shield.authenticate') - .resolves(user); + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.callAsCurrentUser.mockResolvedValue(user); + mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); - mockOptions.client.callAsInternalUser.withArgs('shield.samlAuthenticate').resolves({ + mockOptions.client.callAsInternalUser.mockResolvedValue({ username: 'user', access_token: 'new-valid-token', refresh_token: 'new-valid-refresh-token', }); - mockOptions.tokens.invalidate.resolves(); - - const authenticationResult = await provider.login( - request, - { step: SAMLLoginStep.SAMLResponseReceived, samlResponse: 'saml-response-xml' }, - state + mockOptions.tokens.invalidate.mockResolvedValue(undefined); + + await expect( + provider.login( + request, + { step: SAMLLoginStep.SAMLResponseReceived, samlResponse: 'saml-response-xml' }, + state + ) + ).resolves.toEqual( + AuthenticationResult.redirectTo('/base-path/', { + state: { + username: 'user', + accessToken: 'new-valid-token', + refreshToken: 'new-valid-refresh-token', + }, + }) ); - sinon.assert.calledWithExactly( - mockOptions.client.callAsInternalUser, + expectAuthenticateCall(mockOptions.client, { headers: { authorization } }); + + expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith( 'shield.samlAuthenticate', - { body: { ids: [], content: 'saml-response-xml', realm: 'test-realm' } } + { + body: { ids: [], content: 'saml-response-xml', realm: 'test-realm' }, + } ); - sinon.assert.calledOnce(mockOptions.tokens.invalidate); - sinon.assert.calledWithExactly(mockOptions.tokens.invalidate, { + expect(mockOptions.tokens.invalidate).toHaveBeenCalledTimes(1); + expect(mockOptions.tokens.invalidate).toHaveBeenCalledWith({ accessToken: state.accessToken, refreshToken: state.refreshToken, }); - - expect(authenticationResult.redirected()).toBe(true); - expect(authenticationResult.redirectURL).toBe('/base-path/'); }); it('redirects to `overwritten_session` if new SAML Response is for the another user.', async () => { - const request = httpServerMock.createKibanaRequest(); + const request = httpServerMock.createKibanaRequest({ headers: {} }); const state = { username: 'user', accessToken: 'existing-valid-token', refreshToken: 'existing-valid-refresh-token', }; + const authorization = `Bearer ${state.accessToken}`; const existingUser = { username: 'user' }; - mockScopedClusterClient( - mockOptions.client, - sinon.match({ headers: { authorization: `Bearer ${state.accessToken}` } }) - ) - .callAsCurrentUser.withArgs('shield.authenticate') - .resolves(existingUser); + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.callAsCurrentUser.mockResolvedValue(existingUser); + mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); - mockOptions.client.callAsInternalUser.withArgs('shield.samlAuthenticate').resolves({ + mockOptions.client.callAsInternalUser.mockResolvedValue({ username: 'new-user', access_token: 'new-valid-token', refresh_token: 'new-valid-refresh-token', }); - mockOptions.tokens.invalidate.resolves(); - - const authenticationResult = await provider.login( - request, - { step: SAMLLoginStep.SAMLResponseReceived, samlResponse: 'saml-response-xml' }, - state + mockOptions.tokens.invalidate.mockResolvedValue(undefined); + + await expect( + provider.login( + request, + { step: SAMLLoginStep.SAMLResponseReceived, samlResponse: 'saml-response-xml' }, + state + ) + ).resolves.toEqual( + AuthenticationResult.redirectTo('/base-path/overwritten_session', { + state: { + username: 'new-user', + accessToken: 'new-valid-token', + refreshToken: 'new-valid-refresh-token', + }, + }) ); - sinon.assert.calledWithExactly( - mockOptions.client.callAsInternalUser, + expectAuthenticateCall(mockOptions.client, { headers: { authorization } }); + + expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith( 'shield.samlAuthenticate', - { body: { ids: [], content: 'saml-response-xml', realm: 'test-realm' } } + { + body: { ids: [], content: 'saml-response-xml', realm: 'test-realm' }, + } ); - sinon.assert.calledOnce(mockOptions.tokens.invalidate); - sinon.assert.calledWithExactly(mockOptions.tokens.invalidate, { + expect(mockOptions.tokens.invalidate).toHaveBeenCalledTimes(1); + expect(mockOptions.tokens.invalidate).toHaveBeenCalledWith({ accessToken: state.accessToken, refreshToken: state.refreshToken, }); - - expect(authenticationResult.redirected()).toBe(true); - expect(authenticationResult.redirectURL).toBe('/base-path/overwritten_session'); }); }); @@ -358,170 +395,171 @@ describe('SAMLAuthenticationProvider', () => { it('fails if state is not available', async () => { const request = httpServerMock.createKibanaRequest(); - const authenticationResult = await provider.login(request, { - step: SAMLLoginStep.RedirectURLFragmentCaptured, - redirectURLFragment: '#some-fragment', - }); - - sinon.assert.notCalled(mockOptions.client.callAsInternalUser); - - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.error).toEqual( - Boom.badRequest('State does not include URL path to redirect to.') + await expect( + provider.login(request, { + step: SAMLLoginStep.RedirectURLFragmentCaptured, + redirectURLFragment: '#some-fragment', + }) + ).resolves.toEqual( + AuthenticationResult.failed( + Boom.badRequest('State does not include URL path to redirect to.') + ) ); + + expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled(); }); it('does not handle AJAX requests.', async () => { const request = httpServerMock.createKibanaRequest({ headers: { 'kbn-xsrf': 'xsrf' } }); - const authenticationResult = await provider.login( - request, - { - step: SAMLLoginStep.RedirectURLFragmentCaptured, - redirectURLFragment: '#some-fragment', - }, - { redirectURL: '/test-base-path/some-path' } - ); - - sinon.assert.notCalled(mockOptions.client.callAsInternalUser); - - expect(authenticationResult.notHandled()).toBe(true); + await expect( + provider.login( + request, + { + step: SAMLLoginStep.RedirectURLFragmentCaptured, + redirectURLFragment: '#some-fragment', + }, + { redirectURL: '/test-base-path/some-path' } + ) + ).resolves.toEqual(AuthenticationResult.notHandled()); + + expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled(); }); it('redirects non-AJAX requests to the IdP remembering combined redirect URL.', async () => { const request = httpServerMock.createKibanaRequest(); - mockOptions.client.callAsInternalUser.withArgs('shield.samlPrepare').resolves({ + mockOptions.client.callAsInternalUser.mockResolvedValue({ id: 'some-request-id', redirect: 'https://idp-host/path/login?SAMLRequest=some%20request%20', }); - const authenticationResult = await provider.login( - request, - { - step: SAMLLoginStep.RedirectURLFragmentCaptured, - redirectURLFragment: '#some-fragment', - }, - { redirectURL: '/test-base-path/some-path' } + await expect( + provider.login( + request, + { + step: SAMLLoginStep.RedirectURLFragmentCaptured, + redirectURLFragment: '#some-fragment', + }, + { redirectURL: '/test-base-path/some-path' } + ) + ).resolves.toEqual( + AuthenticationResult.redirectTo( + 'https://idp-host/path/login?SAMLRequest=some%20request%20', + { + state: { + requestId: 'some-request-id', + redirectURL: '/test-base-path/some-path#some-fragment', + }, + } + ) ); - sinon.assert.calledWithExactly( - mockOptions.client.callAsInternalUser, - 'shield.samlPrepare', - { body: { realm: 'test-realm' } } - ); + expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.samlPrepare', { + body: { realm: 'test-realm' }, + }); expect(mockOptions.logger.warn).not.toHaveBeenCalled(); - - expect(authenticationResult.redirected()).toBe(true); - expect(authenticationResult.redirectURL).toBe( - 'https://idp-host/path/login?SAMLRequest=some%20request%20' - ); - expect(authenticationResult.state).toEqual({ - requestId: 'some-request-id', - redirectURL: '/test-base-path/some-path#some-fragment', - }); }); it('prepends redirect URL fragment with `#` if it does not have one.', async () => { const request = httpServerMock.createKibanaRequest(); - mockOptions.client.callAsInternalUser.withArgs('shield.samlPrepare').resolves({ + mockOptions.client.callAsInternalUser.mockResolvedValue({ id: 'some-request-id', redirect: 'https://idp-host/path/login?SAMLRequest=some%20request%20', }); - const authenticationResult = await provider.login( - request, - { - step: SAMLLoginStep.RedirectURLFragmentCaptured, - redirectURLFragment: '../some-fragment', - }, - { redirectURL: '/test-base-path/some-path' } + await expect( + provider.login( + request, + { + step: SAMLLoginStep.RedirectURLFragmentCaptured, + redirectURLFragment: '../some-fragment', + }, + { redirectURL: '/test-base-path/some-path' } + ) + ).resolves.toEqual( + AuthenticationResult.redirectTo( + 'https://idp-host/path/login?SAMLRequest=some%20request%20', + { + state: { + requestId: 'some-request-id', + redirectURL: '/test-base-path/some-path#../some-fragment', + }, + } + ) ); - sinon.assert.calledWithExactly( - mockOptions.client.callAsInternalUser, - 'shield.samlPrepare', - { body: { realm: 'test-realm' } } - ); + expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.samlPrepare', { + body: { realm: 'test-realm' }, + }); expect(mockOptions.logger.warn).toHaveBeenCalledTimes(1); expect(mockOptions.logger.warn).toHaveBeenCalledWith( 'Redirect URL fragment does not start with `#`.' ); - - expect(authenticationResult.redirected()).toBe(true); - expect(authenticationResult.redirectURL).toBe( - 'https://idp-host/path/login?SAMLRequest=some%20request%20' - ); - expect(authenticationResult.state).toEqual({ - requestId: 'some-request-id', - redirectURL: '/test-base-path/some-path#../some-fragment', - }); }); it('redirects non-AJAX requests to the IdP remembering only redirect URL path if fragment is too large.', async () => { const request = httpServerMock.createKibanaRequest(); - mockOptions.client.callAsInternalUser.withArgs('shield.samlPrepare').resolves({ + mockOptions.client.callAsInternalUser.mockResolvedValue({ id: 'some-request-id', redirect: 'https://idp-host/path/login?SAMLRequest=some%20request%20', }); - const authenticationResult = await provider.login( - request, - { - step: SAMLLoginStep.RedirectURLFragmentCaptured, - redirectURLFragment: '#some-fragment'.repeat(10), - }, - { redirectURL: '/test-base-path/some-path' } + await expect( + provider.login( + request, + { + step: SAMLLoginStep.RedirectURLFragmentCaptured, + redirectURLFragment: '#some-fragment'.repeat(10), + }, + { redirectURL: '/test-base-path/some-path' } + ) + ).resolves.toEqual( + AuthenticationResult.redirectTo( + 'https://idp-host/path/login?SAMLRequest=some%20request%20', + { + state: { + requestId: 'some-request-id', + redirectURL: '/test-base-path/some-path', + }, + } + ) ); - sinon.assert.calledWithExactly( - mockOptions.client.callAsInternalUser, - 'shield.samlPrepare', - { body: { realm: 'test-realm' } } - ); + expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.samlPrepare', { + body: { realm: 'test-realm' }, + }); expect(mockOptions.logger.warn).toHaveBeenCalledTimes(1); expect(mockOptions.logger.warn).toHaveBeenCalledWith( 'Max URL size should not exceed 100b but it was 165b. Only URL path is captured.' ); - - expect(authenticationResult.redirected()).toBe(true); - expect(authenticationResult.redirectURL).toBe( - 'https://idp-host/path/login?SAMLRequest=some%20request%20' - ); - expect(authenticationResult.state).toEqual({ - requestId: 'some-request-id', - redirectURL: '/test-base-path/some-path', - }); }); it('fails if SAML request preparation fails.', async () => { const request = httpServerMock.createKibanaRequest(); const failureReason = new Error('Realm is misconfigured!'); - mockOptions.client.callAsInternalUser.withArgs('shield.samlPrepare').rejects(failureReason); - - const authenticationResult = await provider.login( - request, - { - step: SAMLLoginStep.RedirectURLFragmentCaptured, - redirectURLFragment: '#some-fragment', - }, - { redirectURL: '/test-base-path/some-path' } - ); - - sinon.assert.calledWithExactly( - mockOptions.client.callAsInternalUser, - 'shield.samlPrepare', - { body: { realm: 'test-realm' } } - ); - - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.error).toBe(failureReason); + mockOptions.client.callAsInternalUser.mockRejectedValue(failureReason); + + await expect( + provider.login( + request, + { + step: SAMLLoginStep.RedirectURLFragmentCaptured, + redirectURLFragment: '#some-fragment', + }, + { redirectURL: '/test-base-path/some-path' } + ) + ).resolves.toEqual(AuthenticationResult.failed(failureReason)); + + expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.samlPrepare', { + body: { realm: 'test-realm' }, + }); }); }); }); @@ -530,44 +568,57 @@ describe('SAMLAuthenticationProvider', () => { it('does not handle AJAX request that can not be authenticated.', async () => { const request = httpServerMock.createKibanaRequest({ headers: { 'kbn-xsrf': 'xsrf' } }); - const authenticationResult = await provider.authenticate(request, null); - - expect(authenticationResult.notHandled()).toBe(true); + await expect(provider.authenticate(request, null)).resolves.toEqual( + AuthenticationResult.notHandled() + ); }); - it('does not handle `authorization` header with unsupported schema even if state contains a valid token.', async () => { + it('does not handle authentication via `authorization` header.', async () => { const request = httpServerMock.createKibanaRequest({ - headers: { authorization: 'Basic some:credentials' }, + headers: { authorization: 'Bearer some-token' }, }); - const authenticationResult = await provider.authenticate(request, { - username: 'user', - accessToken: 'some-valid-token', - refreshToken: 'some-valid-refresh-token', + await expect(provider.authenticate(request)).resolves.toEqual( + AuthenticationResult.notHandled() + ); + + expect(mockOptions.client.asScoped).not.toHaveBeenCalled(); + expect(request.headers.authorization).toBe('Bearer some-token'); + }); + + it('does not handle authentication via `authorization` header even if state contains a valid token.', async () => { + const request = httpServerMock.createKibanaRequest({ + headers: { authorization: 'Bearer some-token' }, }); - sinon.assert.notCalled(mockOptions.client.asScoped); - expect(request.headers.authorization).toBe('Basic some:credentials'); - expect(authenticationResult.notHandled()).toBe(true); + await expect( + provider.authenticate(request, { + username: 'user', + accessToken: 'some-valid-token', + refreshToken: 'some-valid-refresh-token', + }) + ).resolves.toEqual(AuthenticationResult.notHandled()); + + expect(mockOptions.client.asScoped).not.toHaveBeenCalled(); + expect(request.headers.authorization).toBe('Bearer some-token'); }); it('redirects non-AJAX request that can not be authenticated to the "capture fragment" page.', async () => { const request = httpServerMock.createKibanaRequest({ path: '/s/foo/some-path' }); - mockOptions.client.callAsInternalUser.withArgs('shield.samlPrepare').resolves({ + mockOptions.client.callAsInternalUser.mockResolvedValue({ id: 'some-request-id', redirect: 'https://idp-host/path/login?SAMLRequest=some%20request%20', }); - const authenticationResult = await provider.authenticate(request); - - sinon.assert.notCalled(mockOptions.client.callAsInternalUser); - - expect(authenticationResult.redirected()).toBe(true); - expect(authenticationResult.redirectURL).toBe( - '/mock-server-basepath/api/security/saml/capture-url-fragment' + await expect(provider.authenticate(request)).resolves.toEqual( + AuthenticationResult.redirectTo( + '/mock-server-basepath/api/security/saml/capture-url-fragment', + { state: { redirectURL: '/base-path/s/foo/some-path' } } + ) ); - expect(authenticationResult.state).toEqual({ redirectURL: '/base-path/s/foo/some-path' }); + + expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled(); }); it('redirects non-AJAX request that can not be authenticated to the IdP if request path is too large.', async () => { @@ -575,14 +626,19 @@ describe('SAMLAuthenticationProvider', () => { path: `/s/foo/${'some-path'.repeat(10)}`, }); - mockOptions.client.callAsInternalUser.withArgs('shield.samlPrepare').resolves({ + mockOptions.client.callAsInternalUser.mockResolvedValue({ id: 'some-request-id', redirect: 'https://idp-host/path/login?SAMLRequest=some%20request%20', }); - const authenticationResult = await provider.authenticate(request); + await expect(provider.authenticate(request)).resolves.toEqual( + AuthenticationResult.redirectTo( + 'https://idp-host/path/login?SAMLRequest=some%20request%20', + { state: { requestId: 'some-request-id', redirectURL: '' } } + ) + ); - sinon.assert.calledWithExactly(mockOptions.client.callAsInternalUser, 'shield.samlPrepare', { + expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.samlPrepare', { body: { realm: 'test-realm' }, }); @@ -590,12 +646,6 @@ describe('SAMLAuthenticationProvider', () => { expect(mockOptions.logger.warn).toHaveBeenCalledWith( 'Max URL path size should not exceed 100b but it was 107b. URL is not captured.' ); - - expect(authenticationResult.redirected()).toBe(true); - expect(authenticationResult.redirectURL).toBe( - 'https://idp-host/path/login?SAMLRequest=some%20request%20' - ); - expect(authenticationResult.state).toEqual({ requestId: 'some-request-id', redirectURL: '' }); }); it('fails if SAML request preparation fails.', async () => { @@ -604,21 +654,20 @@ describe('SAMLAuthenticationProvider', () => { }); const failureReason = new Error('Realm is misconfigured!'); - mockOptions.client.callAsInternalUser.withArgs('shield.samlPrepare').rejects(failureReason); + mockOptions.client.callAsInternalUser.mockRejectedValue(failureReason); - const authenticationResult = await provider.authenticate(request, null); + await expect(provider.authenticate(request, null)).resolves.toEqual( + AuthenticationResult.failed(failureReason) + ); - sinon.assert.calledWithExactly(mockOptions.client.callAsInternalUser, 'shield.samlPrepare', { + expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.samlPrepare', { body: { realm: 'test-realm' }, }); - - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.error).toBe(failureReason); }); it('succeeds if state contains a valid token.', async () => { const user = mockAuthenticatedUser(); - const request = httpServerMock.createKibanaRequest(); + const request = httpServerMock.createKibanaRequest({ headers: {} }); const state = { username: 'user', accessToken: 'some-valid-token', @@ -626,40 +675,43 @@ describe('SAMLAuthenticationProvider', () => { }; const authorization = `Bearer ${state.accessToken}`; - mockScopedClusterClient(mockOptions.client, sinon.match({ headers: { authorization } })) - .callAsCurrentUser.withArgs('shield.authenticate') - .resolves(user); + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.callAsCurrentUser.mockResolvedValue(user); + mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); + + await expect(provider.authenticate(request, state)).resolves.toEqual( + AuthenticationResult.succeeded( + { ...user, authentication_provider: 'saml' }, + { authHeaders: { authorization } } + ) + ); - const authenticationResult = await provider.authenticate(request, state); + expectAuthenticateCall(mockOptions.client, { headers: { authorization } }); expect(request.headers).not.toHaveProperty('authorization'); - expect(authenticationResult.succeeded()).toBe(true); - expect(authenticationResult.authHeaders).toEqual({ authorization }); - expect(authenticationResult.user).toEqual({ ...user, authentication_provider: 'saml' }); - expect(authenticationResult.state).toBeUndefined(); }); it('fails if token from the state is rejected because of unknown reason.', async () => { - const request = httpServerMock.createKibanaRequest(); + const request = httpServerMock.createKibanaRequest({ headers: {} }); const state = { username: 'user', accessToken: 'some-valid-token', refreshToken: 'some-valid-refresh-token', }; + const authorization = `Bearer ${state.accessToken}`; const failureReason = { statusCode: 500, message: 'Token is not valid!' }; - mockScopedClusterClient( - mockOptions.client, - sinon.match({ headers: { authorization: `Bearer ${state.accessToken}` } }) - ) - .callAsCurrentUser.withArgs('shield.authenticate') - .rejects(failureReason); + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.callAsCurrentUser.mockRejectedValue(failureReason); + mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); + + await expect(provider.authenticate(request, state)).resolves.toEqual( + AuthenticationResult.failed(failureReason as any) + ); - const authenticationResult = await provider.authenticate(request, state); + expectAuthenticateCall(mockOptions.client, { headers: { authorization } }); expect(request.headers).not.toHaveProperty('authorization'); - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.error).toBe(failureReason); }); it('succeeds if token from the state is expired, but has been successfully refreshed.', async () => { @@ -671,65 +723,80 @@ describe('SAMLAuthenticationProvider', () => { refreshToken: 'valid-refresh-token', }; - mockScopedClusterClient( - mockOptions.client, - sinon.match({ headers: { authorization: `Bearer ${state.accessToken}` } }) - ) - .callAsCurrentUser.withArgs('shield.authenticate') - .rejects({ statusCode: 401 }); + mockOptions.client.asScoped.mockImplementation(scopeableRequest => { + if (scopeableRequest?.headers.authorization === `Bearer ${state.accessToken}`) { + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.callAsCurrentUser.mockRejectedValue( + ElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error()) + ); + return mockScopedClusterClient; + } + + if (scopeableRequest?.headers.authorization === 'Bearer new-access-token') { + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.callAsCurrentUser.mockResolvedValue(user); + return mockScopedClusterClient; + } + + throw new Error('Unexpected call'); + }); - mockScopedClusterClient( - mockOptions.client, - sinon.match({ headers: { authorization: 'Bearer new-access-token' } }) - ) - .callAsCurrentUser.withArgs('shield.authenticate') - .resolves(user); + mockOptions.tokens.refresh.mockResolvedValue({ + accessToken: 'new-access-token', + refreshToken: 'new-refresh-token', + }); - mockOptions.tokens.refresh - .withArgs(state.refreshToken) - .resolves({ accessToken: 'new-access-token', refreshToken: 'new-refresh-token' }); + await expect(provider.authenticate(request, state)).resolves.toEqual( + AuthenticationResult.succeeded( + { ...user, authentication_provider: 'saml' }, + { + authHeaders: { authorization: 'Bearer new-access-token' }, + state: { + username: 'user', + accessToken: 'new-access-token', + refreshToken: 'new-refresh-token', + }, + } + ) + ); - const authenticationResult = await provider.authenticate(request, state); + expect(mockOptions.tokens.refresh).toHaveBeenCalledTimes(1); + expect(mockOptions.tokens.refresh).toHaveBeenCalledWith(state.refreshToken); expect(request.headers).not.toHaveProperty('authorization'); - expect(authenticationResult.succeeded()).toBe(true); - expect(authenticationResult.authHeaders).toEqual({ - authorization: 'Bearer new-access-token', - }); - expect(authenticationResult.user).toEqual({ ...user, authentication_provider: 'saml' }); - expect(authenticationResult.state).toEqual({ - username: 'user', - accessToken: 'new-access-token', - refreshToken: 'new-refresh-token', - }); }); it('fails if token from the state is expired and refresh attempt failed with unknown reason too.', async () => { - const request = httpServerMock.createKibanaRequest(); + const request = httpServerMock.createKibanaRequest({ headers: {} }); const state = { username: 'user', accessToken: 'expired-token', refreshToken: 'invalid-refresh-token', }; + const authorization = `Bearer ${state.accessToken}`; - mockScopedClusterClient( - mockOptions.client, - sinon.match({ headers: { authorization: `Bearer ${state.accessToken}` } }) - ) - .callAsCurrentUser.withArgs('shield.authenticate') - .rejects({ statusCode: 401 }); + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.callAsCurrentUser.mockRejectedValue( + ElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error()) + ); + mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); const refreshFailureReason = { statusCode: 500, message: 'Something is wrong with refresh token.', }; - mockOptions.tokens.refresh.withArgs(state.refreshToken).rejects(refreshFailureReason); + mockOptions.tokens.refresh.mockRejectedValue(refreshFailureReason); - const authenticationResult = await provider.authenticate(request, state); + await expect(provider.authenticate(request, state)).resolves.toEqual( + AuthenticationResult.failed(refreshFailureReason as any) + ); + + expect(mockOptions.tokens.refresh).toHaveBeenCalledTimes(1); + expect(mockOptions.tokens.refresh).toHaveBeenCalledWith(state.refreshToken); + + expectAuthenticateCall(mockOptions.client, { headers: { authorization } }); expect(request.headers).not.toHaveProperty('authorization'); - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.error).toBe(refreshFailureReason); }); it('fails for AJAX requests with user friendly message if refresh token is expired.', async () => { @@ -739,80 +806,100 @@ describe('SAMLAuthenticationProvider', () => { accessToken: 'expired-token', refreshToken: 'expired-refresh-token', }; + const authorization = `Bearer ${state.accessToken}`; - mockScopedClusterClient( - mockOptions.client, - sinon.match({ headers: { authorization: `Bearer ${state.accessToken}` } }) - ) - .callAsCurrentUser.withArgs('shield.authenticate') - .rejects({ statusCode: 401 }); + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.callAsCurrentUser.mockRejectedValue( + ElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error()) + ); + mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); + + mockOptions.tokens.refresh.mockResolvedValue(null); + + await expect(provider.authenticate(request, state)).resolves.toEqual( + AuthenticationResult.failed(Boom.badRequest('Both access and refresh tokens are expired.')) + ); - mockOptions.tokens.refresh.withArgs(state.refreshToken).resolves(null); + expect(mockOptions.tokens.refresh).toHaveBeenCalledTimes(1); + expect(mockOptions.tokens.refresh).toHaveBeenCalledWith(state.refreshToken); - const authenticationResult = await provider.authenticate(request, state); + expectAuthenticateCall(mockOptions.client, { + headers: { 'kbn-xsrf': 'xsrf', authorization }, + }); expect(request.headers).not.toHaveProperty('authorization'); - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.error).toEqual( - Boom.badRequest('Both access and refresh tokens are expired.') - ); }); it('re-capture URL for non-AJAX requests if refresh token is expired.', async () => { - const request = httpServerMock.createKibanaRequest({ path: '/s/foo/some-path' }); + const request = httpServerMock.createKibanaRequest({ path: '/s/foo/some-path', headers: {} }); const state = { username: 'user', accessToken: 'expired-token', refreshToken: 'expired-refresh-token', }; + const authorization = `Bearer ${state.accessToken}`; - mockScopedClusterClient( - mockOptions.client, - sinon.match({ headers: { authorization: `Bearer ${state.accessToken}` } }) - ) - .callAsCurrentUser.withArgs('shield.authenticate') - .rejects({ statusCode: 401 }); + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.callAsCurrentUser.mockRejectedValue( + ElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error()) + ); + mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); - mockOptions.tokens.refresh.withArgs(state.refreshToken).resolves(null); + mockOptions.tokens.refresh.mockResolvedValue(null); - const authenticationResult = await provider.authenticate(request, state); + await expect(provider.authenticate(request, state)).resolves.toEqual( + AuthenticationResult.redirectTo( + '/mock-server-basepath/api/security/saml/capture-url-fragment', + { state: { redirectURL: '/base-path/s/foo/some-path' } } + ) + ); - sinon.assert.notCalled(mockOptions.client.callAsInternalUser); + expect(mockOptions.tokens.refresh).toHaveBeenCalledTimes(1); + expect(mockOptions.tokens.refresh).toHaveBeenCalledWith(state.refreshToken); - expect(authenticationResult.redirected()).toBe(true); - expect(authenticationResult.redirectURL).toBe( - '/mock-server-basepath/api/security/saml/capture-url-fragment' - ); - expect(authenticationResult.state).toEqual({ redirectURL: '/base-path/s/foo/some-path' }); + expectAuthenticateCall(mockOptions.client, { headers: { authorization } }); + + expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled(); }); it('initiates SAML handshake for non-AJAX requests if refresh token is expired and request path is too large.', async () => { const request = httpServerMock.createKibanaRequest({ path: `/s/foo/${'some-path'.repeat(10)}`, + headers: {}, }); const state = { username: 'user', accessToken: 'expired-token', refreshToken: 'expired-refresh-token', }; + const authorization = `Bearer ${state.accessToken}`; - mockOptions.client.callAsInternalUser.withArgs('shield.samlPrepare').resolves({ + mockOptions.client.callAsInternalUser.mockResolvedValue({ id: 'some-request-id', redirect: 'https://idp-host/path/login?SAMLRequest=some%20request%20', }); - mockScopedClusterClient( - mockOptions.client, - sinon.match({ headers: { authorization: `Bearer ${state.accessToken}` } }) - ) - .callAsCurrentUser.withArgs('shield.authenticate') - .rejects({ statusCode: 401 }); + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.callAsCurrentUser.mockRejectedValue( + ElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error()) + ); + mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); - mockOptions.tokens.refresh.withArgs(state.refreshToken).resolves(null); + mockOptions.tokens.refresh.mockResolvedValue(null); - const authenticationResult = await provider.authenticate(request, state); + await expect(provider.authenticate(request, state)).resolves.toEqual( + AuthenticationResult.redirectTo( + 'https://idp-host/path/login?SAMLRequest=some%20request%20', + { state: { requestId: 'some-request-id', redirectURL: '' } } + ) + ); + + expect(mockOptions.tokens.refresh).toHaveBeenCalledTimes(1); + expect(mockOptions.tokens.refresh).toHaveBeenCalledWith(state.refreshToken); - sinon.assert.calledWithExactly(mockOptions.client.callAsInternalUser, 'shield.samlPrepare', { + expectAuthenticateCall(mockOptions.client, { headers: { authorization } }); + + expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.samlPrepare', { body: { realm: 'test-realm' }, }); @@ -820,71 +907,6 @@ describe('SAMLAuthenticationProvider', () => { expect(mockOptions.logger.warn).toHaveBeenCalledWith( 'Max URL path size should not exceed 100b but it was 107b. URL is not captured.' ); - - expect(authenticationResult.redirected()).toBe(true); - expect(authenticationResult.redirectURL).toBe( - 'https://idp-host/path/login?SAMLRequest=some%20request%20' - ); - expect(authenticationResult.state).toEqual({ requestId: 'some-request-id', redirectURL: '' }); - }); - - it('succeeds if `authorization` contains a valid token.', async () => { - const user = mockAuthenticatedUser(); - const authorization = 'Bearer some-valid-token'; - const request = httpServerMock.createKibanaRequest({ headers: { authorization } }); - - mockScopedClusterClient(mockOptions.client, sinon.match({ headers: { authorization } })) - .callAsCurrentUser.withArgs('shield.authenticate') - .resolves(user); - - const authenticationResult = await provider.authenticate(request); - - expect(request.headers.authorization).toBe('Bearer some-valid-token'); - expect(authenticationResult.succeeded()).toBe(true); - expect(authenticationResult.authHeaders).toBeUndefined(); - expect(authenticationResult.user).toEqual({ ...user, authentication_provider: 'saml' }); - expect(authenticationResult.state).toBeUndefined(); - }); - - it('fails if token from `authorization` header is rejected.', async () => { - const authorization = 'Bearer some-invalid-token'; - const request = httpServerMock.createKibanaRequest({ headers: { authorization } }); - - const failureReason = { statusCode: 401 }; - mockScopedClusterClient(mockOptions.client, sinon.match({ headers: { authorization } })) - .callAsCurrentUser.withArgs('shield.authenticate') - .rejects(failureReason); - - const authenticationResult = await provider.authenticate(request); - - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.error).toBe(failureReason); - }); - - it('fails if token from `authorization` header is rejected even if state contains a valid one.', async () => { - const user = mockAuthenticatedUser(); - const authorization = 'Bearer some-invalid-token'; - const request = httpServerMock.createKibanaRequest({ headers: { authorization } }); - - const failureReason = { statusCode: 401 }; - mockScopedClusterClient(mockOptions.client, sinon.match({ headers: { authorization } })) - .callAsCurrentUser.withArgs('shield.authenticate') - .rejects(failureReason); - - mockScopedClusterClient( - mockOptions.client, - sinon.match({ headers: { authorization: 'Bearer some-valid-token' } }) - ) - .callAsCurrentUser.withArgs('shield.authenticate') - .resolves(user); - - const authenticationResult = await provider.authenticate(request, { - accessToken: 'some-valid-token', - refreshToken: 'some-valid-refresh-token', - }); - - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.error).toBe(failureReason); }); }); @@ -892,16 +914,15 @@ describe('SAMLAuthenticationProvider', () => { it('returns `notHandled` if state is not presented or does not include access token.', async () => { const request = httpServerMock.createKibanaRequest(); - let deauthenticateResult = await provider.logout(request); - expect(deauthenticateResult.notHandled()).toBe(true); - - deauthenticateResult = await provider.logout(request, {} as any); - expect(deauthenticateResult.notHandled()).toBe(true); - - deauthenticateResult = await provider.logout(request, { somethingElse: 'x' } as any); - expect(deauthenticateResult.notHandled()).toBe(true); + await expect(provider.logout(request)).resolves.toEqual(DeauthenticationResult.notHandled()); + await expect(provider.logout(request, {} as any)).resolves.toEqual( + DeauthenticationResult.notHandled() + ); + await expect(provider.logout(request, { somethingElse: 'x' } as any)).resolves.toEqual( + DeauthenticationResult.notHandled() + ); - sinon.assert.notCalled(mockOptions.client.callAsInternalUser); + expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled(); }); it('fails if SAML logout call fails.', async () => { @@ -910,42 +931,32 @@ describe('SAMLAuthenticationProvider', () => { const refreshToken = 'x-saml-refresh-token'; const failureReason = new Error('Realm is misconfigured!'); - mockOptions.client.callAsInternalUser.withArgs('shield.samlLogout').rejects(failureReason); + mockOptions.client.callAsInternalUser.mockRejectedValue(failureReason); - const authenticationResult = await provider.logout(request, { - username: 'user', - accessToken, - refreshToken, - }); + await expect( + provider.logout(request, { username: 'user', accessToken, refreshToken }) + ).resolves.toEqual(DeauthenticationResult.failed(failureReason)); - sinon.assert.calledOnce(mockOptions.client.callAsInternalUser); - sinon.assert.calledWithExactly(mockOptions.client.callAsInternalUser, 'shield.samlLogout', { + expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledTimes(1); + expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.samlLogout', { body: { token: accessToken, refresh_token: refreshToken }, }); - - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.error).toBe(failureReason); }); it('fails if SAML invalidate call fails.', async () => { const request = httpServerMock.createKibanaRequest({ query: { SAMLRequest: 'xxx yyy' } }); const failureReason = new Error('Realm is misconfigured!'); - mockOptions.client.callAsInternalUser - .withArgs('shield.samlInvalidate') - .rejects(failureReason); + mockOptions.client.callAsInternalUser.mockRejectedValue(failureReason); - const authenticationResult = await provider.logout(request); - - sinon.assert.calledOnce(mockOptions.client.callAsInternalUser); - sinon.assert.calledWithExactly( - mockOptions.client.callAsInternalUser, - 'shield.samlInvalidate', - { body: { queryString: 'SAMLRequest=xxx%20yyy', realm: 'test-realm' } } + await expect(provider.logout(request)).resolves.toEqual( + DeauthenticationResult.failed(failureReason) ); - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.error).toBe(failureReason); + expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledTimes(1); + expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.samlInvalidate', { + body: { queryString: 'SAMLRequest=xxx%20yyy', realm: 'test-realm' }, + }); }); it('redirects to /logged_out if `redirect` field in SAML logout response is null.', async () => { @@ -953,23 +964,16 @@ describe('SAMLAuthenticationProvider', () => { const accessToken = 'x-saml-token'; const refreshToken = 'x-saml-refresh-token'; - mockOptions.client.callAsInternalUser - .withArgs('shield.samlLogout') - .resolves({ redirect: null }); + mockOptions.client.callAsInternalUser.mockResolvedValue({ redirect: null }); - const authenticationResult = await provider.logout(request, { - username: 'user', - accessToken, - refreshToken, - }); + await expect( + provider.logout(request, { username: 'user', accessToken, refreshToken }) + ).resolves.toEqual(DeauthenticationResult.redirectTo('/mock-server-basepath/logged_out')); - sinon.assert.calledOnce(mockOptions.client.callAsInternalUser); - sinon.assert.calledWithExactly(mockOptions.client.callAsInternalUser, 'shield.samlLogout', { + expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledTimes(1); + expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.samlLogout', { body: { token: accessToken, refresh_token: refreshToken }, }); - - expect(authenticationResult.redirected()).toBe(true); - expect(authenticationResult.redirectURL).toBe('/mock-server-basepath/logged_out'); }); it('redirects to /logged_out if `redirect` field in SAML logout response is not defined.', async () => { @@ -977,23 +981,16 @@ describe('SAMLAuthenticationProvider', () => { const accessToken = 'x-saml-token'; const refreshToken = 'x-saml-refresh-token'; - mockOptions.client.callAsInternalUser - .withArgs('shield.samlLogout') - .resolves({ redirect: undefined }); + mockOptions.client.callAsInternalUser.mockResolvedValue({ redirect: undefined }); - const authenticationResult = await provider.logout(request, { - username: 'user', - accessToken, - refreshToken, - }); + await expect( + provider.logout(request, { username: 'user', accessToken, refreshToken }) + ).resolves.toEqual(DeauthenticationResult.redirectTo('/mock-server-basepath/logged_out')); - sinon.assert.calledOnce(mockOptions.client.callAsInternalUser); - sinon.assert.calledWithExactly(mockOptions.client.callAsInternalUser, 'shield.samlLogout', { + expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledTimes(1); + expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.samlLogout', { body: { token: accessToken, refresh_token: refreshToken }, }); - - expect(authenticationResult.redirected()).toBe(true); - expect(authenticationResult.redirectURL).toBe('/mock-server-basepath/logged_out'); }); it('relies on SAML logout if query string is not empty, but does not include SAMLRequest.', async () => { @@ -1003,87 +1000,65 @@ describe('SAMLAuthenticationProvider', () => { const accessToken = 'x-saml-token'; const refreshToken = 'x-saml-refresh-token'; - mockOptions.client.callAsInternalUser - .withArgs('shield.samlLogout') - .resolves({ redirect: null }); + mockOptions.client.callAsInternalUser.mockResolvedValue({ redirect: null }); - const authenticationResult = await provider.logout(request, { - username: 'user', - accessToken, - refreshToken, - }); + await expect( + provider.logout(request, { username: 'user', accessToken, refreshToken }) + ).resolves.toEqual(DeauthenticationResult.redirectTo('/mock-server-basepath/logged_out')); - sinon.assert.calledOnce(mockOptions.client.callAsInternalUser); - sinon.assert.calledWithExactly(mockOptions.client.callAsInternalUser, 'shield.samlLogout', { + expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledTimes(1); + expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.samlLogout', { body: { token: accessToken, refresh_token: refreshToken }, }); - - expect(authenticationResult.redirected()).toBe(true); - expect(authenticationResult.redirectURL).toBe('/mock-server-basepath/logged_out'); }); it('relies on SAML invalidate call even if access token is presented.', async () => { const request = httpServerMock.createKibanaRequest({ query: { SAMLRequest: 'xxx yyy' } }); - mockOptions.client.callAsInternalUser - .withArgs('shield.samlInvalidate') - .resolves({ redirect: null }); - - const authenticationResult = await provider.logout(request, { - username: 'user', - accessToken: 'x-saml-token', - refreshToken: 'x-saml-refresh-token', - }); + mockOptions.client.callAsInternalUser.mockResolvedValue({ redirect: null }); - sinon.assert.calledOnce(mockOptions.client.callAsInternalUser); - sinon.assert.calledWithExactly( - mockOptions.client.callAsInternalUser, - 'shield.samlInvalidate', - { body: { queryString: 'SAMLRequest=xxx%20yyy', realm: 'test-realm' } } - ); + await expect( + provider.logout(request, { + username: 'user', + accessToken: 'x-saml-token', + refreshToken: 'x-saml-refresh-token', + }) + ).resolves.toEqual(DeauthenticationResult.redirectTo('/mock-server-basepath/logged_out')); - expect(authenticationResult.redirected()).toBe(true); - expect(authenticationResult.redirectURL).toBe('/mock-server-basepath/logged_out'); + expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledTimes(1); + expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.samlInvalidate', { + body: { queryString: 'SAMLRequest=xxx%20yyy', realm: 'test-realm' }, + }); }); it('redirects to /logged_out if `redirect` field in SAML invalidate response is null.', async () => { const request = httpServerMock.createKibanaRequest({ query: { SAMLRequest: 'xxx yyy' } }); - mockOptions.client.callAsInternalUser - .withArgs('shield.samlInvalidate') - .resolves({ redirect: null }); + mockOptions.client.callAsInternalUser.mockResolvedValue({ redirect: null }); - const authenticationResult = await provider.logout(request); - - sinon.assert.calledOnce(mockOptions.client.callAsInternalUser); - sinon.assert.calledWithExactly( - mockOptions.client.callAsInternalUser, - 'shield.samlInvalidate', - { body: { queryString: 'SAMLRequest=xxx%20yyy', realm: 'test-realm' } } + await expect(provider.logout(request)).resolves.toEqual( + DeauthenticationResult.redirectTo('/mock-server-basepath/logged_out') ); - expect(authenticationResult.redirected()).toBe(true); - expect(authenticationResult.redirectURL).toBe('/mock-server-basepath/logged_out'); + expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledTimes(1); + expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.samlInvalidate', { + body: { queryString: 'SAMLRequest=xxx%20yyy', realm: 'test-realm' }, + }); }); it('redirects to /logged_out if `redirect` field in SAML invalidate response is not defined.', async () => { const request = httpServerMock.createKibanaRequest({ query: { SAMLRequest: 'xxx yyy' } }); - mockOptions.client.callAsInternalUser - .withArgs('shield.samlInvalidate') - .resolves({ redirect: undefined }); + mockOptions.client.callAsInternalUser.mockResolvedValue({ redirect: undefined }); - const authenticationResult = await provider.logout(request); - - sinon.assert.calledOnce(mockOptions.client.callAsInternalUser); - sinon.assert.calledWithExactly( - mockOptions.client.callAsInternalUser, - 'shield.samlInvalidate', - { body: { queryString: 'SAMLRequest=xxx%20yyy', realm: 'test-realm' } } + await expect(provider.logout(request)).resolves.toEqual( + DeauthenticationResult.redirectTo('/mock-server-basepath/logged_out') ); - expect(authenticationResult.redirected()).toBe(true); - expect(authenticationResult.redirectURL).toBe('/mock-server-basepath/logged_out'); + expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledTimes(1); + expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.samlInvalidate', { + body: { queryString: 'SAMLRequest=xxx%20yyy', realm: 'test-realm' }, + }); }); it('redirects user to the IdP if SLO is supported by IdP in case of SP initiated logout.', async () => { @@ -1091,37 +1066,41 @@ describe('SAMLAuthenticationProvider', () => { const accessToken = 'x-saml-token'; const refreshToken = 'x-saml-refresh-token'; - mockOptions.client.callAsInternalUser - .withArgs('shield.samlLogout') - .resolves({ redirect: 'http://fake-idp/SLO?SAMLRequest=7zlH37H' }); - - const authenticationResult = await provider.logout(request, { - username: 'user', - accessToken, - refreshToken, + mockOptions.client.callAsInternalUser.mockResolvedValue({ + redirect: 'http://fake-idp/SLO?SAMLRequest=7zlH37H', }); - sinon.assert.calledOnce(mockOptions.client.callAsInternalUser); - expect(authenticationResult.redirected()).toBe(true); - expect(authenticationResult.redirectURL).toBe('http://fake-idp/SLO?SAMLRequest=7zlH37H'); + await expect( + provider.logout(request, { username: 'user', accessToken, refreshToken }) + ).resolves.toEqual( + DeauthenticationResult.redirectTo('http://fake-idp/SLO?SAMLRequest=7zlH37H') + ); + + expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledTimes(1); }); it('redirects user to the IdP if SLO is supported by IdP in case of IdP initiated logout.', async () => { const request = httpServerMock.createKibanaRequest({ query: { SAMLRequest: 'xxx yyy' } }); - mockOptions.client.callAsInternalUser - .withArgs('shield.samlInvalidate') - .resolves({ redirect: 'http://fake-idp/SLO?SAMLRequest=7zlH37H' }); - - const authenticationResult = await provider.logout(request, { - username: 'user', - accessToken: 'x-saml-token', - refreshToken: 'x-saml-refresh-token', + mockOptions.client.callAsInternalUser.mockResolvedValue({ + redirect: 'http://fake-idp/SLO?SAMLRequest=7zlH37H', }); - sinon.assert.calledOnce(mockOptions.client.callAsInternalUser); - expect(authenticationResult.redirected()).toBe(true); - expect(authenticationResult.redirectURL).toBe('http://fake-idp/SLO?SAMLRequest=7zlH37H'); + await expect( + provider.logout(request, { + username: 'user', + accessToken: 'x-saml-token', + refreshToken: 'x-saml-refresh-token', + }) + ).resolves.toEqual( + DeauthenticationResult.redirectTo('http://fake-idp/SLO?SAMLRequest=7zlH37H') + ); + + expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledTimes(1); }); }); + + it('`getHTTPAuthenticationScheme` method', () => { + expect(provider.getHTTPAuthenticationScheme()).toBe('bearer'); + }); }); diff --git a/x-pack/plugins/security/server/authentication/providers/saml.ts b/x-pack/plugins/security/server/authentication/providers/saml.ts index a817159fcd445..1ac59d66a2235 100644 --- a/x-pack/plugins/security/server/authentication/providers/saml.ts +++ b/x-pack/plugins/security/server/authentication/providers/saml.ts @@ -9,9 +9,10 @@ import { ByteSizeValue } from '@kbn/config-schema'; import { KibanaRequest } from '../../../../../../src/core/server'; import { AuthenticationResult } from '../authentication_result'; import { DeauthenticationResult } from '../deauthentication_result'; -import { AuthenticationProviderOptions, BaseAuthenticationProvider } from './base'; import { canRedirectRequest } from '../can_redirect_request'; +import { getHTTPAuthenticationScheme } from '../get_http_authentication_scheme'; import { Tokens, TokenPair } from '../tokens'; +import { AuthenticationProviderOptions, BaseAuthenticationProvider } from './base'; /** * The state supported by the provider (for the SAML handshake or established session). @@ -180,17 +181,13 @@ export class SAMLAuthenticationProvider extends BaseAuthenticationProvider { public async authenticate(request: KibanaRequest, state?: ProviderState | null) { this.logger.debug(`Trying to authenticate user request to ${request.url.path}.`); - // We should get rid of `Bearer` scheme support as soon as Reporting doesn't need it anymore. - let { - authenticationResult, - // eslint-disable-next-line prefer-const - headerNotRecognized, - } = await this.authenticateViaHeader(request); - if (headerNotRecognized) { - return authenticationResult; + if (getHTTPAuthenticationScheme(request) != null) { + this.logger.debug('Cannot authenticate requests with `Authorization` header.'); + return AuthenticationResult.notHandled(); } - if (state && authenticationResult.notHandled()) { + let authenticationResult = AuthenticationResult.notHandled(); + if (state) { authenticationResult = await this.authenticateViaState(request, state); if ( authenticationResult.failed() && @@ -243,37 +240,11 @@ export class SAMLAuthenticationProvider extends BaseAuthenticationProvider { } /** - * Validates whether request contains `Bearer ***` Authorization header and just passes it - * forward to Elasticsearch backend. - * @param request Request instance. + * Returns HTTP authentication scheme (`Bearer`) that's used within `Authorization` HTTP header + * that provider attaches to all successfully authenticated requests to Elasticsearch. */ - private async authenticateViaHeader(request: KibanaRequest) { - this.logger.debug('Trying to authenticate via header.'); - - const authorization = request.headers.authorization; - if (!authorization || typeof authorization !== 'string') { - this.logger.debug('Authorization header is not presented.'); - return { authenticationResult: AuthenticationResult.notHandled() }; - } - - const authenticationSchema = authorization.split(/\s+/)[0]; - if (authenticationSchema.toLowerCase() !== 'bearer') { - this.logger.debug(`Unsupported authentication schema: ${authenticationSchema}`); - return { - authenticationResult: AuthenticationResult.notHandled(), - headerNotRecognized: true, - }; - } - - try { - const user = await this.getUser(request); - - this.logger.debug('Request has been authenticated via header.'); - return { authenticationResult: AuthenticationResult.succeeded(user) }; - } catch (err) { - this.logger.debug(`Failed to authenticate request via header: ${err.message}`); - return { authenticationResult: AuthenticationResult.failed(err) }; - } + public getHTTPAuthenticationScheme() { + return 'bearer'; } /** 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 0a55219e25d91..e81d14e8bf9f3 100644 --- a/x-pack/plugins/security/server/authentication/providers/token.test.ts +++ b/x-pack/plugins/security/server/authentication/providers/token.test.ts @@ -6,18 +6,32 @@ import Boom from 'boom'; import { errors } from 'elasticsearch'; -import sinon from 'sinon'; -import { httpServerMock } from '../../../../../../src/core/server/mocks'; +import { elasticsearchServiceMock, httpServerMock } from '../../../../../../src/core/server/mocks'; import { mockAuthenticatedUser } from '../../../common/model/authenticated_user.mock'; -import { - MockAuthenticationProviderOptions, - mockAuthenticationProviderOptions, - mockScopedClusterClient, -} from './base.mock'; +import { MockAuthenticationProviderOptions, mockAuthenticationProviderOptions } from './base.mock'; +import { + ElasticsearchErrorHelpers, + IClusterClient, + ScopeableRequest, +} from '../../../../../../src/core/server'; +import { AuthenticationResult } from '../authentication_result'; +import { DeauthenticationResult } from '../deauthentication_result'; import { TokenAuthenticationProvider } from './token'; +function expectAuthenticateCall( + mockClusterClient: jest.Mocked, + scopeableRequest: ScopeableRequest +) { + expect(mockClusterClient.asScoped).toHaveBeenCalledTimes(1); + expect(mockClusterClient.asScoped).toHaveBeenCalledWith(scopeableRequest); + + const mockScopedClusterClient = mockClusterClient.asScoped.mock.results[0].value; + expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledTimes(1); + expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith('shield.authenticate'); +} + describe('TokenAuthenticationProvider', () => { let provider: TokenAuthenticationProvider; let mockOptions: MockAuthenticationProviderOptions; @@ -28,29 +42,35 @@ describe('TokenAuthenticationProvider', () => { describe('`login` method', () => { it('succeeds with valid login attempt, creates session and authHeaders', async () => { - const request = httpServerMock.createKibanaRequest(); + const request = httpServerMock.createKibanaRequest({ headers: {} }); const user = mockAuthenticatedUser(); const credentials = { username: 'user', password: 'password' }; const tokenPair = { accessToken: 'foo', refreshToken: 'bar' }; const authorization = `Bearer ${tokenPair.accessToken}`; - mockOptions.client.callAsInternalUser - .withArgs('shield.getAccessToken', { - body: { grant_type: 'password', ...credentials }, - }) - .resolves({ access_token: tokenPair.accessToken, refresh_token: tokenPair.refreshToken }); + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.callAsCurrentUser.mockResolvedValue(user); + mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); + + mockOptions.client.callAsInternalUser.mockResolvedValue({ + access_token: tokenPair.accessToken, + refresh_token: tokenPair.refreshToken, + }); - mockScopedClusterClient(mockOptions.client, sinon.match({ headers: { authorization } })) - .callAsCurrentUser.withArgs('shield.authenticate') - .resolves(user); + await expect(provider.login(request, credentials)).resolves.toEqual( + AuthenticationResult.succeeded( + { ...user, authentication_provider: 'token' }, + { authHeaders: { authorization }, state: tokenPair } + ) + ); - const authenticationResult = await provider.login(request, credentials); + expectAuthenticateCall(mockOptions.client, { headers: { authorization } }); - expect(authenticationResult.succeeded()).toBe(true); - expect(authenticationResult.user).toEqual({ ...user, authentication_provider: 'token' }); - expect(authenticationResult.state).toEqual(tokenPair); - expect(authenticationResult.authHeaders).toEqual({ authorization }); + expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledTimes(1); + expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.getAccessToken', { + body: { grant_type: 'password', ...credentials }, + }); }); it('fails if token cannot be generated during login attempt', async () => { @@ -58,109 +78,125 @@ describe('TokenAuthenticationProvider', () => { const credentials = { username: 'user', password: 'password' }; const authenticationError = new Error('Invalid credentials'); - mockOptions.client.callAsInternalUser - .withArgs('shield.getAccessToken', { - body: { grant_type: 'password', ...credentials }, - }) - .rejects(authenticationError); + mockOptions.client.callAsInternalUser.mockRejectedValue(authenticationError); - const authenticationResult = await provider.login(request, credentials); + await expect(provider.login(request, credentials)).resolves.toEqual( + AuthenticationResult.failed(authenticationError) + ); + + expect(mockOptions.client.asScoped).not.toHaveBeenCalled(); - sinon.assert.notCalled(mockOptions.client.asScoped); + expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledTimes(1); + expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.getAccessToken', { + body: { grant_type: 'password', ...credentials }, + }); expect(request.headers).not.toHaveProperty('authorization'); - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.user).toBeUndefined(); - expect(authenticationResult.state).toBeUndefined(); - expect(authenticationResult.error).toEqual(authenticationError); }); it('fails if user cannot be retrieved during login attempt', async () => { - const request = httpServerMock.createKibanaRequest(); + const request = httpServerMock.createKibanaRequest({ headers: {} }); const credentials = { username: 'user', password: 'password' }; const tokenPair = { accessToken: 'foo', refreshToken: 'bar' }; + const authorization = `Bearer ${tokenPair.accessToken}`; - mockOptions.client.callAsInternalUser - .withArgs('shield.getAccessToken', { - body: { grant_type: 'password', ...credentials }, - }) - .resolves({ access_token: tokenPair.accessToken, refresh_token: tokenPair.refreshToken }); + mockOptions.client.callAsInternalUser.mockResolvedValue({ + access_token: tokenPair.accessToken, + refresh_token: tokenPair.refreshToken, + }); const authenticationError = new Error('Some error'); - mockScopedClusterClient( - mockOptions.client, - sinon.match({ headers: { authorization: `Bearer ${tokenPair.accessToken}` } }) - ) - .callAsCurrentUser.withArgs('shield.authenticate') - .rejects(authenticationError); + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.callAsCurrentUser.mockRejectedValue(authenticationError); + mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); - const authenticationResult = await provider.login(request, credentials); + await expect(provider.login(request, credentials)).resolves.toEqual( + AuthenticationResult.failed(authenticationError) + ); + + expectAuthenticateCall(mockOptions.client, { headers: { authorization } }); + + expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledTimes(1); + expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.getAccessToken', { + body: { grant_type: 'password', ...credentials }, + }); expect(request.headers).not.toHaveProperty('authorization'); - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.user).toBeUndefined(); - expect(authenticationResult.state).toBeUndefined(); - expect(authenticationResult.error).toEqual(authenticationError); }); }); describe('`authenticate` method', () => { - it('does not redirect AJAX requests that can not be authenticated to the login page.', async () => { - // Add `kbn-xsrf` header to make `can_redirect_request` think that it's AJAX request and - // avoid triggering of redirect logic. - const authenticationResult = await provider.authenticate( - httpServerMock.createKibanaRequest({ headers: { 'kbn-xsrf': 'xsrf' } }), - null - ); - - expect(authenticationResult.notHandled()).toBe(true); - }); + it('does not handle authentication via `authorization` header.', async () => { + const request = httpServerMock.createKibanaRequest({ + headers: { authorization: 'Bearer some-token' }, + }); - it('redirects non-AJAX requests that can not be authenticated to the login page.', async () => { - const authenticationResult = await provider.authenticate( - httpServerMock.createKibanaRequest({ path: '/s/foo/some-path # that needs to be encoded' }), - null + await expect(provider.authenticate(request)).resolves.toEqual( + AuthenticationResult.notHandled() ); - expect(authenticationResult.redirected()).toBe(true); - expect(authenticationResult.redirectURL).toBe( - '/base-path/login?next=%2Fbase-path%2Fs%2Ffoo%2Fsome-path%20%23%20that%20needs%20to%20be%20encoded' - ); + expect(mockOptions.client.asScoped).not.toHaveBeenCalled(); + expect(request.headers.authorization).toBe('Bearer some-token'); }); - it('succeeds if only `authorization` header is available and returns neither state nor authHeaders.', async () => { - const authorization = 'Bearer foo'; - const request = httpServerMock.createKibanaRequest({ headers: { authorization } }); - const user = mockAuthenticatedUser(); + it('does not handle authentication via `authorization` header even if state contains valid credentials.', async () => { + const request = httpServerMock.createKibanaRequest({ + headers: { authorization: 'Bearer some-token' }, + }); - mockScopedClusterClient(mockOptions.client, sinon.match({ headers: { authorization } })) - .callAsCurrentUser.withArgs('shield.authenticate') - .resolves(user); + await expect( + provider.authenticate(request, { accessToken: 'foo', refreshToken: 'bar' }) + ).resolves.toEqual(AuthenticationResult.notHandled()); - const authenticationResult = await provider.authenticate(request); + expect(mockOptions.client.asScoped).not.toHaveBeenCalled(); + expect(request.headers.authorization).toBe('Bearer some-token'); + }); - expect(authenticationResult.succeeded()).toBe(true); - expect(authenticationResult.user).toEqual({ ...user, authentication_provider: 'token' }); - expect(authenticationResult.authHeaders).toBeUndefined(); - expect(authenticationResult.state).toBeUndefined(); + it('does not redirect AJAX requests that can not be authenticated to the login page.', async () => { + // Add `kbn-xsrf` header to make `can_redirect_request` think that it's AJAX request and + // avoid triggering of redirect logic. + await expect( + provider.authenticate( + httpServerMock.createKibanaRequest({ headers: { 'kbn-xsrf': 'xsrf' } }), + null + ) + ).resolves.toEqual(AuthenticationResult.notHandled()); + }); + + it('redirects non-AJAX requests that can not be authenticated to the login page.', async () => { + await expect( + provider.authenticate( + httpServerMock.createKibanaRequest({ + path: '/s/foo/some-path # that needs to be encoded', + }), + null + ) + ).resolves.toEqual( + AuthenticationResult.redirectTo( + '/base-path/login?next=%2Fbase-path%2Fs%2Ffoo%2Fsome-path%20%23%20that%20needs%20to%20be%20encoded' + ) + ); }); it('succeeds if only state is available.', async () => { - const request = httpServerMock.createKibanaRequest(); + const request = httpServerMock.createKibanaRequest({ headers: {} }); const tokenPair = { accessToken: 'foo', refreshToken: 'bar' }; const user = mockAuthenticatedUser(); const authorization = `Bearer ${tokenPair.accessToken}`; - mockScopedClusterClient(mockOptions.client, sinon.match({ headers: { authorization } })) - .callAsCurrentUser.withArgs('shield.authenticate') - .resolves(user); + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.callAsCurrentUser.mockResolvedValue(user); + mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); - const authenticationResult = await provider.authenticate(request, tokenPair); + await expect(provider.authenticate(request, tokenPair)).resolves.toEqual( + AuthenticationResult.succeeded( + { ...user, authentication_provider: 'token' }, + { authHeaders: { authorization } } + ) + ); + + expectAuthenticateCall(mockOptions.client, { headers: { authorization } }); - expect(authenticationResult.succeeded()).toBe(true); - expect(authenticationResult.user).toEqual({ ...user, authentication_provider: 'token' }); - expect(authenticationResult.state).toBeUndefined(); - expect(authenticationResult.authHeaders).toEqual({ authorization }); expect(request.headers).not.toHaveProperty('authorization'); }); @@ -169,162 +205,115 @@ describe('TokenAuthenticationProvider', () => { const request = httpServerMock.createKibanaRequest(); const tokenPair = { accessToken: 'foo', refreshToken: 'bar' }; - mockScopedClusterClient( - mockOptions.client, - sinon.match({ headers: { authorization: `Bearer ${tokenPair.accessToken}` } }) - ) - .callAsCurrentUser.withArgs('shield.authenticate') - .rejects({ statusCode: 401 }); - - mockOptions.tokens.refresh - .withArgs(tokenPair.refreshToken) - .resolves({ accessToken: 'newfoo', refreshToken: 'newbar' }); + mockOptions.client.asScoped.mockImplementation(scopeableRequest => { + if (scopeableRequest?.headers.authorization === `Bearer ${tokenPair.accessToken}`) { + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.callAsCurrentUser.mockRejectedValue( + ElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error()) + ); + return mockScopedClusterClient; + } + + if (scopeableRequest?.headers.authorization === 'Bearer newfoo') { + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.callAsCurrentUser.mockResolvedValue(user); + return mockScopedClusterClient; + } + + throw new Error('Unexpected call'); + }); - mockScopedClusterClient( - mockOptions.client, - sinon.match({ headers: { authorization: 'Bearer newfoo' } }) - ) - .callAsCurrentUser.withArgs('shield.authenticate') - .resolves(user); + mockOptions.tokens.refresh.mockResolvedValue({ + accessToken: 'newfoo', + refreshToken: 'newbar', + }); - const authenticationResult = await provider.authenticate(request, tokenPair); + await expect(provider.authenticate(request, tokenPair)).resolves.toEqual( + AuthenticationResult.succeeded( + { ...user, authentication_provider: 'token' }, + { + authHeaders: { authorization: 'Bearer newfoo' }, + state: { accessToken: 'newfoo', refreshToken: 'newbar' }, + } + ) + ); - sinon.assert.calledOnce(mockOptions.tokens.refresh); + expect(mockOptions.tokens.refresh).toHaveBeenCalledTimes(1); + expect(mockOptions.tokens.refresh).toHaveBeenCalledWith(tokenPair.refreshToken); - expect(authenticationResult.succeeded()).toBe(true); - expect(authenticationResult.user).toEqual({ ...user, authentication_provider: 'token' }); - expect(authenticationResult.state).toEqual({ accessToken: 'newfoo', refreshToken: 'newbar' }); - expect(authenticationResult.authHeaders).toEqual({ authorization: 'Bearer newfoo' }); expect(request.headers).not.toHaveProperty('authorization'); }); - it('does not handle `authorization` header with unsupported schema even if state contains valid credentials.', async () => { - const request = httpServerMock.createKibanaRequest({ - headers: { authorization: 'Basic ***' }, - }); + it('fails if authentication with token from state fails with unknown error.', async () => { const tokenPair = { accessToken: 'foo', refreshToken: 'bar' }; - const user = mockAuthenticatedUser(); + const request = httpServerMock.createKibanaRequest({ headers: {} }); const authorization = `Bearer ${tokenPair.accessToken}`; - mockScopedClusterClient(mockOptions.client, sinon.match({ headers: { authorization } })) - .callAsCurrentUser.withArgs('shield.authenticate') - .resolves(user); - - const authenticationResult = await provider.authenticate(request, tokenPair); - - sinon.assert.notCalled(mockOptions.client.asScoped); - expect(request.headers.authorization).toBe('Basic ***'); - expect(authenticationResult.notHandled()).toBe(true); - }); - - it('authenticates only via `authorization` header even if state is available.', async () => { - const tokenPair = { accessToken: 'foo', refreshToken: 'bar' }; - const authorization = `Bearer foo-from-header`; - const request = httpServerMock.createKibanaRequest({ headers: { authorization } }); - const user = mockAuthenticatedUser(); - - // GetUser will be called with request's `authorization` header. - mockScopedClusterClient(mockOptions.client, sinon.match({ headers: { authorization } })) - .callAsCurrentUser.withArgs('shield.authenticate') - .resolves(user); - - const authenticationResult = await provider.authenticate(request, tokenPair); - - expect(authenticationResult.succeeded()).toBe(true); - expect(authenticationResult.user).toEqual({ ...user, authentication_provider: 'token' }); - expect(authenticationResult.state).toBeUndefined(); - expect(authenticationResult.authHeaders).toBeUndefined(); - expect(request.headers.authorization).toEqual('Bearer foo-from-header'); - }); - - it('fails if authentication with token from header fails with unknown error', async () => { - const authorization = `Bearer foo`; - const request = httpServerMock.createKibanaRequest({ headers: { authorization } }); - const authenticationError = new errors.InternalServerError('something went wrong'); - mockScopedClusterClient(mockOptions.client, sinon.match({ headers: { authorization } })) - .callAsCurrentUser.withArgs('shield.authenticate') - .rejects(authenticationError); - - const authenticationResult = await provider.authenticate(request); + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.callAsCurrentUser.mockRejectedValue(authenticationError); + mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.user).toBeUndefined(); - expect(authenticationResult.state).toBeUndefined(); - expect(authenticationResult.error).toEqual(authenticationError); - }); - - it('fails if authentication with token from state fails with unknown error.', async () => { - const tokenPair = { accessToken: 'foo', refreshToken: 'bar' }; - const request = httpServerMock.createKibanaRequest(); - - const authenticationError = new errors.InternalServerError('something went wrong'); - mockScopedClusterClient( - mockOptions.client, - sinon.match({ headers: { authorization: `Bearer ${tokenPair.accessToken}` } }) - ) - .callAsCurrentUser.withArgs('shield.authenticate') - .rejects(authenticationError); + await expect(provider.authenticate(request, tokenPair)).resolves.toEqual( + AuthenticationResult.failed(authenticationError) + ); - const authenticationResult = await provider.authenticate(request, tokenPair); + expectAuthenticateCall(mockOptions.client, { headers: { authorization } }); expect(request.headers).not.toHaveProperty('authorization'); - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.user).toBeUndefined(); - expect(authenticationResult.state).toBeUndefined(); - expect(authenticationResult.error).toEqual(authenticationError); }); it('fails if token refresh is rejected with unknown error', async () => { - const request = httpServerMock.createKibanaRequest(); + const request = httpServerMock.createKibanaRequest({ headers: {} }); const tokenPair = { accessToken: 'foo', refreshToken: 'bar' }; + const authorization = `Bearer ${tokenPair.accessToken}`; - mockScopedClusterClient( - mockOptions.client, - sinon.match({ headers: { authorization: `Bearer ${tokenPair.accessToken}` } }) - ) - .callAsCurrentUser.withArgs('shield.authenticate') - .rejects({ statusCode: 401 }); + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.callAsCurrentUser.mockRejectedValue( + ElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error()) + ); + mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); const refreshError = new errors.InternalServerError('failed to refresh token'); - mockOptions.tokens.refresh.withArgs(tokenPair.refreshToken).rejects(refreshError); + mockOptions.tokens.refresh.mockRejectedValue(refreshError); - const authenticationResult = await provider.authenticate(request, tokenPair); + await expect(provider.authenticate(request, tokenPair)).resolves.toEqual( + AuthenticationResult.failed(refreshError) + ); + + expect(mockOptions.tokens.refresh).toHaveBeenCalledTimes(1); + expect(mockOptions.tokens.refresh).toHaveBeenCalledWith(tokenPair.refreshToken); - sinon.assert.calledOnce(mockOptions.tokens.refresh); + expectAuthenticateCall(mockOptions.client, { headers: { authorization } }); expect(request.headers).not.toHaveProperty('authorization'); - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.user).toBeUndefined(); - expect(authenticationResult.state).toBeUndefined(); - expect(authenticationResult.error).toEqual(refreshError); }); it('redirects non-AJAX requests to /login and clears session if token cannot be refreshed', async () => { - const request = httpServerMock.createKibanaRequest({ path: '/some-path' }); + const request = httpServerMock.createKibanaRequest({ path: '/some-path', headers: {} }); const tokenPair = { accessToken: 'foo', refreshToken: 'bar' }; + const authorization = `Bearer ${tokenPair.accessToken}`; - mockScopedClusterClient( - mockOptions.client, - sinon.match({ headers: { authorization: `Bearer ${tokenPair.accessToken}` } }) - ) - .callAsCurrentUser.withArgs('shield.authenticate') - .rejects({ statusCode: 401 }); + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.callAsCurrentUser.mockRejectedValue( + ElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error()) + ); + mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); + + mockOptions.tokens.refresh.mockResolvedValue(null); - mockOptions.tokens.refresh.withArgs(tokenPair.refreshToken).resolves(null); + await expect(provider.authenticate(request, tokenPair)).resolves.toEqual( + AuthenticationResult.redirectTo('/base-path/login?next=%2Fbase-path%2Fsome-path', { + state: null, + }) + ); - const authenticationResult = await provider.authenticate(request, tokenPair); + expect(mockOptions.tokens.refresh).toHaveBeenCalledTimes(1); + expect(mockOptions.tokens.refresh).toHaveBeenCalledWith(tokenPair.refreshToken); - sinon.assert.calledOnce(mockOptions.tokens.refresh); + expectAuthenticateCall(mockOptions.client, { headers: { authorization } }); expect(request.headers).not.toHaveProperty('authorization'); - expect(authenticationResult.redirected()).toBe(true); - expect(authenticationResult.redirectURL).toBe( - '/base-path/login?next=%2Fbase-path%2Fsome-path' - ); - expect(authenticationResult.user).toBeUndefined(); - expect(authenticationResult.state).toEqual(null); - expect(authenticationResult.error).toBeUndefined(); }); it('does not redirect AJAX requests if token token cannot be refreshed', async () => { @@ -333,61 +322,66 @@ describe('TokenAuthenticationProvider', () => { path: '/some-path', }); const tokenPair = { accessToken: 'foo', refreshToken: 'bar' }; + const authorization = `Bearer ${tokenPair.accessToken}`; + + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.callAsCurrentUser.mockRejectedValue( + ElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error()) + ); + mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); - mockScopedClusterClient( - mockOptions.client, - sinon.match({ headers: { authorization: `Bearer ${tokenPair.accessToken}` } }) - ) - .callAsCurrentUser.withArgs('shield.authenticate') - .rejects({ statusCode: 401 }); + mockOptions.tokens.refresh.mockResolvedValue(null); - mockOptions.tokens.refresh.withArgs(tokenPair.refreshToken).resolves(null); + await expect(provider.authenticate(request, tokenPair)).resolves.toEqual( + AuthenticationResult.failed(Boom.badRequest('Both access and refresh tokens are expired.')) + ); - const authenticationResult = await provider.authenticate(request, tokenPair); + expect(mockOptions.tokens.refresh).toHaveBeenCalledTimes(1); + expect(mockOptions.tokens.refresh).toHaveBeenCalledWith(tokenPair.refreshToken); - sinon.assert.calledOnce(mockOptions.tokens.refresh); + expectAuthenticateCall(mockOptions.client, { + headers: { 'kbn-xsrf': 'xsrf', authorization }, + }); expect(request.headers).not.toHaveProperty('authorization'); - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.error).toEqual( - Boom.badRequest('Both access and refresh tokens are expired.') - ); - expect(authenticationResult.user).toBeUndefined(); - expect(authenticationResult.state).toBeUndefined(); }); it('fails if new access token is rejected after successful refresh', async () => { const request = httpServerMock.createKibanaRequest(); const tokenPair = { accessToken: 'foo', refreshToken: 'bar' }; - mockScopedClusterClient( - mockOptions.client, - sinon.match({ headers: { authorization: `Bearer ${tokenPair.accessToken}` } }) - ) - .callAsCurrentUser.withArgs('shield.authenticate') - .rejects({ statusCode: 401 }); - - mockOptions.tokens.refresh - .withArgs(tokenPair.refreshToken) - .resolves({ accessToken: 'newfoo', refreshToken: 'newbar' }); - const authenticationError = new errors.AuthenticationException('Some error'); - mockScopedClusterClient( - mockOptions.client, - sinon.match({ headers: { authorization: 'Bearer newfoo' } }) - ) - .callAsCurrentUser.withArgs('shield.authenticate') - .rejects(authenticationError); + mockOptions.client.asScoped.mockImplementation(scopeableRequest => { + if (scopeableRequest?.headers.authorization === `Bearer ${tokenPair.accessToken}`) { + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.callAsCurrentUser.mockRejectedValue( + ElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error()) + ); + return mockScopedClusterClient; + } + + if (scopeableRequest?.headers.authorization === 'Bearer newfoo') { + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.callAsCurrentUser.mockRejectedValue(authenticationError); + return mockScopedClusterClient; + } + + throw new Error('Unexpected call'); + }); - const authenticationResult = await provider.authenticate(request, tokenPair); + mockOptions.tokens.refresh.mockResolvedValue({ + accessToken: 'newfoo', + refreshToken: 'newbar', + }); + + await expect(provider.authenticate(request, tokenPair)).resolves.toEqual( + AuthenticationResult.failed(authenticationError) + ); - sinon.assert.calledOnce(mockOptions.tokens.refresh); + expect(mockOptions.tokens.refresh).toHaveBeenCalledTimes(1); + expect(mockOptions.tokens.refresh).toHaveBeenCalledWith(tokenPair.refreshToken); expect(request.headers).not.toHaveProperty('authorization'); - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.user).toBeUndefined(); - expect(authenticationResult.state).toBeUndefined(); - expect(authenticationResult.error).toEqual(authenticationError); }); }); @@ -395,13 +389,15 @@ describe('TokenAuthenticationProvider', () => { it('returns `redirected` if state is not presented.', async () => { const request = httpServerMock.createKibanaRequest(); - let deauthenticateResult = await provider.logout(request); - expect(deauthenticateResult.redirected()).toBe(true); + await expect(provider.logout(request)).resolves.toEqual( + DeauthenticationResult.redirectTo('/base-path/login?msg=LOGGED_OUT') + ); - deauthenticateResult = await provider.logout(request, null); - expect(deauthenticateResult.redirected()).toBe(true); + await expect(provider.logout(request, null)).resolves.toEqual( + DeauthenticationResult.redirectTo('/base-path/login?msg=LOGGED_OUT') + ); - sinon.assert.notCalled(mockOptions.tokens.invalidate); + expect(mockOptions.tokens.invalidate).not.toHaveBeenCalled(); }); it('fails if `tokens.invalidate` fails', async () => { @@ -409,45 +405,46 @@ describe('TokenAuthenticationProvider', () => { const tokenPair = { accessToken: 'foo', refreshToken: 'bar' }; const failureReason = new Error('failed to delete token'); - mockOptions.tokens.invalidate.withArgs(tokenPair).rejects(failureReason); - - const authenticationResult = await provider.logout(request, tokenPair); + mockOptions.tokens.invalidate.mockRejectedValue(failureReason); - sinon.assert.calledOnce(mockOptions.tokens.invalidate); - sinon.assert.calledWithExactly(mockOptions.tokens.invalidate, tokenPair); + await expect(provider.logout(request, tokenPair)).resolves.toEqual( + DeauthenticationResult.failed(failureReason) + ); - expect(authenticationResult.failed()).toBe(true); - expect(authenticationResult.error).toBe(failureReason); + expect(mockOptions.tokens.invalidate).toHaveBeenCalledTimes(1); + expect(mockOptions.tokens.invalidate).toHaveBeenCalledWith(tokenPair); }); it('redirects to /login if tokens are invalidated successfully', async () => { const request = httpServerMock.createKibanaRequest(); const tokenPair = { accessToken: 'foo', refreshToken: 'bar' }; - mockOptions.tokens.invalidate.withArgs(tokenPair).resolves(); + mockOptions.tokens.invalidate.mockResolvedValue(undefined); - const authenticationResult = await provider.logout(request, tokenPair); - - sinon.assert.calledOnce(mockOptions.tokens.invalidate); - sinon.assert.calledWithExactly(mockOptions.tokens.invalidate, tokenPair); + await expect(provider.logout(request, tokenPair)).resolves.toEqual( + DeauthenticationResult.redirectTo('/base-path/login?msg=LOGGED_OUT') + ); - expect(authenticationResult.redirected()).toBe(true); - expect(authenticationResult.redirectURL).toBe('/base-path/login?msg=LOGGED_OUT'); + expect(mockOptions.tokens.invalidate).toHaveBeenCalledTimes(1); + expect(mockOptions.tokens.invalidate).toHaveBeenCalledWith(tokenPair); }); it('redirects to /login with optional search parameters if tokens are invalidated successfully', async () => { const request = httpServerMock.createKibanaRequest({ query: { yep: 'nope' } }); const tokenPair = { accessToken: 'foo', refreshToken: 'bar' }; - mockOptions.tokens.invalidate.withArgs(tokenPair).resolves(); - - const authenticationResult = await provider.logout(request, tokenPair); + mockOptions.tokens.invalidate.mockResolvedValue(undefined); - sinon.assert.calledOnce(mockOptions.tokens.invalidate); - sinon.assert.calledWithExactly(mockOptions.tokens.invalidate, tokenPair); + await expect(provider.logout(request, tokenPair)).resolves.toEqual( + DeauthenticationResult.redirectTo('/base-path/login?yep=nope') + ); - expect(authenticationResult.redirected()).toBe(true); - expect(authenticationResult.redirectURL).toBe('/base-path/login?yep=nope'); + expect(mockOptions.tokens.invalidate).toHaveBeenCalledTimes(1); + expect(mockOptions.tokens.invalidate).toHaveBeenCalledWith(tokenPair); }); }); + + it('`getHTTPAuthenticationScheme` method', () => { + expect(provider.getHTTPAuthenticationScheme()).toBe('bearer'); + }); }); diff --git a/x-pack/plugins/security/server/authentication/providers/token.ts b/x-pack/plugins/security/server/authentication/providers/token.ts index 03fd003e2cbde..fffac254ed30a 100644 --- a/x-pack/plugins/security/server/authentication/providers/token.ts +++ b/x-pack/plugins/security/server/authentication/providers/token.ts @@ -8,9 +8,10 @@ import Boom from 'boom'; import { KibanaRequest } from '../../../../../../src/core/server'; import { AuthenticationResult } from '../authentication_result'; import { DeauthenticationResult } from '../deauthentication_result'; -import { BaseAuthenticationProvider } from './base'; import { canRedirectRequest } from '../can_redirect_request'; +import { getHTTPAuthenticationScheme } from '../get_http_authentication_scheme'; import { Tokens, TokenPair } from '../tokens'; +import { BaseAuthenticationProvider } from './base'; /** * Describes the parameters that are required by the provider to process the initial login request. @@ -34,12 +35,6 @@ export class TokenAuthenticationProvider extends BaseAuthenticationProvider { */ static readonly type = 'token'; - /** - * Performs initial login request using username and password. - * @param request Request instance. - * @param loginAttempt Login attempt description. - * @param [state] Optional state object associated with the provider. - */ /** * Performs initial login request using username and password. * @param request Request instance. @@ -87,18 +82,13 @@ export class TokenAuthenticationProvider extends BaseAuthenticationProvider { public async authenticate(request: KibanaRequest, state?: ProviderState | null) { this.logger.debug(`Trying to authenticate user request to ${request.url.path}.`); - // if there isn't a payload, try header-based token auth - const { - authenticationResult: headerAuthResult, - headerNotRecognized, - } = await this.authenticateViaHeader(request); - if (headerNotRecognized) { - return headerAuthResult; + if (getHTTPAuthenticationScheme(request) != null) { + this.logger.debug('Cannot authenticate requests with `Authorization` header.'); + return AuthenticationResult.notHandled(); } - let authenticationResult = headerAuthResult; - // if we still can't attempt auth, try authenticating via state (session token) - if (authenticationResult.notHandled() && state) { + let authenticationResult = AuthenticationResult.notHandled(); + if (state) { authenticationResult = await this.authenticateViaState(request, state); if ( authenticationResult.failed() && @@ -111,6 +101,7 @@ export class TokenAuthenticationProvider extends BaseAuthenticationProvider { // finally, if authentication still can not be handled for this // request/state combination, redirect to the login page if appropriate if (authenticationResult.notHandled() && canRedirectRequest(request)) { + this.logger.debug('Redirecting request to Login page.'); authenticationResult = AuthenticationResult.redirectTo(this.getLoginPageURL(request)); } @@ -144,37 +135,11 @@ export class TokenAuthenticationProvider extends BaseAuthenticationProvider { } /** - * Validates whether request contains `Bearer ***` Authorization header and just passes it - * forward to Elasticsearch backend. - * @param request Request instance. + * Returns HTTP authentication scheme (`Bearer`) that's used within `Authorization` HTTP header + * that provider attaches to all successfully authenticated requests to Elasticsearch. */ - private async authenticateViaHeader(request: KibanaRequest) { - this.logger.debug('Trying to authenticate via header.'); - - const authorization = request.headers.authorization; - if (!authorization || typeof authorization !== 'string') { - this.logger.debug('Authorization header is not presented.'); - return { authenticationResult: AuthenticationResult.notHandled() }; - } - - const authenticationSchema = authorization.split(/\s+/)[0]; - if (authenticationSchema.toLowerCase() !== 'bearer') { - this.logger.debug(`Unsupported authentication schema: ${authenticationSchema}`); - return { authenticationResult: AuthenticationResult.notHandled(), headerNotRecognized: true }; - } - - try { - const user = await this.getUser(request); - - this.logger.debug('Request has been authenticated via header.'); - - // We intentionally do not store anything in session state because token - // header auth can only be used on a request by request basis. - return { authenticationResult: AuthenticationResult.succeeded(user) }; - } catch (err) { - this.logger.debug(`Failed to authenticate request via header: ${err.message}`); - return { authenticationResult: AuthenticationResult.failed(err) }; - } + public getHTTPAuthenticationScheme() { + return 'bearer'; } /** diff --git a/x-pack/plugins/security/server/config.test.ts b/x-pack/plugins/security/server/config.test.ts index f7374eedb5520..64c695670fa19 100644 --- a/x-pack/plugins/security/server/config.test.ts +++ b/x-pack/plugins/security/server/config.test.ts @@ -13,57 +13,78 @@ 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, - "session": Object { - "idleTimeout": null, - "lifespan": null, - }, - } - `); + Object { + "authc": Object { + "http": Object { + "autoSchemesEnabled": true, + "enabled": true, + "schemes": Array [ + "apikey", + ], + }, + "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, - "session": Object { - "idleTimeout": null, - "lifespan": null, - }, - } - `); + Object { + "authc": Object { + "http": Object { + "autoSchemesEnabled": true, + "enabled": true, + "schemes": Array [ + "apikey", + ], + }, + "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, - "session": Object { - "idleTimeout": null, - "lifespan": null, - }, - } - `); + Object { + "authc": Object { + "http": Object { + "autoSchemesEnabled": true, + "enabled": true, + "schemes": Array [ + "apikey", + ], + }, + "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', () => { @@ -101,15 +122,22 @@ describe('config schema', () => { authc: { providers: ['oidc'], oidc: { realm: 'realm-1' } }, }).authc ).toMatchInlineSnapshot(` - Object { - "oidc": Object { - "realm": "realm-1", - }, - "providers": Array [ - "oidc", - ], - } - `); + Object { + "http": Object { + "autoSchemesEnabled": true, + "enabled": true, + "schemes": Array [ + "apikey", + ], + }, + "oidc": Object { + "realm": "realm-1", + }, + "providers": Array [ + "oidc", + ], + } + `); }); it(`returns a validation error when authc.providers is "['oidc', 'basic']" and realm is unspecified`, async () => { @@ -126,16 +154,23 @@ describe('config schema', () => { authc: { providers: ['oidc', 'basic'], oidc: { realm: 'realm-1' } }, }).authc ).toMatchInlineSnapshot(` - Object { - "oidc": Object { - "realm": "realm-1", - }, - "providers": Array [ - "oidc", - "basic", - ], - } - `); + Object { + "http": Object { + "autoSchemesEnabled": true, + "enabled": true, + "schemes": Array [ + "apikey", + ], + }, + "oidc": Object { + "realm": "realm-1", + }, + "providers": Array [ + "oidc", + "basic", + ], + } + `); }); it(`realm is not allowed when authc.providers is "['basic']"`, async () => { @@ -164,18 +199,25 @@ describe('config schema', () => { authc: { providers: ['saml'], saml: { realm: 'realm-1' } }, }).authc ).toMatchInlineSnapshot(` - Object { - "providers": Array [ - "saml", - ], - "saml": Object { - "maxRedirectURLSize": ByteSizeValue { - "valueInBytes": 2048, - }, - "realm": "realm-1", - }, - } - `); + Object { + "http": Object { + "autoSchemesEnabled": true, + "enabled": true, + "schemes": Array [ + "apikey", + ], + }, + "providers": Array [ + "saml", + ], + "saml": Object { + "maxRedirectURLSize": ByteSizeValue { + "valueInBytes": 2048, + }, + "realm": "realm-1", + }, + } + `); }); it('`realm` is not allowed if saml provider is not enabled', async () => { diff --git a/x-pack/plugins/security/server/config.ts b/x-pack/plugins/security/server/config.ts index db8c48f314d7c..8663a6e61c203 100644 --- a/x-pack/plugins/security/server/config.ts +++ b/x-pack/plugins/security/server/config.ts @@ -49,6 +49,11 @@ export const ConfigSchema = schema.object( maxRedirectURLSize: schema.byteSize({ defaultValue: '2kb' }), }) ), + http: schema.object({ + enabled: schema.boolean({ defaultValue: true }), + autoSchemesEnabled: schema.boolean({ defaultValue: true }), + schemes: schema.arrayOf(schema.string(), { defaultValue: ['apikey'] }), + }), }), }, // This option should be removed as soon as we entirely migrate config from legacy Security plugin. diff --git a/x-pack/plugins/security/server/index.ts b/x-pack/plugins/security/server/index.ts index c0e86b289fe54..e1167af0be7f0 100644 --- a/x-pack/plugins/security/server/index.ts +++ b/x-pack/plugins/security/server/index.ts @@ -32,6 +32,17 @@ export const config: PluginConfigDescriptor> = { deprecations: ({ rename, unused }) => [ rename('sessionTimeout', 'session.idleTimeout'), unused('authorization.legacyFallback.enabled'), + (settings, fromPath, log) => { + const hasProvider = (provider: string) => + settings?.xpack?.security?.authc?.providers?.includes(provider) ?? false; + + if (hasProvider('basic') && hasProvider('token')) { + log( + 'Enabling both `basic` and `token` authentication providers in `xpack.security.authc.providers` is deprecated. Login page will only use `token` provider.' + ); + } + return settings; + }, ], }; export const plugin: PluginInitializer< diff --git a/x-pack/plugins/security/server/plugin.test.ts b/x-pack/plugins/security/server/plugin.test.ts index 56aad4ece3e95..6f5c79e873e86 100644 --- a/x-pack/plugins/security/server/plugin.test.ts +++ b/x-pack/plugins/security/server/plugin.test.ts @@ -28,6 +28,7 @@ describe('Security Plugin', () => { authc: { providers: ['saml', 'token'], saml: { realm: 'saml1', maxRedirectURLSize: new ByteSizeValue(2048) }, + http: { enabled: true, autoSchemesEnabled: true, schemes: ['apikey'] }, }, }) ); @@ -77,6 +78,7 @@ describe('Security Plugin', () => { "getSessionInfo": [Function], "invalidateAPIKey": [Function], "isAuthenticated": [Function], + "isProviderEnabled": [Function], "login": [Function], "logout": [Function], }, diff --git a/x-pack/plugins/security/server/routes/authentication/basic.test.ts b/x-pack/plugins/security/server/routes/authentication/basic.test.ts index be17b3e29f854..cc1c94d799be6 100644 --- a/x-pack/plugins/security/server/routes/authentication/basic.test.ts +++ b/x-pack/plugins/security/server/routes/authentication/basic.test.ts @@ -33,7 +33,9 @@ describe('Basic authentication routes', () => { let mockContext: RequestHandlerContext; beforeEach(() => { router = httpServiceMock.createRouter(); + authc = authenticationMock.create(); + authc.isProviderEnabled.mockImplementation(provider => provider === 'basic'); mockContext = ({ licensing: { @@ -166,6 +168,22 @@ describe('Basic authentication routes', () => { value: { username: 'user', password: 'password' }, }); }); + + it('prefers `token` authentication provider if it is enabled', async () => { + authc.login.mockResolvedValue(AuthenticationResult.succeeded(mockAuthenticatedUser())); + authc.isProviderEnabled.mockImplementation( + provider => provider === 'token' || provider === 'basic' + ); + + const response = await routeHandler(mockContext, mockRequest, kibanaResponseFactory); + + expect(response.status).toBe(204); + expect(response.payload).toBeUndefined(); + expect(authc.login).toHaveBeenCalledWith(mockRequest, { + provider: 'token', + value: { username: 'user', password: 'password' }, + }); + }); }); }); }); diff --git a/x-pack/plugins/security/server/routes/authentication/basic.ts b/x-pack/plugins/security/server/routes/authentication/basic.ts index 453dc1c4ea3b5..db36e45fc07e8 100644 --- a/x-pack/plugins/security/server/routes/authentication/basic.ts +++ b/x-pack/plugins/security/server/routes/authentication/basic.ts @@ -25,16 +25,13 @@ export function defineBasicRoutes({ router, authc, config }: RouteDefinitionPara options: { authRequired: false }, }, createLicensedRouteHandler(async (context, request, response) => { - const { username, password } = request.body; + // We should prefer `token` over `basic` if possible. + const loginAttempt = authc.isProviderEnabled('token') + ? { provider: 'token', value: request.body } + : { provider: 'basic', value: request.body }; try { - // We should prefer `token` over `basic` if possible. - const providerToLoginWith = config.authc.providers.includes('token') ? 'token' : 'basic'; - const authenticationResult = await authc.login(request, { - provider: providerToLoginWith, - value: { username, password }, - }); - + const authenticationResult = await authc.login(request, loginAttempt); if (!authenticationResult.succeeded()) { return response.unauthorized({ body: authenticationResult.error }); } diff --git a/x-pack/plugins/security/server/routes/authentication/index.ts b/x-pack/plugins/security/server/routes/authentication/index.ts index 6035025564cbf..a774edfb4ab2c 100644 --- a/x-pack/plugins/security/server/routes/authentication/index.ts +++ b/x-pack/plugins/security/server/routes/authentication/index.ts @@ -27,18 +27,15 @@ export function defineAuthenticationRoutes(params: RouteDefinitionParams) { defineSessionRoutes(params); defineCommonRoutes(params); - if ( - params.config.authc.providers.includes('basic') || - params.config.authc.providers.includes('token') - ) { + if (params.authc.isProviderEnabled('basic') || params.authc.isProviderEnabled('token')) { defineBasicRoutes(params); } - if (params.config.authc.providers.includes('saml')) { + if (params.authc.isProviderEnabled('saml')) { defineSAMLRoutes(params); } - if (params.config.authc.providers.includes('oidc')) { + if (params.authc.isProviderEnabled('oidc')) { defineOIDCRoutes(params); } } diff --git a/x-pack/plugins/spaces/server/capabilities/capabilities_provider.test.ts b/x-pack/plugins/spaces/server/capabilities/capabilities_provider.test.ts new file mode 100644 index 0000000000000..8678bdceb70f9 --- /dev/null +++ b/x-pack/plugins/spaces/server/capabilities/capabilities_provider.test.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { capabilitiesProvider } from './capabilities_provider'; + +describe('Capabilities provider', () => { + it('provides the expected capabilities', () => { + expect(capabilitiesProvider()).toMatchInlineSnapshot(` + Object { + "management": Object { + "kibana": Object { + "spaces": true, + }, + }, + "spaces": Object { + "manage": true, + }, + } + `); + }); +}); diff --git a/x-pack/legacy/plugins/ml/server/lib/security_utils.d.ts b/x-pack/plugins/spaces/server/capabilities/capabilities_provider.ts similarity index 61% rename from x-pack/legacy/plugins/ml/server/lib/security_utils.d.ts rename to x-pack/plugins/spaces/server/capabilities/capabilities_provider.ts index 26fdff73b3460..5976aabfa66e8 100644 --- a/x-pack/legacy/plugins/ml/server/lib/security_utils.d.ts +++ b/x-pack/plugins/spaces/server/capabilities/capabilities_provider.ts @@ -4,6 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { XPackMainPlugin } from '../../../xpack_main/server/xpack_main'; - -export function isSecurityDisabled(xpackMainPlugin: XPackMainPlugin): boolean; +export const capabilitiesProvider = () => ({ + spaces: { + manage: true, + }, + management: { + kibana: { + spaces: true, + }, + }, +}); diff --git a/x-pack/plugins/spaces/server/lib/toggle_ui_capabilities.test.ts b/x-pack/plugins/spaces/server/capabilities/capabilities_switcher.test.ts similarity index 53% rename from x-pack/plugins/spaces/server/lib/toggle_ui_capabilities.test.ts rename to x-pack/plugins/spaces/server/capabilities/capabilities_switcher.test.ts index b92922def2eb8..3f7b93c754aef 100644 --- a/x-pack/plugins/spaces/server/lib/toggle_ui_capabilities.test.ts +++ b/x-pack/plugins/spaces/server/capabilities/capabilities_switcher.test.ts @@ -6,8 +6,12 @@ import { Feature } from '../../../../plugins/features/server'; import { Space } from '../../common/model/space'; -import { toggleUICapabilities } from './toggle_ui_capabilities'; -import { Capabilities } from 'src/core/public'; +import { setupCapabilitiesSwitcher } from './capabilities_switcher'; +import { Capabilities, CoreSetup } from 'src/core/server'; +import { coreMock, httpServerMock, loggingServiceMock } from 'src/core/server/mocks'; +import { featuresPluginMock } from '../../../features/server/mocks'; +import { spacesServiceMock } from '../spaces_service/spaces_service.mock'; +import { PluginsStart } from '../plugin'; const features: Feature[] = [ { @@ -91,8 +95,33 @@ const buildCapabilities = () => }, }) as Capabilities; -describe('toggleUiCapabilities', () => { - it('does not toggle capabilities when the space has no disabled features', () => { +const setup = (space: Space) => { + const coreSetup = coreMock.createSetup(); + + const featuresStart = featuresPluginMock.createStart(); + featuresStart.getFeatures.mockReturnValue(features); + + coreSetup.getStartServices.mockResolvedValue([ + coreMock.createStart(), + { features: featuresStart }, + ]); + + const spacesService = spacesServiceMock.createSetupContract(); + spacesService.getActiveSpace.mockResolvedValue(space); + + const logger = loggingServiceMock.createLogger(); + + const switcher = setupCapabilitiesSwitcher( + (coreSetup as unknown) as CoreSetup, + spacesService, + logger + ); + + return { switcher, logger, spacesService }; +}; + +describe('capabilitiesSwitcher', () => { + it('does not toggle capabilities when the space has no disabled features', async () => { const space: Space = { id: 'space', name: '', @@ -100,11 +129,54 @@ describe('toggleUiCapabilities', () => { }; const capabilities = buildCapabilities(); - const result = toggleUICapabilities(features, capabilities, space); + + const { switcher } = setup(space); + const request = httpServerMock.createKibanaRequest(); + const result = await switcher(request, capabilities); + + expect(result).toEqual(buildCapabilities()); + }); + + it('does not toggle capabilities when the request is not authenticated', async () => { + const space: Space = { + id: 'space', + name: '', + disabledFeatures: ['feature_1', 'feature_2', 'feature_3'], + }; + + const capabilities = buildCapabilities(); + + const { switcher } = setup(space); + const request = httpServerMock.createKibanaRequest({ routeAuthRequired: false }); + + const result = await switcher(request, capabilities); + + expect(result).toEqual(buildCapabilities()); + }); + + it('logs a warning, and does not toggle capabilities if an error is encountered', async () => { + const space: Space = { + id: 'space', + name: '', + disabledFeatures: ['feature_1', 'feature_2', 'feature_3'], + }; + + const capabilities = buildCapabilities(); + + const { switcher, logger, spacesService } = setup(space); + const request = httpServerMock.createKibanaRequest(); + + spacesService.getActiveSpace.mockRejectedValue(new Error('Something terrible happened')); + + const result = await switcher(request, capabilities); + expect(result).toEqual(buildCapabilities()); + expect(logger.warn).toHaveBeenCalledWith( + `Error toggling capabilities for request to /path: Error: Something terrible happened` + ); }); - it('ignores unknown disabledFeatures', () => { + it('ignores unknown disabledFeatures', async () => { const space: Space = { id: 'space', name: '', @@ -112,11 +184,15 @@ describe('toggleUiCapabilities', () => { }; const capabilities = buildCapabilities(); - const result = toggleUICapabilities(features, capabilities, space); + + const { switcher } = setup(space); + const request = httpServerMock.createKibanaRequest(); + const result = await switcher(request, capabilities); + expect(result).toEqual(buildCapabilities()); }); - it('disables the corresponding navLink, catalogue, management sections, and all capability flags for disabled features', () => { + it('disables the corresponding navLink, catalogue, management sections, and all capability flags for disabled features', async () => { const space: Space = { id: 'space', name: '', @@ -124,7 +200,10 @@ describe('toggleUiCapabilities', () => { }; const capabilities = buildCapabilities(); - const result = toggleUICapabilities(features, capabilities, space); + + const { switcher } = setup(space); + const request = httpServerMock.createKibanaRequest(); + const result = await switcher(request, capabilities); const expectedCapabilities = buildCapabilities(); @@ -137,7 +216,7 @@ describe('toggleUiCapabilities', () => { expect(result).toEqual(expectedCapabilities); }); - it('can disable everything', () => { + it('can disable everything', async () => { const space: Space = { id: 'space', name: '', @@ -145,7 +224,10 @@ describe('toggleUiCapabilities', () => { }; const capabilities = buildCapabilities(); - const result = toggleUICapabilities(features, capabilities, space); + + const { switcher } = setup(space); + const request = httpServerMock.createKibanaRequest(); + const result = await switcher(request, capabilities); const expectedCapabilities = buildCapabilities(); diff --git a/x-pack/plugins/spaces/server/lib/toggle_ui_capabilities.ts b/x-pack/plugins/spaces/server/capabilities/capabilities_switcher.ts similarity index 66% rename from x-pack/plugins/spaces/server/lib/toggle_ui_capabilities.ts rename to x-pack/plugins/spaces/server/capabilities/capabilities_switcher.ts index 2de84ec05017b..317cc7fe0e3c3 100644 --- a/x-pack/plugins/spaces/server/lib/toggle_ui_capabilities.ts +++ b/x-pack/plugins/spaces/server/capabilities/capabilities_switcher.ts @@ -4,15 +4,41 @@ * you may not use this file except in compliance with the Elastic License. */ import _ from 'lodash'; -import { UICapabilities } from 'ui/capabilities'; +import { Capabilities, CapabilitiesSwitcher, CoreSetup, Logger } from 'src/core/server'; import { Feature } from '../../../../plugins/features/server'; import { Space } from '../../common/model/space'; +import { SpacesServiceSetup } from '../spaces_service'; +import { PluginsStart } from '../plugin'; -export function toggleUICapabilities( - features: Feature[], - capabilities: UICapabilities, - activeSpace: Space -) { +export function setupCapabilitiesSwitcher( + core: CoreSetup, + spacesService: SpacesServiceSetup, + logger: Logger +): CapabilitiesSwitcher { + return async (request, capabilities) => { + const isAnonymousRequest = !request.route.options.authRequired; + + if (isAnonymousRequest) { + return capabilities; + } + + try { + const [activeSpace, [, { features }]] = await Promise.all([ + spacesService.getActiveSpace(request), + core.getStartServices(), + ]); + + const registeredFeatures = features.getFeatures(); + + return toggleCapabilities(registeredFeatures, capabilities, activeSpace); + } catch (e) { + logger.warn(`Error toggling capabilities for request to ${request.url.pathname}: ${e}`); + return capabilities; + } + }; +} + +function toggleCapabilities(features: Feature[], capabilities: Capabilities, activeSpace: Space) { const clonedCapabilities = _.cloneDeep(capabilities); toggleDisabledFeatures(features, clonedCapabilities, activeSpace); @@ -22,7 +48,7 @@ export function toggleUICapabilities( function toggleDisabledFeatures( features: Feature[], - capabilities: UICapabilities, + capabilities: Capabilities, activeSpace: Space ) { const disabledFeatureKeys = activeSpace.disabledFeatures; diff --git a/x-pack/plugins/spaces/server/capabilities/index.ts b/x-pack/plugins/spaces/server/capabilities/index.ts new file mode 100644 index 0000000000000..56a72a2eeaf19 --- /dev/null +++ b/x-pack/plugins/spaces/server/capabilities/index.ts @@ -0,0 +1,20 @@ +/* + * 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, Logger } from 'src/core/server'; +import { capabilitiesProvider } from './capabilities_provider'; +import { setupCapabilitiesSwitcher } from './capabilities_switcher'; +import { PluginsStart } from '../plugin'; +import { SpacesServiceSetup } from '../spaces_service'; + +export const setupCapabilities = ( + core: CoreSetup, + spacesService: SpacesServiceSetup, + logger: Logger +) => { + core.capabilities.registerProvider(capabilitiesProvider); + core.capabilities.registerSwitcher(setupCapabilitiesSwitcher(core, spacesService, logger)); +}; diff --git a/x-pack/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.test.ts b/x-pack/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.test.ts index 92be88b91c652..61157a9318781 100644 --- a/x-pack/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.test.ts +++ b/x-pack/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.test.ts @@ -201,12 +201,10 @@ describe('onPostAuthInterceptor', () => { // interceptor to parse out the space id and rewrite the request's URL. Rather than duplicating that logic, // we are including the already tested interceptor here in the test chain. initSpacesOnRequestInterceptor({ - getLegacyAPI: () => legacyAPI, http: (http as unknown) as CoreSetup['http'], }); initSpacesOnPostAuthRequestInterceptor({ - getLegacyAPI: () => legacyAPI, http: (http as unknown) as CoreSetup['http'], log: loggingMock, features: featuresPlugin, diff --git a/x-pack/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.ts b/x-pack/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.ts index 4674f3641084a..b07ff11f6efc6 100644 --- a/x-pack/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.ts +++ b/x-pack/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.ts @@ -7,13 +7,12 @@ import { Logger, CoreSetup } from 'src/core/server'; import { Space } from '../../../common/model/space'; import { wrapError } from '../errors'; import { SpacesServiceSetup } from '../../spaces_service/spaces_service'; -import { LegacyAPI, PluginsSetup } from '../../plugin'; +import { PluginsSetup } from '../../plugin'; import { getSpaceSelectorUrl } from '../get_space_selector_url'; import { DEFAULT_SPACE_ID, ENTER_SPACE_PATH } from '../../../common/constants'; import { addSpaceIdToPath } from '../../../common'; export interface OnPostAuthInterceptorDeps { - getLegacyAPI(): LegacyAPI; http: CoreSetup['http']; features: PluginsSetup['features']; spacesService: SpacesServiceSetup; @@ -22,7 +21,6 @@ export interface OnPostAuthInterceptorDeps { export function initSpacesOnPostAuthRequestInterceptor({ features, - getLegacyAPI, spacesService, log, http, diff --git a/x-pack/plugins/spaces/server/lib/request_interceptors/on_request_interceptor.test.ts b/x-pack/plugins/spaces/server/lib/request_interceptors/on_request_interceptor.test.ts index 5e6cf67ee8c90..448bc39eb606e 100644 --- a/x-pack/plugins/spaces/server/lib/request_interceptors/on_request_interceptor.test.ts +++ b/x-pack/plugins/spaces/server/lib/request_interceptors/on_request_interceptor.test.ts @@ -16,7 +16,6 @@ import { } from '../../../../../../src/core/server'; import * as kbnTestServer from '../../../../../../src/test_utils/kbn_server'; -import { LegacyAPI } from '../../plugin'; import { elasticsearchServiceMock } from 'src/core/server/mocks'; describe('onRequestInterceptor', () => { @@ -110,10 +109,6 @@ describe('onRequestInterceptor', () => { elasticsearch.esNodesCompatibility$ = elasticsearchServiceMock.createInternalSetup().esNodesCompatibility$; initSpacesOnRequestInterceptor({ - getLegacyAPI: () => - ({ - legacyConfig: {}, - } as LegacyAPI), http: (http as unknown) as CoreSetup['http'], }); diff --git a/x-pack/plugins/spaces/server/lib/request_interceptors/on_request_interceptor.ts b/x-pack/plugins/spaces/server/lib/request_interceptors/on_request_interceptor.ts index 22d704c1b7e13..c59851f8b8061 100644 --- a/x-pack/plugins/spaces/server/lib/request_interceptors/on_request_interceptor.ts +++ b/x-pack/plugins/spaces/server/lib/request_interceptors/on_request_interceptor.ts @@ -12,14 +12,12 @@ import { import { format } from 'url'; import { DEFAULT_SPACE_ID } from '../../../common/constants'; import { modifyUrl } from '../utils/url'; -import { LegacyAPI } from '../../plugin'; import { getSpaceIdFromPath } from '../../../common'; export interface OnRequestInterceptorDeps { - getLegacyAPI(): LegacyAPI; http: CoreSetup['http']; } -export function initSpacesOnRequestInterceptor({ getLegacyAPI, http }: OnRequestInterceptorDeps) { +export function initSpacesOnRequestInterceptor({ http }: OnRequestInterceptorDeps) { http.registerOnPreAuth(async function spacesOnPreAuthHandler( request: KibanaRequest, response: LifecycleResponseFactory, diff --git a/x-pack/plugins/spaces/server/lib/spaces_tutorial_context_factory.test.ts b/x-pack/plugins/spaces/server/lib/spaces_tutorial_context_factory.test.ts index a3396e98c3512..094ca8a11816e 100644 --- a/x-pack/plugins/spaces/server/lib/spaces_tutorial_context_factory.test.ts +++ b/x-pack/plugins/spaces/server/lib/spaces_tutorial_context_factory.test.ts @@ -23,7 +23,6 @@ import { securityMock } from '../../../security/server/mocks'; const log = loggingServiceMock.createLogger(); const legacyAPI: LegacyAPI = { - legacyConfig: {}, savedObjects: {} as SavedObjectsLegacyService, } as LegacyAPI; diff --git a/x-pack/plugins/spaces/server/plugin.test.ts b/x-pack/plugins/spaces/server/plugin.test.ts new file mode 100644 index 0000000000000..4e3f4f52cbeb4 --- /dev/null +++ b/x-pack/plugins/spaces/server/plugin.test.ts @@ -0,0 +1,72 @@ +/* + * 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 } from 'src/core/server'; +import { coreMock } from 'src/core/server/mocks'; +import { featuresPluginMock } from '../../features/server/mocks'; +import { licensingMock } from '../../licensing/server/mocks'; +import { Plugin, PluginsSetup } from './plugin'; +import { usageCollectionPluginMock } from '../../../../src/plugins/usage_collection/server/mocks'; + +describe('Spaces Plugin', () => { + describe('#setup', () => { + it('can setup with all optional plugins disabled, exposing the expected contract', async () => { + const initializerContext = coreMock.createPluginInitializerContext({}); + const core = coreMock.createSetup() as CoreSetup; + const features = featuresPluginMock.createSetup(); + const licensing = licensingMock.createSetup(); + + const plugin = new Plugin(initializerContext); + const spacesSetup = await plugin.setup(core, { features, licensing }); + expect(spacesSetup).toMatchInlineSnapshot(` + Object { + "__legacyCompat": Object { + "createDefaultSpace": [Function], + "registerLegacyAPI": [Function], + }, + "spacesService": Object { + "getActiveSpace": [Function], + "getBasePath": [Function], + "getSpaceId": [Function], + "isInDefaultSpace": [Function], + "namespaceToSpaceId": [Function], + "scopedClient": [Function], + "spaceIdToNamespace": [Function], + }, + } + `); + }); + + it('registers the capabilities provider and switcher', async () => { + const initializerContext = coreMock.createPluginInitializerContext({}); + const core = coreMock.createSetup() as CoreSetup; + const features = featuresPluginMock.createSetup(); + const licensing = licensingMock.createSetup(); + + const plugin = new Plugin(initializerContext); + + await plugin.setup(core, { features, licensing }); + + expect(core.capabilities.registerProvider).toHaveBeenCalledTimes(1); + expect(core.capabilities.registerSwitcher).toHaveBeenCalledTimes(1); + }); + + it('registers the usage collector', async () => { + const initializerContext = coreMock.createPluginInitializerContext({}); + const core = coreMock.createSetup() as CoreSetup; + const features = featuresPluginMock.createSetup(); + const licensing = licensingMock.createSetup(); + + const usageCollection = usageCollectionPluginMock.createSetupContract(); + + const plugin = new Plugin(initializerContext); + + await plugin.setup(core, { features, licensing, usageCollection }); + + expect(usageCollection.getCollectorByType('spaces')).toBeDefined(); + }); + }); +}); diff --git a/x-pack/plugins/spaces/server/plugin.ts b/x-pack/plugins/spaces/server/plugin.ts index 90c2da6e69df8..d125e0f54e9c1 100644 --- a/x-pack/plugins/spaces/server/plugin.ts +++ b/x-pack/plugins/spaces/server/plugin.ts @@ -13,7 +13,10 @@ import { Logger, PluginInitializerContext, } from '../../../../src/core/server'; -import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server'; +import { + PluginSetupContract as FeaturesPluginSetup, + PluginStartContract as FeaturesPluginStart, +} from '../../features/server'; import { SecurityPluginSetup } from '../../security/server'; import { LicensingPluginSetup } from '../../licensing/server'; import { createDefaultSpace } from './lib/create_default_space'; @@ -22,15 +25,15 @@ import { AuditLogger } from '../../../../server/lib/audit_logger'; import { spacesSavedObjectsClientWrapperFactory } from './lib/saved_objects_client/saved_objects_client_wrapper_factory'; import { SpacesAuditLogger } from './lib/audit_logger'; import { createSpacesTutorialContextFactory } from './lib/spaces_tutorial_context_factory'; -import { registerSpacesUsageCollector } from './lib/spaces_usage_collector'; +import { registerSpacesUsageCollector } from './usage_collection'; import { SpacesService } from './spaces_service'; import { SpacesServiceSetup } from './spaces_service'; import { ConfigType } from './config'; -import { toggleUICapabilities } from './lib/toggle_ui_capabilities'; import { initSpacesRequestInterceptors } from './lib/request_interceptors'; import { initExternalSpacesApi } from './routes/api/external'; import { initInternalSpacesApi } from './routes/api/internal'; import { initSpacesViewsRoutes } from './routes/views'; +import { setupCapabilities } from './capabilities'; /** * Describes a set of APIs that is available in the legacy platform only and required by this plugin @@ -41,9 +44,6 @@ export interface LegacyAPI { auditLogger: { create: (pluginId: string) => AuditLogger; }; - legacyConfig: { - kibanaIndex: string; - }; } export interface PluginsSetup { @@ -54,6 +54,10 @@ export interface PluginsSetup { home?: HomeServerPluginSetup; } +export interface PluginsStart { + features: FeaturesPluginStart; +} + export interface SpacesPluginSetup { spacesService: SpacesServiceSetup; __legacyCompat: { @@ -70,6 +74,8 @@ export class Plugin { private readonly config$: Observable; + private readonly kibanaIndexConfig$: Observable<{ kibana: { index: string } }>; + private readonly log: Logger; private legacyAPI?: LegacyAPI; @@ -92,12 +98,16 @@ export class Plugin { constructor(initializerContext: PluginInitializerContext) { this.config$ = initializerContext.config.create(); + this.kibanaIndexConfig$ = initializerContext.config.legacy.globalConfig$; this.log = initializerContext.logger.get(); } public async start() {} - public async setup(core: CoreSetup, plugins: PluginsSetup): Promise { + public async setup( + core: CoreSetup, + plugins: PluginsSetup + ): Promise { const service = new SpacesService(this.log, this.getLegacyAPI); const spacesService = await service.setup({ @@ -131,20 +141,19 @@ export class Plugin { initSpacesRequestInterceptors({ http: core.http, log: this.log, - getLegacyAPI: this.getLegacyAPI, spacesService, features: plugins.features, }); - core.capabilities.registerSwitcher(async (request, uiCapabilities) => { - try { - const activeSpace = await spacesService.getActiveSpace(request); - const features = plugins.features.getFeatures(); - return toggleUICapabilities(features, uiCapabilities, activeSpace); - } catch (e) { - return uiCapabilities; - } - }); + setupCapabilities(core, spacesService, this.log); + + if (plugins.usageCollection) { + registerSpacesUsageCollector(plugins.usageCollection, { + kibanaIndexConfig$: this.kibanaIndexConfig$, + features: plugins.features, + licensing: plugins.licensing, + }); + } if (plugins.security) { plugins.security.registerSpacesService(spacesService); @@ -161,12 +170,7 @@ export class Plugin { __legacyCompat: { registerLegacyAPI: (legacyAPI: LegacyAPI) => { this.legacyAPI = legacyAPI; - this.setupLegacyComponents( - spacesService, - plugins.features, - plugins.licensing, - plugins.usageCollection - ); + this.setupLegacyComponents(spacesService); }, createDefaultSpace: async () => { return await createDefaultSpace({ @@ -180,12 +184,7 @@ export class Plugin { public stop() {} - private setupLegacyComponents( - spacesService: SpacesServiceSetup, - featuresSetup: FeaturesPluginSetup, - licensingSetup: LicensingPluginSetup, - usageCollectionSetup?: UsageCollectionSetup - ) { + private setupLegacyComponents(spacesService: SpacesServiceSetup) { const legacyAPI = this.getLegacyAPI(); const { addScopedSavedObjectsClientWrapperFactory, types } = legacyAPI.savedObjects; addScopedSavedObjectsClientWrapperFactory( @@ -193,11 +192,5 @@ export class Plugin { 'spaces', spacesSavedObjectsClientWrapperFactory(spacesService, types) ); - // Register a function with server to manage the collection of usage stats - registerSpacesUsageCollector(usageCollectionSetup, { - kibanaIndex: legacyAPI.legacyConfig.kibanaIndex, - features: featuresSetup, - licensing: licensingSetup, - }); } } diff --git a/x-pack/plugins/spaces/server/routes/api/__fixtures__/create_legacy_api.ts b/x-pack/plugins/spaces/server/routes/api/__fixtures__/create_legacy_api.ts index 812b02e94f591..7765cc3c52e96 100644 --- a/x-pack/plugins/spaces/server/routes/api/__fixtures__/create_legacy_api.ts +++ b/x-pack/plugins/spaces/server/routes/api/__fixtures__/create_legacy_api.ts @@ -100,9 +100,6 @@ export const createLegacyAPI = ({ } as unknown) as jest.Mocked; const legacyAPI: jest.Mocked = { - legacyConfig: { - kibanaIndex: '', - }, auditLogger: {} as any, savedObjects: savedObjectsService, }; diff --git a/x-pack/plugins/spaces/server/spaces_service/spaces_service.test.ts b/x-pack/plugins/spaces/server/spaces_service/spaces_service.test.ts index 68d096e046ed4..fc5ff39780524 100644 --- a/x-pack/plugins/spaces/server/spaces_service/spaces_service.test.ts +++ b/x-pack/plugins/spaces/server/spaces_service/spaces_service.test.ts @@ -28,7 +28,6 @@ const mockLogger = loggingServiceMock.createLogger(); const createService = async (serverBasePath: string = '') => { const legacyAPI = { - legacyConfig: {}, savedObjects: ({ getSavedObjectsRepository: jest.fn().mockReturnValue({ get: jest.fn().mockImplementation((type, id) => { diff --git a/x-pack/legacy/plugins/transform/public/app/services/documentation/index.ts b/x-pack/plugins/spaces/server/usage_collection/index.ts similarity index 76% rename from x-pack/legacy/plugins/transform/public/app/services/documentation/index.ts rename to x-pack/plugins/spaces/server/usage_collection/index.ts index 34209d10c6b8b..01df2b815f5ff 100644 --- a/x-pack/legacy/plugins/transform/public/app/services/documentation/index.ts +++ b/x-pack/plugins/spaces/server/usage_collection/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { documentationLinksService } from './documentation_links'; +export { registerSpacesUsageCollector } from './spaces_usage_collector'; diff --git a/x-pack/plugins/spaces/server/lib/spaces_usage_collector.test.ts b/x-pack/plugins/spaces/server/usage_collection/spaces_usage_collector.test.ts similarity index 85% rename from x-pack/plugins/spaces/server/lib/spaces_usage_collector.test.ts rename to x-pack/plugins/spaces/server/usage_collection/spaces_usage_collector.test.ts index c0a6a152c8322..57ec688ab70e8 100644 --- a/x-pack/plugins/spaces/server/lib/spaces_usage_collector.test.ts +++ b/x-pack/plugins/spaces/server/usage_collection/spaces_usage_collector.test.ts @@ -9,6 +9,7 @@ import * as Rx from 'rxjs'; import { PluginsSetup } from '../plugin'; import { Feature } from '../../../features/server'; import { ILicense, LicensingPluginSetup } from '../../../licensing/server'; +import { pluginInitializerContextConfigMock } from 'src/core/server/mocks'; interface SetupOpts { license?: Partial; @@ -72,7 +73,7 @@ describe('error handling', () => { license: { isAvailable: true, type: 'basic' }, }); const { fetch: getSpacesUsage } = getSpacesUsageCollector(usageCollecion as any, { - kibanaIndex: '.kibana', + kibanaIndexConfig$: Rx.of({ kibana: { index: '.kibana' } }), features, licensing, }); @@ -85,7 +86,7 @@ describe('error handling', () => { license: { isAvailable: true, type: 'basic' }, }); const { fetch: getSpacesUsage } = getSpacesUsageCollector(usageCollecion as any, { - kibanaIndex: '.kibana', + kibanaIndexConfig$: Rx.of({ kibana: { index: '.kibana' } }), features, licensing, }); @@ -105,11 +106,25 @@ describe('with a basic license', () => { license: { isAvailable: true, type: 'basic' }, }); const { fetch: getSpacesUsage } = getSpacesUsageCollector(usageCollecion as any, { - kibanaIndex: '.kibana', + kibanaIndexConfig$: pluginInitializerContextConfigMock({}).legacy.globalConfig$, features, licensing, }); usageStats = await getSpacesUsage(defaultCallClusterMock); + + expect(defaultCallClusterMock).toHaveBeenCalledWith('search', { + body: { + aggs: { + disabledFeatures: { + terms: { field: 'space.disabledFeatures', include: ['feature1', 'feature2'], size: 2 }, + }, + }, + query: { term: { type: { value: 'space' } } }, + size: 0, + track_total_hits: true, + }, + index: '.kibana-tests', + }); }); test('sets enabled to true', () => { @@ -139,7 +154,7 @@ describe('with no license', () => { beforeAll(async () => { const { features, licensing, usageCollecion } = setup({ license: { isAvailable: false } }); const { fetch: getSpacesUsage } = getSpacesUsageCollector(usageCollecion as any, { - kibanaIndex: '.kibana', + kibanaIndexConfig$: pluginInitializerContextConfigMock({}).legacy.globalConfig$, features, licensing, }); @@ -170,7 +185,7 @@ describe('with platinum license', () => { license: { isAvailable: true, type: 'platinum' }, }); const { fetch: getSpacesUsage } = getSpacesUsageCollector(usageCollecion as any, { - kibanaIndex: '.kibana', + kibanaIndexConfig$: pluginInitializerContextConfigMock({}).legacy.globalConfig$, features, licensing, }); diff --git a/x-pack/plugins/spaces/server/lib/spaces_usage_collector.ts b/x-pack/plugins/spaces/server/usage_collection/spaces_usage_collector.ts similarity index 90% rename from x-pack/plugins/spaces/server/lib/spaces_usage_collector.ts rename to x-pack/plugins/spaces/server/usage_collection/spaces_usage_collector.ts index af77f2d3a72ba..90187b7853185 100644 --- a/x-pack/plugins/spaces/server/lib/spaces_usage_collector.ts +++ b/x-pack/plugins/spaces/server/usage_collection/spaces_usage_collector.ts @@ -4,11 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { get } from 'lodash'; import { CallAPIOptions } from 'src/core/server'; import { take } from 'rxjs/operators'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; -// @ts-ignore +import { Observable } from 'rxjs'; import { KIBANA_STATS_TYPE_MONITORING } from '../../../../legacy/plugins/monitoring/common/constants'; import { KIBANA_SPACES_STATS_TYPE } from '../../common/constants'; import { PluginsSetup } from '../plugin'; @@ -85,8 +84,8 @@ async function getSpacesUsage( const { hits, aggregations } = resp!; - const count = get(hits, 'total.value', 0); - const disabledFeatureBuckets = get(aggregations, 'disabledFeatures.buckets', []); + const count = hits?.total?.value ?? 0; + const disabledFeatureBuckets = aggregations?.disabledFeatures?.buckets ?? []; const initialCounts = knownFeatureIds.reduce( (acc, featureId) => ({ ...acc, [featureId]: 0 }), @@ -125,7 +124,7 @@ export interface UsageStats { } interface CollectorDeps { - kibanaIndex: string; + kibanaIndexConfig$: Observable<{ kibana: { index: string } }>; features: PluginsSetup['features']; licensing: PluginsSetup['licensing']; } @@ -145,12 +144,9 @@ export function getSpacesUsageCollector( const license = await deps.licensing.license$.pipe(take(1)).toPromise(); const available = license.isAvailable; // some form of spaces is available for all valid licenses - const usageStats = await getSpacesUsage( - callCluster, - deps.kibanaIndex, - deps.features, - available - ); + const kibanaIndex = (await deps.kibanaIndexConfig$.pipe(take(1)).toPromise()).kibana.index; + + const usageStats = await getSpacesUsage(callCluster, kibanaIndex, deps.features, available); return { available, @@ -178,12 +174,9 @@ export function getSpacesUsageCollector( } export function registerSpacesUsageCollector( - usageCollection: UsageCollectionSetup | undefined, + usageCollection: UsageCollectionSetup, deps: CollectorDeps ) { - if (!usageCollection) { - return; - } const collector = getSpacesUsageCollector(usageCollection, deps); usageCollection.registerCollector(collector); } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/home.tsx b/x-pack/plugins/triggers_actions_ui/public/application/home.tsx index ba30b838a8593..b478a9f0ced8b 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/home.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/home.tsx @@ -16,8 +16,10 @@ import { EuiTab, EuiTabs, EuiTitle, + EuiBetaBadge, } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { BASE_PATH, Section, routeToConnectors, routeToAlerts } from './constants'; import { getCurrentBreadcrumb } from './lib/breadcrumb'; import { getCurrentDocTitle } from './lib/doc_title'; @@ -91,6 +93,17 @@ export const TriggersActionsUIHome: React.FunctionComponent +   + diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.tsx index 0749ae1d30e9e..1eabf2441da4f 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.tsx @@ -17,6 +17,7 @@ import { EuiButtonEmpty, EuiButton, EuiFlyoutBody, + EuiBetaBadge, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { useActionsConnectorsContext } from '../../context/actions_connectors_context'; @@ -146,6 +147,17 @@ export const ConnectorAddFlyout = () => { actionTypeName: actionType.name, }} /> +   + @@ -159,6 +171,17 @@ export const ConnectorAddFlyout = () => { defaultMessage="Select a connector" id="xpack.triggersActionsUI.sections.addConnectorForm.selectConnectorFlyoutTitle" /> +   + )} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.tsx index 55386ec6d61f9..6486292725660 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.tsx @@ -5,7 +5,7 @@ */ import React, { useCallback, useReducer, useState } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiTitle, EuiFlexItem, EuiIcon, EuiFlexGroup } from '@elastic/eui'; +import { EuiTitle, EuiFlexItem, EuiIcon, EuiFlexGroup, EuiBetaBadge } from '@elastic/eui'; import { EuiModal, EuiButton, @@ -129,6 +129,17 @@ export const ConnectorAddModal = ({ actionTypeName: actionType.name, }} /> +   + diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.tsx index f7ad6f95d048f..6fe555fd74b39 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.tsx @@ -16,6 +16,7 @@ import { EuiFlyoutFooter, EuiButtonEmpty, EuiButton, + EuiBetaBadge, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { useActionsConnectorsContext } from '../../context/actions_connectors_context'; @@ -96,6 +97,17 @@ export const ConnectorEditFlyout = ({ initialConnector }: ConnectorEditProps) => defaultMessage="Edit connector" id="xpack.triggersActionsUI.sections.editConnectorForm.flyoutTitle" /> +   + diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.tsx index bed285f668e01..f48e27791419d 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.tsx @@ -14,6 +14,7 @@ import { EuiEmptyPrompt, EuiTitle, EuiLink, + EuiLoadingSpinner, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -370,8 +371,9 @@ export const ActionsConnectorsList: React.FunctionComponent = () => { /> {/* Render the view based on if there's data or if they can save */} + {(isLoadingActions || isLoadingActionTypes) && } {data.length !== 0 && table} - {data.length === 0 && canSave && emptyPrompt} + {data.length === 0 && canSave && !isLoadingActions && !isLoadingActionTypes && emptyPrompt} {data.length === 0 && !canSave && noPermissionPrompt} +   + diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx index d2cf2decc4a16..2625768dc7242 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx @@ -8,9 +8,17 @@ import uuid from 'uuid'; import { shallow } from 'enzyme'; import { AlertDetails } from './alert_details'; import { Alert, ActionType } from '../../../../types'; -import { EuiTitle, EuiBadge, EuiFlexItem, EuiButtonEmpty, EuiSwitch } from '@elastic/eui'; +import { + EuiTitle, + EuiBadge, + EuiFlexItem, + EuiButtonEmpty, + EuiSwitch, + EuiBetaBadge, +} from '@elastic/eui'; import { times, random } from 'lodash'; import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; jest.mock('../../../app_context', () => ({ useAppDependencies: jest.fn(() => ({ @@ -54,7 +62,19 @@ describe('alert_details', () => { ).containsMatchingElement( -

{alert.name}

+

+ {alert.name} +   + +

) ).toBeTruthy(); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx index 9c3b69962879f..1952e35c22924 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx @@ -21,8 +21,10 @@ import { EuiSwitch, EuiCallOut, EuiSpacer, + EuiBetaBadge, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; import { useAppDependencies } from '../../../app_context'; import { hasSaveAlertsCapability } from '../../../lib/capabilities'; import { Alert, AlertType, ActionType } from '../../../../types'; @@ -66,7 +68,20 @@ export const AlertDetails: React.FunctionComponent = ({ -

{alert.name}

+

+ {alert.name} +   + +

diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx index d9ccb84452e47..49e25dfbbf957 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx @@ -17,6 +17,7 @@ import { EuiSpacer, EuiEmptyPrompt, EuiLink, + EuiLoadingSpinner, } from '@elastic/eui'; import { useHistory } from 'react-router-dom'; @@ -389,7 +390,10 @@ export const AlertsList: React.FunctionComponent = () => { {convertAlertsToTableItems(alertsState.data, alertTypesState.data).length !== 0 && table} {convertAlertsToTableItems(alertsState.data, alertTypesState.data).length === 0 && + !alertTypesState.isLoading && + !alertsState.isLoading && emptyPrompt} + {(alertTypesState.isLoading || alertsState.isLoading) && } { + // FLAKY: https://github.com/elastic/kibana/issues/58785 + describe.skip('Privileges', () => { describe('GET /api/security/privileges', () => { it('should return a privilege map with all known privileges, without actions', async () => { await supertest diff --git a/x-pack/test/api_integration/apis/security/session.ts b/x-pack/test/api_integration/apis/security/session.ts index d819dd38dddb1..ef7e48388ff66 100644 --- a/x-pack/test/api_integration/apis/security/session.ts +++ b/x-pack/test/api_integration/apis/security/session.ts @@ -9,7 +9,7 @@ import expect from '@kbn/expect/expect.js'; import { FtrProviderContext } from '../../ftr_provider_context'; export default function({ getService }: FtrProviderContext) { - const supertest = getService('supertest'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); const config = getService('config'); const kibanaServerConfig = config.get('servers.kibana'); @@ -25,7 +25,7 @@ export default function({ getService }: FtrProviderContext) { return response; }; const getSessionInfo = async () => - supertest + supertestWithoutAuth .get('/internal/security/session') .set('kbn-xsrf', 'xxx') .set('kbn-system-request', 'true') @@ -33,7 +33,7 @@ export default function({ getService }: FtrProviderContext) { .send() .expect(200); const extendSession = async () => - supertest + supertestWithoutAuth .post('/internal/security/session') .set('kbn-xsrf', 'xxx') .set('Cookie', sessionCookie.cookieString()) @@ -42,7 +42,7 @@ export default function({ getService }: FtrProviderContext) { .then(saveCookie); beforeEach(async () => { - await supertest + await supertestWithoutAuth .post('/internal/security/login') .set('kbn-xsrf', 'xxx') .send({ username: validUsername, password: validPassword }) diff --git a/x-pack/test/functional/page_objects/uptime_page.ts b/x-pack/test/functional/page_objects/uptime_page.ts index a5bd4cc480287..f6e93cd14e497 100644 --- a/x-pack/test/functional/page_objects/uptime_page.ts +++ b/x-pack/test/functional/page_objects/uptime_page.ts @@ -57,7 +57,7 @@ export function UptimePageProvider({ getPageObjects, getService }: FtrProviderCo } public async pageUrlContains(value: string, expected: boolean = true) { - retry.try(async () => { + await retry.try(async () => { expect(await uptimeService.urlContains(value)).to.eql(expected); }); } diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/home_page.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/home_page.ts index 307f39382a236..f049406b639c7 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/home_page.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/home_page.ts @@ -23,7 +23,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await log.debug('Checking for section heading to say Triggers and Actions.'); const headingText = await pageObjects.triggersActionsUI.getSectionHeadingText(); - expect(headingText).to.be('Alerts and Actions'); + expect(headingText).to.be('Alerts and Actions BETA'); }); describe('Connectors tab', () => {