diff --git a/docs/user/security/audit-logging.asciidoc b/docs/user/security/audit-logging.asciidoc
index 33e1c2b5235110..1e7eb1971af087 100644
--- a/docs/user/security/audit-logging.asciidoc
+++ b/docs/user/security/audit-logging.asciidoc
@@ -53,8 +53,11 @@ Refer to the corresponding {es} logs for potential write errors.
| `user_logout`
| `unknown` | User is logging out.
+| `session_cleanup`
+| `unknown` | Removing invalid or expired session.
+
| `access_agreement_acknowledged`
-| N/A | User has acknowledged the access agreement.
+| n/a | User has acknowledged the access agreement.
3+a|
===== Category: database
diff --git a/package.json b/package.json
index 79a2f47d25ba76..ed36f3277ee643 100644
--- a/package.json
+++ b/package.json
@@ -100,8 +100,8 @@
"@dnd-kit/core": "^3.1.1",
"@dnd-kit/sortable": "^4.0.0",
"@dnd-kit/utilities": "^2.0.0",
- "@elastic/apm-rum": "^5.10.0",
- "@elastic/apm-rum-react": "^1.3.2",
+ "@elastic/apm-rum": "^5.10.1",
+ "@elastic/apm-rum-react": "^1.3.3",
"@elastic/apm-synthtrace": "link:bazel-bin/packages/elastic-apm-synthtrace",
"@elastic/charts": "40.2.0",
"@elastic/datemath": "link:bazel-bin/packages/elastic-datemath",
@@ -602,6 +602,7 @@
"@types/kbn__server-route-repository": "link:bazel-bin/packages/kbn-server-route-repository/npm_module_types",
"@types/kbn__std": "link:bazel-bin/packages/kbn-std/npm_module_types",
"@types/kbn__telemetry-tools": "link:bazel-bin/packages/kbn-telemetry-tools/npm_module_types",
+ "@types/kbn__ui-shared-deps-npm": "link:bazel-bin/packages/kbn-ui-shared-deps-npm/npm_module_types",
"@types/kbn__utility-types": "link:bazel-bin/packages/kbn-utility-types/npm_module_types",
"@types/kbn__utils": "link:bazel-bin/packages/kbn-utils/npm_module_types",
"@types/license-checker": "15.0.0",
diff --git a/packages/BUILD.bazel b/packages/BUILD.bazel
index 3580bcfbf65711..7c8259b2f6857e 100644
--- a/packages/BUILD.bazel
+++ b/packages/BUILD.bazel
@@ -77,6 +77,7 @@ filegroup(
srcs = [
"//packages/elastic-apm-synthtrace:build_types",
"//packages/elastic-datemath:build_types",
+ "//packages/elastic-safer-lodash-set:build_types",
"//packages/kbn-ace:build_types",
"//packages/kbn-alerts:build_types",
"//packages/kbn-analytics:build_types",
@@ -119,6 +120,7 @@ filegroup(
"//packages/kbn-server-route-repository:build_types",
"//packages/kbn-std:build_types",
"//packages/kbn-telemetry-tools:build_types",
+ "//packages/kbn-ui-shared-deps-npm:build_types",
"//packages/kbn-utility-types:build_types",
"//packages/kbn-utils:build_types",
],
diff --git a/packages/elastic-safer-lodash-set/BUILD.bazel b/packages/elastic-safer-lodash-set/BUILD.bazel
index cba719ee4f0eff..4a1c8b4290f334 100644
--- a/packages/elastic-safer-lodash-set/BUILD.bazel
+++ b/packages/elastic-safer-lodash-set/BUILD.bazel
@@ -63,3 +63,15 @@ filegroup(
],
visibility = ["//visibility:public"],
)
+
+alias(
+ name = "npm_module_types",
+ actual = PKG_BASE_NAME,
+ visibility = ["//visibility:public"],
+)
+
+alias(
+ name = "build_types",
+ actual = "build",
+ visibility = ["//visibility:public"],
+)
diff --git a/packages/kbn-apm-config-loader/BUILD.bazel b/packages/kbn-apm-config-loader/BUILD.bazel
index 0e3bc444acd241..a18a5e973d3a0f 100644
--- a/packages/kbn-apm-config-loader/BUILD.bazel
+++ b/packages/kbn-apm-config-loader/BUILD.bazel
@@ -35,7 +35,7 @@ RUNTIME_DEPS = [
]
TYPES_DEPS = [
- "//packages/elastic-safer-lodash-set",
+ "//packages/elastic-safer-lodash-set:npm_module_types",
"//packages/kbn-utils:npm_module_types",
"@npm//@elastic/apm-rum",
"@npm//@types/jest",
diff --git a/packages/kbn-config/BUILD.bazel b/packages/kbn-config/BUILD.bazel
index da4532f7d61c41..d7046a26ff92f2 100644
--- a/packages/kbn-config/BUILD.bazel
+++ b/packages/kbn-config/BUILD.bazel
@@ -45,7 +45,7 @@ RUNTIME_DEPS = [
]
TYPES_DEPS = [
- "//packages/elastic-safer-lodash-set",
+ "//packages/elastic-safer-lodash-set:npm_module_types",
"//packages/kbn-config-schema:npm_module_types",
"//packages/kbn-logging",
"//packages/kbn-std:npm_module_types",
diff --git a/packages/kbn-optimizer/BUILD.bazel b/packages/kbn-optimizer/BUILD.bazel
index 548d410d2f3162..0e5e99c5d1096d 100644
--- a/packages/kbn-optimizer/BUILD.bazel
+++ b/packages/kbn-optimizer/BUILD.bazel
@@ -69,7 +69,7 @@ TYPES_DEPS = [
"//packages/kbn-config-schema:npm_module_types",
"//packages/kbn-dev-utils:npm_module_types",
"//packages/kbn-std:npm_module_types",
- "//packages/kbn-ui-shared-deps-npm",
+ "//packages/kbn-ui-shared-deps-npm:npm_module_types",
"//packages/kbn-ui-shared-deps-src",
"//packages/kbn-utils:npm_module_types",
"@npm//chalk",
diff --git a/packages/kbn-storybook/BUILD.bazel b/packages/kbn-storybook/BUILD.bazel
index 92650e4bbca1f5..729a60f8f7dc8b 100644
--- a/packages/kbn-storybook/BUILD.bazel
+++ b/packages/kbn-storybook/BUILD.bazel
@@ -49,7 +49,7 @@ RUNTIME_DEPS = [
TYPES_DEPS = [
"//packages/kbn-dev-utils:npm_module_types",
- "//packages/kbn-ui-shared-deps-npm",
+ "//packages/kbn-ui-shared-deps-npm:npm_module_types",
"//packages/kbn-ui-shared-deps-src",
"//packages/kbn-utils:npm_module_types",
"@npm//@storybook/addons",
diff --git a/packages/kbn-ui-shared-deps-npm/BUILD.bazel b/packages/kbn-ui-shared-deps-npm/BUILD.bazel
index 2beedafd699fdf..b2295ecfc8f282 100644
--- a/packages/kbn-ui-shared-deps-npm/BUILD.bazel
+++ b/packages/kbn-ui-shared-deps-npm/BUILD.bazel
@@ -1,10 +1,11 @@
-load("@npm//@bazel/typescript:index.bzl", "ts_config", "ts_project")
-load("@build_bazel_rules_nodejs//:index.bzl", "js_library", "pkg_npm")
+load("@npm//@bazel/typescript:index.bzl", "ts_config")
+load("@build_bazel_rules_nodejs//:index.bzl", "js_library")
load("@npm//webpack-cli:index.bzl", webpack = "webpack_cli")
-load("//src/dev/bazel:index.bzl", "jsts_transpiler")
+load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", "ts_project")
PKG_BASE_NAME = "kbn-ui-shared-deps-npm"
PKG_REQUIRE_NAME = "@kbn/ui-shared-deps-npm"
+TYPES_PKG_REQUIRE_NAME = "@types/kbn__ui-shared-deps-npm"
SOURCE_FILES = glob(
[
@@ -150,7 +151,7 @@ webpack(
js_library(
name = PKG_BASE_NAME,
srcs = NPM_MODULE_EXTRA_FILES,
- deps = RUNTIME_DEPS + [":target_node", ":tsc_types", ":shared_built_assets"],
+ deps = RUNTIME_DEPS + [":target_node", ":shared_built_assets"],
package_name = PKG_REQUIRE_NAME,
visibility = ["//visibility:public"],
)
@@ -169,3 +170,20 @@ filegroup(
],
visibility = ["//visibility:public"],
)
+
+pkg_npm_types(
+ name = "npm_module_types",
+ srcs = SRCS,
+ deps = [":tsc_types"],
+ package_name = TYPES_PKG_REQUIRE_NAME,
+ tsconfig = ":tsconfig",
+ visibility = ["//visibility:public"],
+)
+
+filegroup(
+ name = "build_types",
+ srcs = [
+ ":npm_module_types",
+ ],
+ visibility = ["//visibility:public"],
+)
diff --git a/packages/kbn-ui-shared-deps-npm/package.json b/packages/kbn-ui-shared-deps-npm/package.json
index 0ed7ea661c818e..353918eca145f5 100644
--- a/packages/kbn-ui-shared-deps-npm/package.json
+++ b/packages/kbn-ui-shared-deps-npm/package.json
@@ -4,6 +4,5 @@
"private": true,
"license": "SSPL-1.0 OR Elastic License 2.0",
"main": "target_node/index.js",
- "browser": "target_node/entry.js",
- "types": "target_types/index.d.ts"
+ "browser": "target_node/entry.js"
}
\ No newline at end of file
diff --git a/packages/kbn-ui-shared-deps-npm/src/index.js b/packages/kbn-ui-shared-deps-npm/src/index.js
index 678e7c845e6a5e..768c6131e0c368 100644
--- a/packages/kbn-ui-shared-deps-npm/src/index.js
+++ b/packages/kbn-ui-shared-deps-npm/src/index.js
@@ -12,20 +12,26 @@
const Path = require('path');
+// extracted const vars
+const distDir = Path.resolve(__dirname, '../shared_built_assets');
+const dllManifestPath = Path.resolve(distDir, 'kbn-ui-shared-deps-npm-manifest.json');
+const dllFilename = 'kbn-ui-shared-deps-npm.dll.js';
+const publicPathLoader = require.resolve('./public_path_loader');
+
/**
* Absolute path to the distributable directory
*/
-exports.distDir = Path.resolve(__dirname, '../shared_built_assets');
+exports.distDir = distDir;
/**
* Path to dll manifest of modules included in this bundle
*/
-exports.dllManifestPath = Path.resolve(exports.distDir, 'kbn-ui-shared-deps-npm-manifest.json');
+exports.dllManifestPath = dllManifestPath;
/**
* Filename of the main bundle file in the distributable directory
*/
-exports.dllFilename = 'kbn-ui-shared-deps-npm.dll.js';
+exports.dllFilename = dllFilename;
/**
* Filename of the light-theme css file in the distributable directory
@@ -54,4 +60,4 @@ exports.darkCssDistFilename = (themeVersion) => {
/**
* Webpack loader for configuring the public path lookup from `window.__kbnPublicPath__`.
*/
-exports.publicPathLoader = require.resolve('./public_path_loader');
+exports.publicPathLoader = publicPathLoader;
diff --git a/packages/kbn-ui-shared-deps-src/BUILD.bazel b/packages/kbn-ui-shared-deps-src/BUILD.bazel
index e32834b3f2e8f9..ce2cbe714a16c8 100644
--- a/packages/kbn-ui-shared-deps-src/BUILD.bazel
+++ b/packages/kbn-ui-shared-deps-src/BUILD.bazel
@@ -43,13 +43,13 @@ RUNTIME_DEPS = [
TYPES_DEPS = [
"//packages/elastic-datemath:npm_module_types",
- "//packages/elastic-safer-lodash-set",
+ "//packages/elastic-safer-lodash-set:npm_module_types",
"//packages/kbn-analytics:npm_module_types",
"//packages/kbn-i18n:npm_module_types",
"//packages/kbn-i18n-react:npm_module_types",
"//packages/kbn-monaco:npm_module_types",
"//packages/kbn-std:npm_module_types",
- "//packages/kbn-ui-shared-deps-npm",
+ "//packages/kbn-ui-shared-deps-npm:npm_module_types",
"@npm//@elastic/eui",
"@npm//webpack",
]
diff --git a/src/core/server/saved_objects/import/import_saved_objects.test.mock.ts b/src/core/server/saved_objects/import/import_saved_objects.test.mock.ts
new file mode 100644
index 00000000000000..82e5aa4a5d77f1
--- /dev/null
+++ b/src/core/server/saved_objects/import/import_saved_objects.test.mock.ts
@@ -0,0 +1,60 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import type { collectSavedObjects } from './lib/collect_saved_objects';
+import type { checkReferenceOrigins } from './lib/check_reference_origins';
+import type { regenerateIds } from './lib/regenerate_ids';
+import type { validateReferences } from './lib/validate_references';
+import type { checkConflicts } from './lib/check_conflicts';
+import type { checkOriginConflicts } from './lib/check_origin_conflicts';
+import type { createSavedObjects } from './lib/create_saved_objects';
+import type { executeImportHooks } from './lib/execute_import_hooks';
+
+export const mockCollectSavedObjects = jest.fn() as jest.MockedFunction;
+jest.mock('./lib/collect_saved_objects', () => ({
+ collectSavedObjects: mockCollectSavedObjects,
+}));
+
+export const mockCheckReferenceOrigins = jest.fn() as jest.MockedFunction<
+ typeof checkReferenceOrigins
+>;
+jest.mock('./lib/check_reference_origins', () => ({
+ checkReferenceOrigins: mockCheckReferenceOrigins,
+}));
+
+export const mockRegenerateIds = jest.fn() as jest.MockedFunction;
+jest.mock('./lib/regenerate_ids', () => ({
+ regenerateIds: mockRegenerateIds,
+}));
+
+export const mockValidateReferences = jest.fn() as jest.MockedFunction;
+jest.mock('./lib/validate_references', () => ({
+ validateReferences: mockValidateReferences,
+}));
+
+export const mockCheckConflicts = jest.fn() as jest.MockedFunction;
+jest.mock('./lib/check_conflicts', () => ({
+ checkConflicts: mockCheckConflicts,
+}));
+
+export const mockCheckOriginConflicts = jest.fn() as jest.MockedFunction<
+ typeof checkOriginConflicts
+>;
+jest.mock('./lib/check_origin_conflicts', () => ({
+ checkOriginConflicts: mockCheckOriginConflicts,
+}));
+
+export const mockCreateSavedObjects = jest.fn() as jest.MockedFunction;
+jest.mock('./lib/create_saved_objects', () => ({
+ createSavedObjects: mockCreateSavedObjects,
+}));
+
+export const mockExecuteImportHooks = jest.fn() as jest.MockedFunction;
+jest.mock('./lib/execute_import_hooks', () => ({
+ executeImportHooks: mockExecuteImportHooks,
+}));
diff --git a/src/core/server/saved_objects/import/import_saved_objects.test.ts b/src/core/server/saved_objects/import/import_saved_objects.test.ts
index cf30d6c803933c..2f31b4cf3ead3a 100644
--- a/src/core/server/saved_objects/import/import_saved_objects.test.ts
+++ b/src/core/server/saved_objects/import/import_saved_objects.test.ts
@@ -6,6 +6,17 @@
* Side Public License, v 1.
*/
+import {
+ mockCollectSavedObjects,
+ mockCheckReferenceOrigins,
+ mockRegenerateIds,
+ mockValidateReferences,
+ mockCheckConflicts,
+ mockCheckOriginConflicts,
+ mockCreateSavedObjects,
+ mockExecuteImportHooks,
+} from './import_saved_objects.test.mock';
+
import { Readable } from 'stream';
import { v4 as uuidv4 } from 'uuid';
import {
@@ -19,52 +30,33 @@ import { ISavedObjectTypeRegistry } from '..';
import { typeRegistryMock } from '../saved_objects_type_registry.mock';
import { importSavedObjectsFromStream, ImportSavedObjectsOptions } from './import_saved_objects';
import { SavedObjectsImportHook, SavedObjectsImportWarning } from './types';
-
-import {
- collectSavedObjects,
- regenerateIds,
- validateReferences,
- checkConflicts,
- checkOriginConflicts,
- createSavedObjects,
- executeImportHooks,
-} from './lib';
-
-jest.mock('./lib/collect_saved_objects');
-jest.mock('./lib/regenerate_ids');
-jest.mock('./lib/validate_references');
-jest.mock('./lib/check_conflicts');
-jest.mock('./lib/check_origin_conflicts');
-jest.mock('./lib/create_saved_objects');
-jest.mock('./lib/execute_import_hooks');
-
-const getMockFn = any, U>(fn: (...args: Parameters) => U) =>
- fn as jest.MockedFunction<(...args: Parameters) => U>;
+import type { ImportStateMap } from './lib';
describe('#importSavedObjectsFromStream', () => {
beforeEach(() => {
jest.clearAllMocks();
// mock empty output of each of these mocked modules so the import doesn't throw an error
- getMockFn(collectSavedObjects).mockResolvedValue({
+ mockCollectSavedObjects.mockResolvedValue({
errors: [],
collectedObjects: [],
- importIdMap: new Map(),
+ importStateMap: new Map(),
});
- getMockFn(regenerateIds).mockReturnValue(new Map());
- getMockFn(validateReferences).mockResolvedValue([]);
- getMockFn(checkConflicts).mockResolvedValue({
+ mockCheckReferenceOrigins.mockResolvedValue({ importStateMap: new Map() });
+ mockRegenerateIds.mockReturnValue(new Map());
+ mockValidateReferences.mockResolvedValue([]);
+ mockCheckConflicts.mockResolvedValue({
errors: [],
filteredObjects: [],
- importIdMap: new Map(),
+ importStateMap: new Map(),
pendingOverwrites: new Set(),
});
- getMockFn(checkOriginConflicts).mockResolvedValue({
+ mockCheckOriginConflicts.mockResolvedValue({
errors: [],
- importIdMap: new Map(),
+ importStateMap: new Map(),
pendingOverwrites: new Set(),
});
- getMockFn(createSavedObjects).mockResolvedValue({ errors: [], createdObjects: [] });
- getMockFn(executeImportHooks).mockResolvedValue([]);
+ mockCreateSavedObjects.mockResolvedValue({ errors: [], createdObjects: [] });
+ mockExecuteImportHooks.mockResolvedValue([]);
});
let readStream: Readable;
@@ -143,24 +135,57 @@ describe('#importSavedObjectsFromStream', () => {
await importSavedObjectsFromStream(options);
expect(typeRegistry.getImportableAndExportableTypes).toHaveBeenCalled();
const collectSavedObjectsOptions = { readStream, objectLimit, supportedTypes };
- expect(collectSavedObjects).toHaveBeenCalledWith(collectSavedObjectsOptions);
+ expect(mockCollectSavedObjects).toHaveBeenCalledWith(collectSavedObjectsOptions);
});
- test('validates references', async () => {
+ test('checks reference origins', async () => {
const options = setupOptions();
const collectedObjects = [createObject()];
- getMockFn(collectSavedObjects).mockResolvedValue({
+ const importStateMap = new Map([
+ [`${collectedObjects[0].type}:${collectedObjects[0].id}`, {}],
+ [`foo:bar`, { isOnlyReference: true }],
+ ]);
+ mockCollectSavedObjects.mockResolvedValue({
errors: [],
collectedObjects,
- importIdMap: new Map(),
+ importStateMap,
});
await importSavedObjectsFromStream(options);
- expect(validateReferences).toHaveBeenCalledWith(
+ expect(mockCheckReferenceOrigins).toHaveBeenCalledWith({
+ savedObjectsClient,
+ typeRegistry,
+ namespace,
+ importStateMap,
+ });
+ });
+
+ test('validates references', async () => {
+ const options = setupOptions();
+ const collectedObjects = [createObject()];
+ mockCollectSavedObjects.mockResolvedValue({
+ errors: [],
collectedObjects,
+ importStateMap: new Map([
+ [`${collectedObjects[0].type}:${collectedObjects[0].id}`, {}],
+ [`foo:bar`, { isOnlyReference: true }],
+ ]),
+ });
+ mockCheckReferenceOrigins.mockResolvedValue({
+ importStateMap: new Map([[`foo:bar`, { isOnlyReference: true, id: 'baz' }]]),
+ });
+
+ await importSavedObjectsFromStream(options);
+ expect(mockValidateReferences).toHaveBeenCalledWith({
+ objects: collectedObjects,
savedObjectsClient,
- namespace
- );
+ namespace,
+ importStateMap: new Map([
+ // This importStateMap is a combination of the other two
+ [`${collectedObjects[0].type}:${collectedObjects[0].id}`, {}],
+ [`foo:bar`, { isOnlyReference: true, id: 'baz' }],
+ ]),
+ });
});
test('executes import hooks', async () => {
@@ -170,19 +195,19 @@ describe('#importSavedObjectsFromStream', () => {
const options = setupOptions({ importHooks });
const collectedObjects = [createObject()];
- getMockFn(collectSavedObjects).mockResolvedValue({
+ mockCollectSavedObjects.mockResolvedValue({
errors: [],
collectedObjects,
- importIdMap: new Map(),
+ importStateMap: new Map(),
});
- getMockFn(createSavedObjects).mockResolvedValue({
+ mockCreateSavedObjects.mockResolvedValue({
errors: [],
createdObjects: collectedObjects,
});
await importSavedObjectsFromStream(options);
- expect(executeImportHooks).toHaveBeenCalledWith({
+ expect(mockExecuteImportHooks).toHaveBeenCalledWith({
objects: collectedObjects,
importHooks,
});
@@ -192,23 +217,23 @@ describe('#importSavedObjectsFromStream', () => {
test('does not regenerate object IDs', async () => {
const options = setupOptions();
const collectedObjects = [createObject()];
- getMockFn(collectSavedObjects).mockResolvedValue({
+ mockCollectSavedObjects.mockResolvedValue({
errors: [],
collectedObjects,
- importIdMap: new Map(),
+ importStateMap: new Map(),
});
await importSavedObjectsFromStream(options);
- expect(regenerateIds).not.toHaveBeenCalled();
+ expect(mockRegenerateIds).not.toHaveBeenCalled();
});
test('checks conflicts', async () => {
const options = setupOptions();
const collectedObjects = [createObject()];
- getMockFn(collectSavedObjects).mockResolvedValue({
+ mockCollectSavedObjects.mockResolvedValue({
errors: [],
collectedObjects,
- importIdMap: new Map(),
+ importStateMap: new Map(),
});
await importSavedObjectsFromStream(options);
@@ -218,18 +243,19 @@ describe('#importSavedObjectsFromStream', () => {
namespace,
ignoreRegularConflicts: overwrite,
};
- expect(checkConflicts).toHaveBeenCalledWith(checkConflictsParams);
+ expect(mockCheckConflicts).toHaveBeenCalledWith(checkConflictsParams);
});
test('checks origin conflicts', async () => {
const options = setupOptions();
const filteredObjects = [createObject()];
- const importIdMap = new Map();
- getMockFn(checkConflicts).mockResolvedValue({
+ const importStateMap = new Map();
+ const pendingOverwrites = new Set();
+ mockCheckConflicts.mockResolvedValue({
errors: [],
filteredObjects,
- importIdMap,
- pendingOverwrites: new Set(),
+ importStateMap,
+ pendingOverwrites,
});
await importSavedObjectsFromStream(options);
@@ -239,9 +265,10 @@ describe('#importSavedObjectsFromStream', () => {
typeRegistry,
namespace,
ignoreRegularConflicts: overwrite,
- importIdMap,
+ importStateMap,
+ pendingOverwrites,
};
- expect(checkOriginConflicts).toHaveBeenCalledWith(checkOriginConflictsParams);
+ expect(mockCheckOriginConflicts).toHaveBeenCalledWith(checkOriginConflictsParams);
});
test('creates saved objects', async () => {
@@ -249,43 +276,47 @@ describe('#importSavedObjectsFromStream', () => {
const collectedObjects = [createObject()];
const filteredObjects = [createObject()];
const errors = [createError(), createError(), createError(), createError()];
- getMockFn(collectSavedObjects).mockResolvedValue({
+ mockCollectSavedObjects.mockResolvedValue({
errors: [errors[0]],
collectedObjects,
- importIdMap: new Map([
+ importStateMap: new Map([
['foo', {}],
['bar', {}],
- ['baz', {}],
+ ['baz', { isOnlyReference: true }],
]),
});
- getMockFn(validateReferences).mockResolvedValue([errors[1]]);
- getMockFn(checkConflicts).mockResolvedValue({
+ mockCheckReferenceOrigins.mockResolvedValue({
+ importStateMap: new Map([['baz', { isOnlyReference: true, destinationId: 'newId1' }]]),
+ });
+ mockValidateReferences.mockResolvedValue([errors[1]]);
+ mockCheckConflicts.mockResolvedValue({
errors: [errors[2]],
filteredObjects,
- importIdMap: new Map([['bar', { id: 'newId1' }]]),
+ importStateMap: new Map([['foo', { destinationId: 'newId2' }]]),
pendingOverwrites: new Set(),
});
- getMockFn(checkOriginConflicts).mockResolvedValue({
+ mockCheckOriginConflicts.mockResolvedValue({
errors: [errors[3]],
- importIdMap: new Map([['baz', { id: 'newId2' }]]),
+ importStateMap: new Map([['bar', { destinationId: 'newId3' }]]),
pendingOverwrites: new Set(),
});
await importSavedObjectsFromStream(options);
- const importIdMap = new Map([
- ['foo', {}],
- ['bar', { id: 'newId1' }],
- ['baz', { id: 'newId2' }],
+ // assert that the importStateMap is correctly composed of the results from the four modules
+ const importStateMap = new Map([
+ ['foo', { destinationId: 'newId2' }],
+ ['bar', { destinationId: 'newId3' }],
+ ['baz', { isOnlyReference: true, destinationId: 'newId1' }],
]);
const createSavedObjectsParams = {
objects: collectedObjects,
accumulatedErrors: errors,
savedObjectsClient,
- importIdMap,
+ importStateMap,
overwrite,
namespace,
};
- expect(createSavedObjects).toHaveBeenCalledWith(createSavedObjectsParams);
+ expect(mockCreateSavedObjects).toHaveBeenCalledWith(createSavedObjectsParams);
});
});
@@ -293,52 +324,58 @@ describe('#importSavedObjectsFromStream', () => {
test('regenerates object IDs', async () => {
const options = setupOptions({ createNewCopies: true });
const collectedObjects = [createObject()];
- getMockFn(collectSavedObjects).mockResolvedValue({
+ mockCollectSavedObjects.mockResolvedValue({
errors: [],
collectedObjects,
- importIdMap: new Map(), // doesn't matter
+ importStateMap: new Map(), // doesn't matter
});
await importSavedObjectsFromStream(options);
- expect(regenerateIds).toHaveBeenCalledWith(collectedObjects);
+ expect(mockRegenerateIds).toHaveBeenCalledWith(collectedObjects);
});
test('does not check conflicts or check origin conflicts', async () => {
const options = setupOptions({ createNewCopies: true });
- getMockFn(validateReferences).mockResolvedValue([]);
+ mockValidateReferences.mockResolvedValue([]);
await importSavedObjectsFromStream(options);
- expect(checkConflicts).not.toHaveBeenCalled();
- expect(checkOriginConflicts).not.toHaveBeenCalled();
+ expect(mockCheckConflicts).not.toHaveBeenCalled();
+ expect(mockCheckOriginConflicts).not.toHaveBeenCalled();
});
test('creates saved objects', async () => {
const options = setupOptions({ createNewCopies: true });
const collectedObjects = [createObject()];
const errors = [createError(), createError()];
- getMockFn(collectSavedObjects).mockResolvedValue({
+ mockCollectSavedObjects.mockResolvedValue({
errors: [errors[0]],
collectedObjects,
- importIdMap: new Map([
+ importStateMap: new Map([
['foo', {}],
- ['bar', {}],
+ ['bar', { isOnlyReference: true }],
]),
});
- getMockFn(validateReferences).mockResolvedValue([errors[1]]);
- // this importIdMap is not composed with the one obtained from `collectSavedObjects`
- const importIdMap = new Map().set(`id1`, { id: `newId1` });
- getMockFn(regenerateIds).mockReturnValue(importIdMap);
+ mockCheckReferenceOrigins.mockResolvedValue({
+ importStateMap: new Map([['bar', { isOnlyReference: true, destinationId: 'newId' }]]),
+ });
+ mockValidateReferences.mockResolvedValue([errors[1]]);
+ mockRegenerateIds.mockReturnValue(new Map([['foo', { destinationId: `randomId1` }]]));
await importSavedObjectsFromStream(options);
+ // assert that the importStateMap is correctly composed of the results from the three modules
+ const importStateMap: ImportStateMap = new Map([
+ ['foo', { destinationId: `randomId1` }],
+ ['bar', { isOnlyReference: true, destinationId: 'newId' }],
+ ]);
const createSavedObjectsParams = {
objects: collectedObjects,
accumulatedErrors: errors,
savedObjectsClient,
- importIdMap,
+ importStateMap,
overwrite,
namespace,
};
- expect(createSavedObjects).toHaveBeenCalledWith(createSavedObjectsParams);
+ expect(mockCreateSavedObjects).toHaveBeenCalledWith(createSavedObjectsParams);
});
});
});
@@ -353,10 +390,10 @@ describe('#importSavedObjectsFromStream', () => {
test('returns success=false if an error occurred', async () => {
const options = setupOptions();
- getMockFn(collectSavedObjects).mockResolvedValue({
+ mockCollectSavedObjects.mockResolvedValue({
errors: [createError()],
collectedObjects: [],
- importIdMap: new Map(), // doesn't matter
+ importStateMap: new Map(), // doesn't matter
});
const result = await importSavedObjectsFromStream(options);
@@ -371,18 +408,18 @@ describe('#importSavedObjectsFromStream', () => {
test('returns warnings from the import hooks', async () => {
const options = setupOptions();
const collectedObjects = [createObject()];
- getMockFn(collectSavedObjects).mockResolvedValue({
+ mockCollectSavedObjects.mockResolvedValue({
errors: [],
collectedObjects,
- importIdMap: new Map(),
+ importStateMap: new Map(),
});
- getMockFn(createSavedObjects).mockResolvedValue({
+ mockCreateSavedObjects.mockResolvedValue({
errors: [],
createdObjects: collectedObjects,
});
const warnings: SavedObjectsImportWarning[] = [{ type: 'simple', message: 'foo' }];
- getMockFn(executeImportHooks).mockResolvedValue(warnings);
+ mockExecuteImportHooks.mockResolvedValue(warnings);
const result = await importSavedObjectsFromStream(options);
@@ -419,16 +456,16 @@ describe('#importSavedObjectsFromStream', () => {
test('with createNewCopies disabled', async () => {
const options = setupOptions();
- getMockFn(checkConflicts).mockResolvedValue({
+ mockCheckConflicts.mockResolvedValue({
errors: [],
filteredObjects: [],
- importIdMap: new Map(),
+ importStateMap: new Map(),
pendingOverwrites: new Set([
`${success2.type}:${success2.id}`, // the success2 object was overwritten
`${error2.type}:${error2.id}`, // an attempt was made to overwrite the error2 object
]),
});
- getMockFn(createSavedObjects).mockResolvedValue({ errors, createdObjects });
+ mockCreateSavedObjects.mockResolvedValue({ errors, createdObjects });
const result = await importSavedObjectsFromStream(options);
// successResults only includes the imported object's type, id, and destinationId (if a new one was generated)
@@ -457,7 +494,7 @@ describe('#importSavedObjectsFromStream', () => {
test('with createNewCopies enabled', async () => {
// however, we include it here for posterity
const options = setupOptions({ createNewCopies: true });
- getMockFn(createSavedObjects).mockResolvedValue({ errors, createdObjects });
+ mockCreateSavedObjects.mockResolvedValue({ errors, createdObjects });
const result = await importSavedObjectsFromStream(options);
// successResults only includes the imported object's type, id, and destinationId (if a new one was generated)
@@ -495,13 +532,13 @@ describe('#importSavedObjectsFromStream', () => {
},
});
- getMockFn(checkConflicts).mockResolvedValue({
+ mockCheckConflicts.mockResolvedValue({
errors: [],
filteredObjects: [],
- importIdMap: new Map(),
+ importStateMap: new Map(),
pendingOverwrites: new Set(),
});
- getMockFn(createSavedObjects).mockResolvedValue({ errors: [], createdObjects: [obj1, obj2] });
+ mockCreateSavedObjects.mockResolvedValue({ errors: [], createdObjects: [obj1, obj2] });
const result = await importSavedObjectsFromStream(options);
// successResults only includes the imported object's type, id, and destinationId (if a new one was generated)
@@ -529,24 +566,24 @@ describe('#importSavedObjectsFromStream', () => {
test('accumulates multiple errors', async () => {
const options = setupOptions();
const errors = [createError(), createError(), createError(), createError(), createError()];
- getMockFn(collectSavedObjects).mockResolvedValue({
+ mockCollectSavedObjects.mockResolvedValue({
errors: [errors[0]],
collectedObjects: [],
- importIdMap: new Map(), // doesn't matter
+ importStateMap: new Map(), // doesn't matter
});
- getMockFn(validateReferences).mockResolvedValue([errors[1]]);
- getMockFn(checkConflicts).mockResolvedValue({
+ mockValidateReferences.mockResolvedValue([errors[1]]);
+ mockCheckConflicts.mockResolvedValue({
errors: [errors[2]],
filteredObjects: [],
- importIdMap: new Map(), // doesn't matter
+ importStateMap: new Map(), // doesn't matter
pendingOverwrites: new Set(),
});
- getMockFn(checkOriginConflicts).mockResolvedValue({
+ mockCheckOriginConflicts.mockResolvedValue({
errors: [errors[3]],
- importIdMap: new Map(), // doesn't matter
+ importStateMap: new Map(), // doesn't matter
pendingOverwrites: new Set(),
});
- getMockFn(createSavedObjects).mockResolvedValue({ errors: [errors[4]], createdObjects: [] });
+ mockCreateSavedObjects.mockResolvedValue({ errors: [errors[4]], createdObjects: [] });
const result = await importSavedObjectsFromStream(options);
const expectedErrors = errors.map(({ type, id }) => expect.objectContaining({ type, id }));
diff --git a/src/core/server/saved_objects/import/import_saved_objects.ts b/src/core/server/saved_objects/import/import_saved_objects.ts
index 4fc8f04a40270e..0631d97b58a72b 100644
--- a/src/core/server/saved_objects/import/import_saved_objects.ts
+++ b/src/core/server/saved_objects/import/import_saved_objects.ts
@@ -15,6 +15,7 @@ import {
SavedObjectsImportHook,
} from './types';
import {
+ checkReferenceOrigins,
validateReferences,
checkOriginConflicts,
createSavedObjects,
@@ -72,20 +73,34 @@ export async function importSavedObjectsFromStream({
supportedTypes,
});
errorAccumulator = [...errorAccumulator, ...collectSavedObjectsResult.errors];
- /** Map of all IDs for objects that we are attempting to import; each value is empty by default */
- let importIdMap = collectSavedObjectsResult.importIdMap;
+ // Map of all IDs for objects that we are attempting to import, and any references that are not included in the read stream;
+ // each value is empty by default
+ let importStateMap = collectSavedObjectsResult.importStateMap;
let pendingOverwrites = new Set();
+ // Check any references that aren't included in the import file and retries, to see if they have a match with a different origin
+ const checkReferenceOriginsResult = await checkReferenceOrigins({
+ savedObjectsClient,
+ typeRegistry,
+ namespace,
+ importStateMap,
+ });
+ importStateMap = new Map([...importStateMap, ...checkReferenceOriginsResult.importStateMap]);
+
// Validate references
- const validateReferencesResult = await validateReferences(
- collectSavedObjectsResult.collectedObjects,
+ const validateReferencesResult = await validateReferences({
+ objects: collectSavedObjectsResult.collectedObjects,
savedObjectsClient,
- namespace
- );
+ namespace,
+ importStateMap,
+ });
errorAccumulator = [...errorAccumulator, ...validateReferencesResult];
if (createNewCopies) {
- importIdMap = regenerateIds(collectSavedObjectsResult.collectedObjects);
+ importStateMap = new Map([
+ ...importStateMap, // preserve any entries for references that aren't included in collectedObjects
+ ...regenerateIds(collectSavedObjectsResult.collectedObjects),
+ ]);
} else {
// Check single-namespace objects for conflicts in this namespace, and check multi-namespace objects for conflicts across all namespaces
const checkConflictsParams = {
@@ -96,7 +111,7 @@ export async function importSavedObjectsFromStream({
};
const checkConflictsResult = await checkConflicts(checkConflictsParams);
errorAccumulator = [...errorAccumulator, ...checkConflictsResult.errors];
- importIdMap = new Map([...importIdMap, ...checkConflictsResult.importIdMap]);
+ importStateMap = new Map([...importStateMap, ...checkConflictsResult.importStateMap]);
pendingOverwrites = checkConflictsResult.pendingOverwrites;
// Check multi-namespace object types for origin conflicts in this namespace
@@ -106,11 +121,12 @@ export async function importSavedObjectsFromStream({
typeRegistry,
namespace,
ignoreRegularConflicts: overwrite,
- importIdMap,
+ importStateMap,
+ pendingOverwrites,
};
const checkOriginConflictsResult = await checkOriginConflicts(checkOriginConflictsParams);
errorAccumulator = [...errorAccumulator, ...checkOriginConflictsResult.errors];
- importIdMap = new Map([...importIdMap, ...checkOriginConflictsResult.importIdMap]);
+ importStateMap = new Map([...importStateMap, ...checkOriginConflictsResult.importStateMap]);
pendingOverwrites = new Set([
...pendingOverwrites,
...checkOriginConflictsResult.pendingOverwrites,
@@ -122,7 +138,7 @@ export async function importSavedObjectsFromStream({
objects: collectSavedObjectsResult.collectedObjects,
accumulatedErrors: errorAccumulator,
savedObjectsClient,
- importIdMap,
+ importStateMap,
overwrite,
namespace,
};
diff --git a/src/core/server/saved_objects/import/lib/check_conflicts.test.ts b/src/core/server/saved_objects/import/lib/check_conflicts.test.ts
index 6ab37b0122e4bc..b2de6f11d5cb89 100644
--- a/src/core/server/saved_objects/import/lib/check_conflicts.test.ts
+++ b/src/core/server/saved_objects/import/lib/check_conflicts.test.ts
@@ -6,13 +6,16 @@
* Side Public License, v 1.
*/
-import { mockUuidv4 } from './__mocks__';
import { savedObjectsClientMock } from '../../../mocks';
import { SavedObjectReference, SavedObjectsImportRetry } from 'kibana/public';
import { SavedObjectsClientContract, SavedObject } from '../../types';
import { SavedObjectsErrorHelpers } from '../../service';
import { checkConflicts } from './check_conflicts';
+jest.mock('uuid', () => ({
+ v4: () => 'uuidv4',
+}));
+
type SavedObjectType = SavedObject<{ title?: string }>;
type CheckConflictsParams = Parameters[0];
@@ -71,11 +74,6 @@ describe('#checkConflicts', () => {
return { ...partial, savedObjectsClient };
};
- beforeEach(() => {
- mockUuidv4.mockReset();
- mockUuidv4.mockReturnValueOnce(`new-object-id`);
- });
-
it('exits early if there are no objects to check', async () => {
const namespace = 'foo-namespace';
const params = setupParams({ objects: [], namespace });
@@ -85,7 +83,7 @@ describe('#checkConflicts', () => {
expect(checkConflictsResult).toEqual({
filteredObjects: [],
errors: [],
- importIdMap: new Map(),
+ importStateMap: new Map(),
pendingOverwrites: new Set(),
});
});
@@ -121,7 +119,7 @@ describe('#checkConflicts', () => {
error: { ...obj4Error.error, type: 'unknown' },
},
],
- importIdMap: new Map([[`${obj3.type}:${obj3.id}`, { id: `new-object-id` }]]),
+ importStateMap: new Map([[`${obj3.type}:${obj3.id}`, { destinationId: 'uuidv4' }]]),
pendingOverwrites: new Set(),
});
});
@@ -187,14 +185,14 @@ describe('#checkConflicts', () => {
error: { ...obj4Error.error, type: 'unknown' },
},
],
- importIdMap: new Map([
- [`${obj3.type}:${obj3.id}`, { id: `new-object-id`, omitOriginId: true }],
+ importStateMap: new Map([
+ [`${obj3.type}:${obj3.id}`, { destinationId: 'uuidv4', omitOriginId: true }],
]),
pendingOverwrites: new Set([`${obj5.type}:${obj5.id}`]),
});
});
- it('adds `omitOriginId` field to `importIdMap` entries when createNewCopies=true', async () => {
+ it('adds `omitOriginId` field to `importStateMap` entries when createNewCopies=true', async () => {
const namespace = 'foo-namespace';
const params = setupParams({ objects, namespace, createNewCopies: true });
socCheckConflicts.mockResolvedValue({ errors: [obj2Error, obj3Error, obj4Error] });
@@ -202,8 +200,8 @@ describe('#checkConflicts', () => {
const checkConflictsResult = await checkConflicts(params);
expect(checkConflictsResult).toEqual(
expect.objectContaining({
- importIdMap: new Map([
- [`${obj3.type}:${obj3.id}`, { id: `new-object-id`, omitOriginId: true }],
+ importStateMap: new Map([
+ [`${obj3.type}:${obj3.id}`, { destinationId: 'uuidv4', omitOriginId: true }],
]),
})
);
diff --git a/src/core/server/saved_objects/import/lib/check_conflicts.ts b/src/core/server/saved_objects/import/lib/check_conflicts.ts
index d5e37f21fc84a4..c15c4302491b48 100644
--- a/src/core/server/saved_objects/import/lib/check_conflicts.ts
+++ b/src/core/server/saved_objects/import/lib/check_conflicts.ts
@@ -14,6 +14,7 @@ import {
SavedObjectError,
SavedObjectsImportRetry,
} from '../../types';
+import type { ImportStateMap } from './types';
interface CheckConflictsParams {
objects: Array>;
@@ -37,12 +38,12 @@ export async function checkConflicts({
}: CheckConflictsParams) {
const filteredObjects: Array> = [];
const errors: SavedObjectsImportFailure[] = [];
- const importIdMap = new Map();
+ const importStateMap: ImportStateMap = new Map();
const pendingOverwrites = new Set();
// exit early if there are no objects to check
if (objects.length === 0) {
- return { filteredObjects, errors, importIdMap, pendingOverwrites };
+ return { filteredObjects, errors, importStateMap, pendingOverwrites };
}
const retryMap = retries.reduce(
@@ -76,7 +77,7 @@ export async function checkConflicts({
// This code path should not be triggered for a retry, but in case the consumer is using the import APIs incorrectly and attempting to
// retry an object with a destinationId that would result in an unresolvable conflict, we regenerate the ID here as a fail-safe.
const omitOriginId = createNewCopies || createNewCopy;
- importIdMap.set(`${type}:${id}`, { id: uuidv4(), omitOriginId });
+ importStateMap.set(`${type}:${id}`, { destinationId: uuidv4(), omitOriginId });
filteredObjects.push(object);
} else if (errorObj && errorObj.statusCode !== 409) {
errors.push({ type, id, title, meta: { title }, error: { ...errorObj, type: 'unknown' } });
@@ -90,5 +91,5 @@ export async function checkConflicts({
}
}
});
- return { filteredObjects, errors, importIdMap, pendingOverwrites };
+ return { filteredObjects, errors, importStateMap, pendingOverwrites };
}
diff --git a/src/core/server/saved_objects/import/lib/check_origin_conflicts.test.mock.ts b/src/core/server/saved_objects/import/lib/check_origin_conflicts.test.mock.ts
new file mode 100644
index 00000000000000..8fb5704af9d821
--- /dev/null
+++ b/src/core/server/saved_objects/import/lib/check_origin_conflicts.test.mock.ts
@@ -0,0 +1,14 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import type { createOriginQuery } from './utils';
+
+export const mockCreateOriginQuery = jest.fn() as jest.MockedFunction;
+jest.mock('./utils', () => ({
+ createOriginQuery: mockCreateOriginQuery,
+}));
diff --git a/src/core/server/saved_objects/import/lib/check_origin_conflicts.test.ts b/src/core/server/saved_objects/import/lib/check_origin_conflicts.test.ts
index 03d94492e7ec89..6c633b1a119d1a 100644
--- a/src/core/server/saved_objects/import/lib/check_origin_conflicts.test.ts
+++ b/src/core/server/saved_objects/import/lib/check_origin_conflicts.test.ts
@@ -6,18 +6,23 @@
* Side Public License, v 1.
*/
-import { mockUuidv4 } from './__mocks__';
+import { mockCreateOriginQuery } from './check_reference_origins.test.mock';
+
import {
SavedObjectsClientContract,
SavedObjectReference,
SavedObject,
- SavedObjectsImportRetry,
SavedObjectsImportFailure,
} from '../../types';
-import { checkOriginConflicts, getImportIdMapForRetries } from './check_origin_conflicts';
+import { checkOriginConflicts } from './check_origin_conflicts';
import { savedObjectsClientMock } from '../../../mocks';
import { typeRegistryMock } from '../../saved_objects_type_registry.mock';
import { ISavedObjectTypeRegistry } from '../../saved_objects_type_registry';
+import type { ImportStateMap } from './types';
+
+jest.mock('uuid', () => ({
+ v4: () => 'uuidv4',
+}));
type SavedObjectType = SavedObject<{ title?: string }>;
type CheckOriginConflictsParams = Parameters[0];
@@ -42,10 +47,6 @@ const createObject = (
const MULTI_NS_TYPE = 'multi';
const OTHER_TYPE = 'other';
-beforeEach(() => {
- mockUuidv4.mockClear();
-});
-
describe('#checkOriginConflicts', () => {
let savedObjectsClient: jest.Mocked;
let typeRegistry: jest.Mocked;
@@ -61,8 +62,9 @@ describe('#checkOriginConflicts', () => {
const setupParams = (partial: {
objects: SavedObjectType[];
namespace?: string;
- importIdMap?: Map;
ignoreRegularConflicts?: boolean;
+ importStateMap?: ImportStateMap;
+ pendingOverwrites?: Set;
}): CheckOriginConflictsParams => {
savedObjectsClient = savedObjectsClientMock.create();
find = savedObjectsClient.find;
@@ -70,7 +72,8 @@ describe('#checkOriginConflicts', () => {
typeRegistry = typeRegistryMock.create();
typeRegistry.isMultiNamespace.mockImplementation((type) => type === MULTI_NS_TYPE);
return {
- importIdMap: new Map(), // empty by default
+ importStateMap: new Map(), // empty by default
+ pendingOverwrites: new Set(), // empty by default
...partial,
savedObjectsClient,
typeRegistry,
@@ -82,19 +85,21 @@ describe('#checkOriginConflicts', () => {
};
describe('cluster calls', () => {
+ beforeEach(() => {
+ mockCreateOriginQuery.mockClear();
+ });
+
const multiNsObj = createObject(MULTI_NS_TYPE, 'id-1');
const multiNsObjWithOriginId = createObject(MULTI_NS_TYPE, 'id-2', 'originId-foo');
const otherObj = createObject(OTHER_TYPE, 'id-3');
// non-multi-namespace types shouldn't have origin IDs, but we include a test case to ensure it's handled gracefully
const otherObjWithOriginId = createObject(OTHER_TYPE, 'id-4', 'originId-bar');
- const expectFindArgs = (n: number, object: SavedObject, rawIdPrefix: string) => {
- const { type, id, originId } = object;
- const search = `"${rawIdPrefix}${type}:${originId || id}" | "${originId || id}"`; // this template works for our basic test cases
- const expectedArgs = expect.objectContaining({ type, search });
- // exclude rootSearchFields, page, perPage, and fields attributes from assertion -- these are constant
+ const expectFindArgs = (n: number, object: SavedObject) => {
+ const idToCheck = object.originId || object.id;
+ expect(mockCreateOriginQuery).toHaveBeenNthCalledWith(n, object.type, idToCheck);
// exclude namespace from assertion -- a separate test covers that
- expect(find).toHaveBeenNthCalledWith(n, expectedArgs);
+ expect(find).toHaveBeenNthCalledWith(n, expect.objectContaining({ type: object.type }));
};
test('does not execute searches for non-multi-namespace objects', async () => {
@@ -105,21 +110,26 @@ describe('#checkOriginConflicts', () => {
expect(find).not.toHaveBeenCalled();
});
+ test('does not execute searches for multi-namespace objects that already have pending overwrites (exact match conflicts)', async () => {
+ const objects = [multiNsObj, multiNsObjWithOriginId];
+ const pendingOverwrites = new Set([
+ `${multiNsObj.type}:${multiNsObj.id}`,
+ `${multiNsObjWithOriginId.type}:${multiNsObjWithOriginId.id}`,
+ ]);
+ const params = setupParams({ objects, pendingOverwrites });
+
+ await checkOriginConflicts(params);
+ expect(find).not.toHaveBeenCalled();
+ });
+
test('executes searches for multi-namespace objects', async () => {
const objects = [multiNsObj, otherObj, multiNsObjWithOriginId, otherObjWithOriginId];
const params1 = setupParams({ objects });
await checkOriginConflicts(params1);
expect(find).toHaveBeenCalledTimes(2);
- expectFindArgs(1, multiNsObj, '');
- expectFindArgs(2, multiNsObjWithOriginId, '');
-
- find.mockClear();
- const params2 = setupParams({ objects, namespace: 'some-namespace' });
- await checkOriginConflicts(params2);
- expect(find).toHaveBeenCalledTimes(2);
- expectFindArgs(1, multiNsObj, 'some-namespace:');
- expectFindArgs(2, multiNsObjWithOriginId, 'some-namespace:');
+ expectFindArgs(1, multiNsObj);
+ expectFindArgs(2, multiNsObjWithOriginId);
});
test('searches within the current `namespace`', async () => {
@@ -131,22 +141,6 @@ describe('#checkOriginConflicts', () => {
expect(find).toHaveBeenCalledTimes(1);
expect(find).toHaveBeenCalledWith(expect.objectContaining({ namespaces: [namespace] }));
});
-
- test('search query escapes quote and backslash characters in `id` and/or `originId`', async () => {
- const weirdId = `some"weird\\id`;
- const objects = [
- createObject(MULTI_NS_TYPE, weirdId),
- createObject(MULTI_NS_TYPE, 'some-id', weirdId),
- ];
- const params = setupParams({ objects });
-
- await checkOriginConflicts(params);
- const escapedId = `some\\"weird\\\\id`;
- const expectedQuery = `"${MULTI_NS_TYPE}:${escapedId}" | "${escapedId}"`;
- expect(find).toHaveBeenCalledTimes(2);
- expect(find).toHaveBeenNthCalledWith(1, expect.objectContaining({ search: expectedQuery }));
- expect(find).toHaveBeenNthCalledWith(2, expect.objectContaining({ search: expectedQuery }));
- });
});
describe('results', () => {
@@ -183,7 +177,35 @@ describe('#checkOriginConflicts', () => {
},
});
- describe('object result without a `importIdMap` entry (no match or exact match)', () => {
+ test('filters inexact matches of other objects that are being imported, but does not filter inexact matches of references that are not being imported', async () => {
+ // obj1, obj2, and obj3 exist in this space, and obj1 has references to both obj2 and obj3
+ // try to import obj1, obj2, and obj4; simulating a scenario where obj1 and obj2 were filtered out during `checkConflicts`, so we only call `checkOriginConflicts` with the remainder
+ const obj1 = createObject(MULTI_NS_TYPE, 'id-1');
+ const obj2 = createObject(MULTI_NS_TYPE, 'id-2', 'some-originId');
+ const obj3 = createObject(MULTI_NS_TYPE, 'id-3', 'some-originId');
+ const obj4 = createObject(MULTI_NS_TYPE, 'id-4', 'some-originId');
+ const objects = [obj4];
+ const params = setupParams({
+ objects,
+ importStateMap: new Map([
+ [`${obj1.type}:${obj1.id}`, {}],
+ [`${obj2.type}:${obj2.id}`, {}],
+ [`${obj3.type}:${obj3.id}`, { isOnlyReference: true }], // this attribute signifies that there is a reference to this object, but it is not present in the collected objects from the import file
+ [`${obj4.type}:${obj4.id}`, {}],
+ ]),
+ });
+ mockFindResult(obj2, obj3); // find for obj4: the result is an inexact match with two destinations, one of which is exactly matched by obj2 -- accordingly, obj4 has an inexact match to obj3
+
+ const checkOriginConflictsResult = await checkOriginConflicts(params);
+ const expectedResult = {
+ importStateMap: new Map(),
+ errors: [createConflictError(obj4, obj3.id)],
+ pendingOverwrites: new Set(),
+ };
+ expect(checkOriginConflictsResult).toEqual(expectedResult);
+ });
+
+ describe('object result without a `importStateMap` entry (no match or exact match)', () => {
test('returns object when no match is detected (0 hits)', async () => {
// no objects exist in this space
// try to import obj1, obj2, obj3, and obj4
@@ -198,7 +220,7 @@ describe('#checkOriginConflicts', () => {
const checkOriginConflictsResult = await checkOriginConflicts(params);
const expectedResult = {
- importIdMap: new Map(),
+ importStateMap: new Map(),
errors: [],
pendingOverwrites: new Set(),
};
@@ -215,7 +237,7 @@ describe('#checkOriginConflicts', () => {
const objects = [obj2, obj4];
const params = setupParams({
objects,
- importIdMap: new Map([
+ importStateMap: new Map([
[`${obj1.type}:${obj1.id}`, {}],
[`${obj2.type}:${obj2.id}`, {}],
[`${obj3.type}:${obj3.id}`, {}],
@@ -227,7 +249,7 @@ describe('#checkOriginConflicts', () => {
const checkOriginConflictsResult = await checkOriginConflicts(params);
const expectedResult = {
- importIdMap: new Map(),
+ importStateMap: new Map(),
errors: [],
pendingOverwrites: new Set(),
};
@@ -243,7 +265,7 @@ describe('#checkOriginConflicts', () => {
const objects = [obj3];
const params = setupParams({
objects,
- importIdMap: new Map([
+ importStateMap: new Map([
[`${obj1.type}:${obj1.id}`, {}],
[`${obj2.type}:${obj2.id}`, {}],
[`${obj3.type}:${obj3.id}`, {}],
@@ -253,7 +275,7 @@ describe('#checkOriginConflicts', () => {
const checkOriginConflictsResult = await checkOriginConflicts(params);
const expectedResult = {
- importIdMap: new Map(),
+ importStateMap: new Map(),
errors: [],
pendingOverwrites: new Set(),
};
@@ -261,7 +283,7 @@ describe('#checkOriginConflicts', () => {
});
});
- describe('object result with a `importIdMap` entry (partial match with a single destination)', () => {
+ describe('object result with a `importStateMap` entry (partial match with a single destination)', () => {
describe('when an inexact match is detected (1 hit)', () => {
// objA and objB exist in this space
// try to import obj1 and obj2
@@ -282,20 +304,20 @@ describe('#checkOriginConflicts', () => {
const params = setup(false);
const checkOriginConflictsResult = await checkOriginConflicts(params);
const expectedResult = {
- importIdMap: new Map(),
+ importStateMap: new Map(),
errors: [createConflictError(obj1, objA.id), createConflictError(obj2, objB.id)],
pendingOverwrites: new Set(),
};
expect(checkOriginConflictsResult).toEqual(expectedResult);
});
- test('returns object with a `importIdMap` entry when ignoreRegularConflicts=true', async () => {
+ test('returns object with a `importStateMap` entry when ignoreRegularConflicts=true', async () => {
const params = setup(true);
const checkOriginConflictsResult = await checkOriginConflicts(params);
const expectedResult = {
- importIdMap: new Map([
- [`${obj1.type}:${obj1.id}`, { id: objA.id }],
- [`${obj2.type}:${obj2.id}`, { id: objB.id }],
+ importStateMap: new Map([
+ [`${obj1.type}:${obj1.id}`, { destinationId: objA.id }],
+ [`${obj2.type}:${obj2.id}`, { destinationId: objB.id }],
]),
errors: [],
pendingOverwrites: new Set([`${obj1.type}:${obj1.id}`, `${obj2.type}:${obj2.id}`]),
@@ -319,7 +341,7 @@ describe('#checkOriginConflicts', () => {
const params = setupParams({
objects,
ignoreRegularConflicts,
- importIdMap: new Map([
+ importStateMap: new Map([
[`${obj1.type}:${obj1.id}`, {}],
[`${obj2.type}:${obj2.id}`, {}],
[`${obj3.type}:${obj3.id}`, {}],
@@ -335,20 +357,20 @@ describe('#checkOriginConflicts', () => {
const params = setup(false);
const checkOriginConflictsResult = await checkOriginConflicts(params);
const expectedResult = {
- importIdMap: new Map(),
+ importStateMap: new Map(),
errors: [createConflictError(obj2, objA.id), createConflictError(obj4, objB.id)],
pendingOverwrites: new Set(),
};
expect(checkOriginConflictsResult).toEqual(expectedResult);
});
- test('returns object with a `importIdMap` entry when ignoreRegularConflicts=true', async () => {
+ test('returns object with a `importStateMap` entry when ignoreRegularConflicts=true', async () => {
const params = setup(true);
const checkOriginConflictsResult = await checkOriginConflicts(params);
const expectedResult = {
- importIdMap: new Map([
- [`${obj2.type}:${obj2.id}`, { id: objA.id }],
- [`${obj4.type}:${obj4.id}`, { id: objB.id }],
+ importStateMap: new Map([
+ [`${obj2.type}:${obj2.id}`, { destinationId: objA.id }],
+ [`${obj4.type}:${obj4.id}`, { destinationId: objB.id }],
]),
errors: [],
pendingOverwrites: new Set([`${obj2.type}:${obj2.id}`, `${obj4.type}:${obj4.id}`]),
@@ -359,7 +381,7 @@ describe('#checkOriginConflicts', () => {
});
describe('ambiguous conflicts', () => {
- test('returns object with a `importIdMap` entry when multiple inexact matches are detected that target the same single destination', async () => {
+ test('returns object with a `importStateMap` entry when multiple inexact matches are detected that target the same single destination', async () => {
// objA and objB exist in this space
// try to import obj1, obj2, obj3, and obj4
const obj1 = createObject(MULTI_NS_TYPE, 'id-1');
@@ -377,16 +399,15 @@ describe('#checkOriginConflicts', () => {
const checkOriginConflictsResult = await checkOriginConflicts(params);
const expectedResult = {
- importIdMap: new Map([
- [`${obj1.type}:${obj1.id}`, { id: 'uuidv4', omitOriginId: true }],
- [`${obj2.type}:${obj2.id}`, { id: 'uuidv4', omitOriginId: true }],
- [`${obj3.type}:${obj3.id}`, { id: 'uuidv4', omitOriginId: true }],
- [`${obj4.type}:${obj4.id}`, { id: 'uuidv4', omitOriginId: true }],
+ importStateMap: new Map([
+ [`${obj1.type}:${obj1.id}`, { destinationId: 'uuidv4', omitOriginId: true }],
+ [`${obj2.type}:${obj2.id}`, { destinationId: 'uuidv4', omitOriginId: true }],
+ [`${obj3.type}:${obj3.id}`, { destinationId: 'uuidv4', omitOriginId: true }],
+ [`${obj4.type}:${obj4.id}`, { destinationId: 'uuidv4', omitOriginId: true }],
]),
errors: [],
pendingOverwrites: new Set(),
};
- expect(mockUuidv4).toHaveBeenCalledTimes(4);
expect(checkOriginConflictsResult).toEqual(expectedResult);
});
@@ -406,18 +427,17 @@ describe('#checkOriginConflicts', () => {
const checkOriginConflictsResult = await checkOriginConflicts(params);
const expectedResult = {
- importIdMap: new Map(),
+ importStateMap: new Map(),
errors: [
createAmbiguousConflictError(obj1, [objB, objA]), // Assert that these have been sorted by updatedAt in descending order
createAmbiguousConflictError(obj2, [objC, objD]), // Assert that these have been sorted by ID in ascending order (since their updatedAt values are the same)
],
pendingOverwrites: new Set(),
};
- expect(mockUuidv4).not.toHaveBeenCalled();
expect(checkOriginConflictsResult).toEqual(expectedResult);
});
- test('returns object with a `importIdMap` entry when multiple inexact matches are detected that target the same multiple destinations', async () => {
+ test('returns object with a `importStateMap` entry when multiple inexact matches are detected that target the same multiple destinations', async () => {
// objA, objB, objC, and objD exist in this space
// try to import obj1, obj2, obj3, and obj4
const obj1 = createObject(MULTI_NS_TYPE, 'id-1');
@@ -437,16 +457,15 @@ describe('#checkOriginConflicts', () => {
const checkOriginConflictsResult = await checkOriginConflicts(params);
const expectedResult = {
- importIdMap: new Map([
- [`${obj1.type}:${obj1.id}`, { id: 'uuidv4', omitOriginId: true }],
- [`${obj2.type}:${obj2.id}`, { id: 'uuidv4', omitOriginId: true }],
- [`${obj3.type}:${obj3.id}`, { id: 'uuidv4', omitOriginId: true }],
- [`${obj4.type}:${obj4.id}`, { id: 'uuidv4', omitOriginId: true }],
+ importStateMap: new Map([
+ [`${obj1.type}:${obj1.id}`, { destinationId: 'uuidv4', omitOriginId: true }],
+ [`${obj2.type}:${obj2.id}`, { destinationId: 'uuidv4', omitOriginId: true }],
+ [`${obj3.type}:${obj3.id}`, { destinationId: 'uuidv4', omitOriginId: true }],
+ [`${obj4.type}:${obj4.id}`, { destinationId: 'uuidv4', omitOriginId: true }],
]),
errors: [],
pendingOverwrites: new Set(),
};
- expect(mockUuidv4).toHaveBeenCalledTimes(4);
expect(checkOriginConflictsResult).toEqual(expectedResult);
});
});
@@ -470,10 +489,12 @@ describe('#checkOriginConflicts', () => {
const objE = createObject(MULTI_NS_TYPE, 'id-E', obj7.id);
const objects = [obj1, obj2, obj4, obj5, obj6, obj7, obj8];
- const importIdMap = new Map([...objects, obj3].map(({ type, id }) => [`${type}:${id}`, {}]));
+ const importStateMap = new Map(
+ [...objects, obj3].map(({ type, id }) => [`${type}:${id}`, {}])
+ );
const setup = (ignoreRegularConflicts: boolean) => {
- const params = setupParams({ objects, importIdMap, ignoreRegularConflicts });
+ const params = setupParams({ objects, importStateMap, ignoreRegularConflicts });
// obj1 is a non-multi-namespace type, so it is skipped while searching
mockFindResult(); // find for obj2: the result is no match
mockFindResult(obj3); // find for obj4: the result is an inexact match with one destination that is exactly matched by obj3 so it is ignored -- accordingly, obj4 has no match
@@ -488,9 +509,9 @@ describe('#checkOriginConflicts', () => {
const params = setup(false);
const checkOriginConflictsResult = await checkOriginConflicts(params);
const expectedResult = {
- importIdMap: new Map([
- [`${obj7.type}:${obj7.id}`, { id: 'uuidv4', omitOriginId: true }],
- [`${obj8.type}:${obj8.id}`, { id: 'uuidv4', omitOriginId: true }],
+ importStateMap: new Map([
+ [`${obj7.type}:${obj7.id}`, { destinationId: 'uuidv4', omitOriginId: true }],
+ [`${obj8.type}:${obj8.id}`, { destinationId: 'uuidv4', omitOriginId: true }],
]),
errors: [
createConflictError(obj5, objA.id),
@@ -498,7 +519,6 @@ describe('#checkOriginConflicts', () => {
],
pendingOverwrites: new Set(),
};
- expect(mockUuidv4).toHaveBeenCalledTimes(2);
expect(checkOriginConflictsResult).toEqual(expectedResult);
});
@@ -506,74 +526,16 @@ describe('#checkOriginConflicts', () => {
const params = setup(true);
const checkOriginConflictsResult = await checkOriginConflicts(params);
const expectedResult = {
- importIdMap: new Map([
- [`${obj5.type}:${obj5.id}`, { id: objA.id }],
- [`${obj7.type}:${obj7.id}`, { id: 'uuidv4', omitOriginId: true }],
- [`${obj8.type}:${obj8.id}`, { id: 'uuidv4', omitOriginId: true }],
+ importStateMap: new Map([
+ [`${obj5.type}:${obj5.id}`, { destinationId: objA.id }],
+ [`${obj7.type}:${obj7.id}`, { destinationId: 'uuidv4', omitOriginId: true }],
+ [`${obj8.type}:${obj8.id}`, { destinationId: 'uuidv4', omitOriginId: true }],
]),
errors: [createAmbiguousConflictError(obj6, [objB, objC])],
pendingOverwrites: new Set([`${obj5.type}:${obj5.id}`]),
};
- expect(mockUuidv4).toHaveBeenCalledTimes(2);
expect(checkOriginConflictsResult).toEqual(expectedResult);
});
});
});
});
-
-describe('#getImportIdMapForRetries', () => {
- const createRetry = (
- { type, id }: { type: string; id: string },
- params: { destinationId?: string; createNewCopy?: boolean } = {}
- ): SavedObjectsImportRetry => {
- const { destinationId, createNewCopy } = params;
- return { type, id, overwrite: false, destinationId, replaceReferences: [], createNewCopy };
- };
-
- test('throws an error if retry is not found for an object', async () => {
- const obj1 = createObject(MULTI_NS_TYPE, 'id-1');
- const obj2 = createObject(MULTI_NS_TYPE, 'id-2');
- const objects = [obj1, obj2];
- const retries = [createRetry(obj1)];
- const params = { objects, retries, createNewCopies: false };
-
- expect(() => getImportIdMapForRetries(params)).toThrowErrorMatchingInlineSnapshot(
- `"Retry was expected for \\"multi:id-2\\" but not found"`
- );
- });
-
- test('returns expected results', async () => {
- const obj1 = createObject('type-1', 'id-1');
- const obj2 = createObject('type-2', 'id-2');
- const obj3 = createObject('type-3', 'id-3');
- const obj4 = createObject('type-4', 'id-4');
- const objects = [obj1, obj2, obj3, obj4];
- const retries = [
- createRetry(obj1), // retries that do not have `destinationId` specified are ignored
- createRetry(obj2, { destinationId: obj2.id }), // retries that have `id` that matches `destinationId` are ignored
- createRetry(obj3, { destinationId: 'id-X' }), // this retry will get added to the `importIdMap`!
- createRetry(obj4, { destinationId: 'id-Y', createNewCopy: true }), // this retry will get added to the `importIdMap`!
- ];
- const params = { objects, retries, createNewCopies: false };
-
- const checkOriginConflictsResult = await getImportIdMapForRetries(params);
- expect(checkOriginConflictsResult).toEqual(
- new Map([
- [`${obj3.type}:${obj3.id}`, { id: 'id-X', omitOriginId: false }],
- [`${obj4.type}:${obj4.id}`, { id: 'id-Y', omitOriginId: true }],
- ])
- );
- });
-
- test('omits origin ID in `importIdMap` entries when createNewCopies=true', async () => {
- const obj = createObject('type-1', 'id-1');
- const objects = [obj];
- const retries = [createRetry(obj, { destinationId: 'id-X' })];
- const params = { objects, retries, createNewCopies: true };
-
- const checkOriginConflictsResult = await getImportIdMapForRetries(params);
- expect(checkOriginConflictsResult).toEqual(
- new Map([[`${obj.type}:${obj.id}`, { id: 'id-X', omitOriginId: true }]])
- );
- });
-});
diff --git a/src/core/server/saved_objects/import/lib/check_origin_conflicts.ts b/src/core/server/saved_objects/import/lib/check_origin_conflicts.ts
index d689f37f5ad260..f1bb1afb2e3e4a 100644
--- a/src/core/server/saved_objects/import/lib/check_origin_conflicts.ts
+++ b/src/core/server/saved_objects/import/lib/check_origin_conflicts.ts
@@ -8,13 +8,10 @@
import pMap from 'p-map';
import { v4 as uuidv4 } from 'uuid';
-import {
- SavedObject,
- SavedObjectsClientContract,
- SavedObjectsImportFailure,
- SavedObjectsImportRetry,
-} from '../../types';
+import { SavedObject, SavedObjectsClientContract, SavedObjectsImportFailure } from '../../types';
import { ISavedObjectTypeRegistry } from '../../saved_objects_type_registry';
+import type { ImportStateMap } from './types';
+import { createOriginQuery } from './utils';
interface CheckOriginConflictsParams {
objects: Array>;
@@ -22,19 +19,15 @@ interface CheckOriginConflictsParams {
typeRegistry: ISavedObjectTypeRegistry;
namespace?: string;
ignoreRegularConflicts?: boolean;
- importIdMap: Map;
+ importStateMap: ImportStateMap;
+ pendingOverwrites: Set;
}
-type CheckOriginConflictParams = Omit & {
+type CheckOriginConflictParams = Omit & {
object: SavedObject<{ title?: string }>;
+ objectIdsBeingImported: Set;
};
-interface GetImportIdMapForRetriesParams {
- objects: SavedObject[];
- retries: SavedObjectsImportRetry[];
- createNewCopies: boolean;
-}
-
interface InexactMatch {
object: SavedObject;
destinations: Array<{ id: string; title?: string; updatedAt?: string }>;
@@ -52,9 +45,6 @@ const isLeft = (object: Either): object is Left => object.tag === 'left
const MAX_CONCURRENT_SEARCHES = 10;
-const createQueryTerm = (input: string) => input.replace(/\\/g, '\\\\').replace(/\"/g, '\\"');
-const createQuery = (type: string, id: string, rawIdPrefix: string) =>
- `"${createQueryTerm(`${rawIdPrefix}${type}:${id}`)}" | "${createQueryTerm(id)}"`;
const transformObjectsToAmbiguousConflictFields = (
objects: Array>
) =>
@@ -81,25 +71,32 @@ const getAmbiguousConflictSourceKey = ({ object }: InexactMatch) =>
* specified namespace:
* - A `Right` result indicates that no conflict destinations were found in this namespace ("no match").
* - A `Left` result indicates that one or more conflict destinations exist in this namespace, none of which exactly match this object's ID
- * ("inexact match"). We can make this assumption because any "exact match" results would have been obtained and filtered out by the
- * `checkConflicts` submodule, which is called before this.
+ * ("inexact match"). We can make this assumption because any "exact match" conflict errors would have been obtained and filtered out by
+ * the `checkConflicts` submodule, which is called before this, *or* if `overwrite: true` is used, we explicitly filter out any pending
+ * overwrites for exact matches.
*/
const checkOriginConflict = async (
params: CheckOriginConflictParams
): Promise> => {
- const { object, savedObjectsClient, typeRegistry, namespace, importIdMap } = params;
- const importIds = new Set(importIdMap.keys());
- const { type, originId } = object;
-
- if (!typeRegistry.isMultiNamespace(type)) {
+ const {
+ object,
+ savedObjectsClient,
+ typeRegistry,
+ namespace,
+ objectIdsBeingImported,
+ pendingOverwrites,
+ } = params;
+ const { type, originId, id } = object;
+
+ if (!typeRegistry.isMultiNamespace(type) || pendingOverwrites.has(`${type}:${id}`)) {
// Skip the search request for non-multi-namespace types, since by definition they cannot have inexact matches or ambiguous conflicts.
+ // Also skip the search request for objects that we've already determined have an "exact match" conflict.
return { tag: 'right', value: object };
}
- const search = createQuery(type, originId || object.id, namespace ? `${namespace}:` : '');
const findOptions = {
type,
- search,
+ search: createOriginQuery(type, originId || id),
rootSearchFields: ['_id', 'originId'],
page: 1,
perPage: 10,
@@ -114,7 +111,9 @@ const checkOriginConflict = async (
return { tag: 'right', value: object };
}
// This is an "inexact match" so far; filter the conflict destination(s) to exclude any that exactly match other objects we are importing.
- const objects = savedObjects.filter((obj) => !importIds.has(`${obj.type}:${obj.id}`));
+ const objects = savedObjects.filter(
+ (obj) => !objectIdsBeingImported.has(`${obj.type}:${obj.id}`)
+ );
const destinations = transformObjectsToAmbiguousConflictFields(objects);
if (destinations.length === 0) {
// No conflict destinations remain after filtering, so this is a "no match" result.
@@ -137,14 +136,20 @@ const checkOriginConflict = async (
* that match this object's `originId` or `id` exist in the specified namespace:
* - If this is a `Right` result; return the import object and allow `createSavedObjects` to handle the conflict (if any).
* - If this is a `Left` "partial match" result:
- * A. If there is a single source and destination match, add the destination to the importIdMap and return the import object, which
+ * A. If there is a single source and destination match, add the destination to the importStateMap and return the import object, which
* will allow `createSavedObjects` to modify the ID before creating the object (thus ensuring a conflict during).
* B. Otherwise, this is an "ambiguous conflict" result; return an error.
*/
export async function checkOriginConflicts({ objects, ...params }: CheckOriginConflictsParams) {
+ const objectIdsBeingImported = new Set();
+ for (const [key, { isOnlyReference }] of params.importStateMap.entries()) {
+ if (!isOnlyReference) {
+ objectIdsBeingImported.add(key);
+ }
+ }
// Check each object for possible destination conflicts, ensuring we don't too many concurrent searches running.
const mapper = async (object: SavedObject<{ title?: string }>) =>
- checkOriginConflict({ object, ...params });
+ checkOriginConflict({ object, objectIdsBeingImported, ...params });
const checkOriginConflictResults = await pMap(objects, mapper, {
concurrency: MAX_CONCURRENT_SEARCHES,
});
@@ -159,7 +164,7 @@ export async function checkOriginConflicts({ objects, ...params }: CheckOriginCo
}, new Map>>());
const errors: SavedObjectsImportFailure[] = [];
- const importIdMap = new Map();
+ const importStateMap: ImportStateMap = new Map();
const pendingOverwrites = new Set();
checkOriginConflictResults.forEach((result) => {
if (!isLeft(result)) {
@@ -174,7 +179,7 @@ export async function checkOriginConflicts({ objects, ...params }: CheckOriginCo
if (sources.length === 1 && destinations.length === 1) {
// This is a simple "inexact match" result -- a single import object has a single destination conflict.
if (params.ignoreRegularConflicts) {
- importIdMap.set(`${type}:${id}`, { id: destinations[0].id });
+ importStateMap.set(`${type}:${id}`, { destinationId: destinations[0].id });
pendingOverwrites.add(`${type}:${id}`);
} else {
const { title } = attributes;
@@ -198,7 +203,7 @@ export async function checkOriginConflicts({ objects, ...params }: CheckOriginCo
if (sources.length > 1) {
// In the case of ambiguous source conflicts, don't treat them as errors; instead, regenerate the object ID and reset its origin
// (e.g., the same outcome as if `createNewCopies` was enabled for the entire import operation).
- importIdMap.set(`${type}:${id}`, { id: uuidv4(), omitOriginId: true });
+ importStateMap.set(`${type}:${id}`, { destinationId: uuidv4(), omitOriginId: true });
return;
}
const { title } = attributes;
@@ -214,32 +219,5 @@ export async function checkOriginConflicts({ objects, ...params }: CheckOriginCo
});
});
- return { errors, importIdMap, pendingOverwrites };
-}
-
-/**
- * Assume that all objects exist in the `retries` map (due to filtering at the beginning of `resolveSavedObjectsImportErrors`).
- */
-export function getImportIdMapForRetries(params: GetImportIdMapForRetriesParams) {
- const { objects, retries, createNewCopies } = params;
-
- const retryMap = retries.reduce(
- (acc, cur) => acc.set(`${cur.type}:${cur.id}`, cur),
- new Map()
- );
- const importIdMap = new Map();
-
- objects.forEach(({ type, id }) => {
- const retry = retryMap.get(`${type}:${id}`);
- if (!retry) {
- throw new Error(`Retry was expected for "${type}:${id}" but not found`);
- }
- const { destinationId } = retry;
- const omitOriginId = createNewCopies || Boolean(retry.createNewCopy);
- if (destinationId && destinationId !== id) {
- importIdMap.set(`${type}:${id}`, { id: destinationId, omitOriginId });
- }
- });
-
- return importIdMap;
+ return { errors, importStateMap, pendingOverwrites };
}
diff --git a/src/core/server/saved_objects/import/lib/check_reference_origins.test.mock.ts b/src/core/server/saved_objects/import/lib/check_reference_origins.test.mock.ts
new file mode 100644
index 00000000000000..8fb5704af9d821
--- /dev/null
+++ b/src/core/server/saved_objects/import/lib/check_reference_origins.test.mock.ts
@@ -0,0 +1,14 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import type { createOriginQuery } from './utils';
+
+export const mockCreateOriginQuery = jest.fn() as jest.MockedFunction;
+jest.mock('./utils', () => ({
+ createOriginQuery: mockCreateOriginQuery,
+}));
diff --git a/src/core/server/saved_objects/import/lib/check_reference_origins.test.ts b/src/core/server/saved_objects/import/lib/check_reference_origins.test.ts
new file mode 100644
index 00000000000000..de162856b9873e
--- /dev/null
+++ b/src/core/server/saved_objects/import/lib/check_reference_origins.test.ts
@@ -0,0 +1,182 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { mockCreateOriginQuery } from './check_reference_origins.test.mock';
+
+import type { SavedObjectsFindResult } from '../../service';
+import type { SavedObjectsClientContract } from '../../types';
+import { checkReferenceOrigins, CheckReferenceOriginsParams } from './check_reference_origins';
+import { savedObjectsClientMock } from '../../../mocks';
+import { typeRegistryMock } from '../../saved_objects_type_registry.mock';
+import { ISavedObjectTypeRegistry } from '../../saved_objects_type_registry';
+import type { ImportStateMap } from './types';
+
+const MULTI_NS_TYPE = 'multi';
+const OTHER_TYPE = 'other';
+
+describe('checkReferenceOrigins', () => {
+ let savedObjectsClient: jest.Mocked;
+ let typeRegistry: jest.Mocked;
+ let find: typeof savedObjectsClient['find'];
+
+ const getResultMock = (...objectIds: string[]) => ({
+ page: 1,
+ per_page: 1,
+ total: objectIds.length,
+ saved_objects: objectIds.map((id) => ({ id, score: 0 } as unknown as SavedObjectsFindResult)),
+ });
+
+ const setupParams = (partial: {
+ namespace?: string;
+ importStateMap: ImportStateMap;
+ }): CheckReferenceOriginsParams => {
+ savedObjectsClient = savedObjectsClientMock.create();
+ find = savedObjectsClient.find;
+ find.mockResolvedValue(getResultMock()); // mock zero hits response by default
+ typeRegistry = typeRegistryMock.create();
+ typeRegistry.isMultiNamespace.mockImplementation((type) => type === MULTI_NS_TYPE);
+ return {
+ ...partial,
+ savedObjectsClient,
+ typeRegistry,
+ };
+ };
+
+ const mockFindResult = (...objectIds: string[]) => {
+ // doesn't matter if the mocked result is a "realistic" object, it just needs an `id` field
+ find.mockResolvedValueOnce(getResultMock(...objectIds));
+ };
+
+ describe('cluster calls', () => {
+ beforeEach(() => {
+ mockCreateOriginQuery.mockClear();
+ });
+
+ const expectFindArgs = (n: number, type: string, id: string) => {
+ expect(mockCreateOriginQuery).toHaveBeenNthCalledWith(n, type, id);
+ // exclude namespace from assertion -- a separate test covers that
+ expect(find).toHaveBeenNthCalledWith(n, expect.objectContaining({ type }));
+ };
+
+ test('does not execute searches for non-multi-namespace objects', async () => {
+ const params = setupParams({
+ importStateMap: new Map([[`${OTHER_TYPE}:1`, { isOnlyReference: true }]]),
+ });
+
+ await checkReferenceOrigins(params);
+ expect(find).not.toHaveBeenCalled();
+ });
+
+ test('does not execute searches for multi-namespace objects without the isOnlyReference attribute', async () => {
+ const params = setupParams({
+ importStateMap: new Map([[`${MULTI_NS_TYPE}:1`, {}]]),
+ });
+
+ await checkReferenceOrigins(params);
+ expect(find).not.toHaveBeenCalled();
+ });
+
+ test('executes searches for multi-namespace objects with the isOnlyReference attribute', async () => {
+ const params = setupParams({
+ importStateMap: new Map([[`${MULTI_NS_TYPE}:1`, { isOnlyReference: true }]]),
+ });
+
+ await checkReferenceOrigins(params);
+ expect(find).toHaveBeenCalledTimes(1);
+ expectFindArgs(1, MULTI_NS_TYPE, '1');
+ });
+
+ test('executes mixed searches', async () => {
+ const params = setupParams({
+ importStateMap: new Map([
+ [`${MULTI_NS_TYPE}:1`, {}],
+ [`${MULTI_NS_TYPE}:2`, { isOnlyReference: true }],
+ [`${OTHER_TYPE}:3`, { isOnlyReference: true }],
+ [`${MULTI_NS_TYPE}:4`, { isOnlyReference: true }],
+ ]),
+ });
+
+ await checkReferenceOrigins(params);
+ expect(find).toHaveBeenCalledTimes(2);
+ expectFindArgs(1, MULTI_NS_TYPE, '2');
+ expectFindArgs(2, MULTI_NS_TYPE, '4');
+ });
+
+ test('searches within the current `namespace`', async () => {
+ const namespace = 'some-namespace';
+ const params = setupParams({
+ namespace,
+ importStateMap: new Map([[`${MULTI_NS_TYPE}:1`, { isOnlyReference: true }]]),
+ });
+
+ await checkReferenceOrigins(params);
+ expect(find).toHaveBeenCalledTimes(1);
+ expect(find).toHaveBeenCalledWith(expect.objectContaining({ namespaces: [namespace] }));
+ });
+ });
+
+ describe('results', () => {
+ test('does not return an entry if search resulted in 0 matches', async () => {
+ const params = setupParams({
+ importStateMap: new Map([[`${MULTI_NS_TYPE}:1`, { isOnlyReference: true }]]),
+ });
+ // mock find returns an empty search result by default
+
+ const checkReferenceOriginsResult = await checkReferenceOrigins(params);
+
+ const expectedResult = {
+ importStateMap: new Map(),
+ };
+ expect(checkReferenceOriginsResult).toEqual(expectedResult);
+ });
+
+ test('does not return an entry if search resulted in 2+ matches', async () => {
+ const params = setupParams({
+ importStateMap: new Map([[`${MULTI_NS_TYPE}:1`, { isOnlyReference: true }]]),
+ });
+ mockFindResult('2', '3');
+
+ const checkReferenceOriginsResult = await checkReferenceOrigins(params);
+
+ const expectedResult = {
+ importStateMap: new Map(),
+ };
+ expect(checkReferenceOriginsResult).toEqual(expectedResult);
+ });
+
+ test('does not return an entry if search resulted in 1 match with the same ID', async () => {
+ const params = setupParams({
+ importStateMap: new Map([[`${MULTI_NS_TYPE}:1`, { isOnlyReference: true }]]),
+ });
+ mockFindResult('1');
+
+ const checkReferenceOriginsResult = await checkReferenceOrigins(params);
+
+ const expectedResult = {
+ importStateMap: new Map(),
+ };
+ expect(checkReferenceOriginsResult).toEqual(expectedResult);
+ });
+
+ test('returns an entry if search resulted in 1 match with a different ID', async () => {
+ const params = setupParams({
+ importStateMap: new Map([[`${MULTI_NS_TYPE}:1`, { isOnlyReference: true }]]),
+ });
+ mockFindResult('2');
+
+ const checkReferenceOriginsResult = await checkReferenceOrigins(params);
+
+ const expectedResult = {
+ importStateMap: new Map([
+ [`${MULTI_NS_TYPE}:1`, { isOnlyReference: true, destinationId: '2' }],
+ ]),
+ };
+ expect(checkReferenceOriginsResult).toEqual(expectedResult);
+ });
+ });
+});
diff --git a/src/core/server/saved_objects/import/lib/check_reference_origins.ts b/src/core/server/saved_objects/import/lib/check_reference_origins.ts
new file mode 100644
index 00000000000000..6ee4c615f3fed7
--- /dev/null
+++ b/src/core/server/saved_objects/import/lib/check_reference_origins.ts
@@ -0,0 +1,91 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import pMap from 'p-map';
+import { SavedObjectsClientContract } from '../../types';
+import { ISavedObjectTypeRegistry } from '../../saved_objects_type_registry';
+import type { ImportStateMap, ImportStateValue } from './types';
+import { getObjectKey, parseObjectKey } from '../../service/lib/internal_utils';
+import { createOriginQuery } from './utils';
+
+export interface CheckReferenceOriginsParams {
+ savedObjectsClient: SavedObjectsClientContract;
+ typeRegistry: ISavedObjectTypeRegistry;
+ namespace?: string;
+ importStateMap: ImportStateMap;
+}
+
+interface Reference {
+ type: string;
+ id: string;
+}
+
+const MAX_CONCURRENT_SEARCHES = 10;
+
+/**
+ * Searches for any existing object(s) for the given reference; if there is exactly one object with a matching origin *and* its ID is
+ * different than this reference ID, then this returns the different ID. Otherwise, it returns null.
+ */
+async function checkOrigin(
+ type: string,
+ id: string,
+ savedObjectsClient: SavedObjectsClientContract,
+ namespace: string | undefined
+) {
+ const findOptions = {
+ type,
+ search: createOriginQuery(type, id),
+ rootSearchFields: ['_id', 'originId'],
+ page: 1,
+ perPage: 1, // we only need one result for now
+ fields: ['title'], // we don't actually need the object's title, we just specify one field so we don't fetch *all* fields
+ sortField: 'updated_at',
+ sortOrder: 'desc' as const,
+ ...(namespace && { namespaces: [namespace] }),
+ };
+ const findResult = await savedObjectsClient.find<{ title?: string }>(findOptions);
+ const { total, saved_objects: savedObjects } = findResult;
+ if (total === 1) {
+ const [object] = savedObjects;
+ if (id !== object.id) {
+ return {
+ key: getObjectKey({ type, id }),
+ value: { isOnlyReference: true, destinationId: object.id } as ImportStateValue,
+ };
+ }
+ }
+ // TODO: if the total is 2+, return an "ambiguous reference origin match" to the consumer (#120313)
+ return null;
+}
+
+export async function checkReferenceOrigins(params: CheckReferenceOriginsParams) {
+ const { savedObjectsClient, namespace } = params;
+ const referencesToCheck: Reference[] = [];
+ for (const [key, { isOnlyReference }] of params.importStateMap.entries()) {
+ const { type, id } = parseObjectKey(key);
+ if (params.typeRegistry.isMultiNamespace(type) && isOnlyReference) {
+ referencesToCheck.push({ type, id });
+ }
+ }
+ // Check each object for possible destination conflicts, ensuring we don't too many concurrent searches running.
+ const mapper = async ({ type, id }: Reference) =>
+ checkOrigin(type, id, savedObjectsClient, namespace);
+ const checkOriginResults = await pMap(referencesToCheck, mapper, {
+ concurrency: MAX_CONCURRENT_SEARCHES,
+ });
+
+ const importStateMap: ImportStateMap = new Map();
+ for (const result of checkOriginResults) {
+ if (result) {
+ const { key, value } = result;
+ importStateMap.set(key, value);
+ }
+ }
+
+ return { importStateMap };
+}
diff --git a/src/core/server/saved_objects/import/lib/collect_saved_objects.test.ts b/src/core/server/saved_objects/import/lib/collect_saved_objects.test.ts
index c6307070d92315..b401d71ffe4980 100644
--- a/src/core/server/saved_objects/import/lib/collect_saved_objects.test.ts
+++ b/src/core/server/saved_objects/import/lib/collect_saved_objects.test.ts
@@ -39,8 +39,18 @@ describe('collectSavedObjects()', () => {
},
});
- const obj1 = { type: 'a', id: '1', attributes: { title: 'my title 1' } };
- const obj2 = { type: 'b', id: '2', attributes: { title: 'my title 2' } };
+ const obj1 = {
+ type: 'a',
+ id: '1',
+ attributes: { title: 'my title 1' },
+ references: [{ type: 'b', id: '2', name: 'b2' }],
+ };
+ const obj2 = {
+ type: 'b',
+ id: '2',
+ attributes: { title: 'my title 2' },
+ references: [{ type: 'c', id: '3', name: 'c3' }],
+ };
describe('module calls', () => {
test('limit stream with empty input stream is called with null', async () => {
@@ -120,17 +130,24 @@ describe('collectSavedObjects()', () => {
const readStream = createReadStream();
const result = await collectSavedObjects({ readStream, supportedTypes: [], objectLimit });
- expect(result).toEqual({ collectedObjects: [], errors: [], importIdMap: new Map() });
+ expect(result).toEqual({ collectedObjects: [], errors: [], importStateMap: new Map() });
});
test('collects objects from stream', async () => {
- const readStream = createReadStream(obj1);
- const supportedTypes = [obj1.type];
+ const readStream = createReadStream(obj1, obj2);
+ const supportedTypes = [obj1.type, obj2.type];
const result = await collectSavedObjects({ readStream, supportedTypes, objectLimit });
- const collectedObjects = [{ ...obj1, migrationVersion: {} }];
- const importIdMap = new Map([[`${obj1.type}:${obj1.id}`, {}]]);
- expect(result).toEqual({ collectedObjects, errors: [], importIdMap });
+ const collectedObjects = [
+ { ...obj1, migrationVersion: {} },
+ { ...obj2, migrationVersion: {} },
+ ];
+ const importStateMap = new Map([
+ [`a:1`, {}], // a:1 is included because it is present in the collected objects
+ [`b:2`, {}], // b:2 is included because it is present in the collected objects
+ [`c:3`, { isOnlyReference: true }], // c:3 is included because b:2 has a reference to c:3, but this is marked as `isOnlyReference` because c:3 is not present in the collected objects
+ ]);
+ expect(result).toEqual({ collectedObjects, errors: [], importStateMap });
});
test('unsupported types return as import errors', async () => {
@@ -141,20 +158,24 @@ describe('collectSavedObjects()', () => {
const error = { type: 'unsupported_type' };
const { title } = obj1.attributes;
const errors = [{ error, type: obj1.type, id: obj1.id, title, meta: { title } }];
- expect(result).toEqual({ collectedObjects: [], errors, importIdMap: new Map() });
+ expect(result).toEqual({ collectedObjects: [], errors, importStateMap: new Map() });
});
test('returns mixed results', async () => {
const readStream = createReadStream(obj1, obj2);
- const supportedTypes = [obj2.type];
+ const supportedTypes = [obj1.type];
const result = await collectSavedObjects({ readStream, supportedTypes, objectLimit });
- const collectedObjects = [{ ...obj2, migrationVersion: {} }];
- const importIdMap = new Map([[`${obj2.type}:${obj2.id}`, {}]]);
+ const collectedObjects = [{ ...obj1, migrationVersion: {} }];
+ const importStateMap = new Map([
+ [`a:1`, {}], // a:1 is included because it is present in the collected objects
+ [`b:2`, { isOnlyReference: true }], // b:2 was filtered out due to an unsupported type; b:2 is included because a:1 has a reference to b:2, but this is marked as `isOnlyReference` because b:2 is not present in the collected objects
+ // c:3 is not included at all, because b:2 was filtered out and there are no other references to c:3
+ ]);
const error = { type: 'unsupported_type' };
- const { title } = obj1.attributes;
- const errors = [{ error, type: obj1.type, id: obj1.id, title, meta: { title } }];
- expect(result).toEqual({ collectedObjects, errors, importIdMap });
+ const { title } = obj2.attributes;
+ const errors = [{ error, type: obj2.type, id: obj2.id, title, meta: { title } }];
+ expect(result).toEqual({ collectedObjects, errors, importStateMap });
});
describe('with optional filter', () => {
@@ -172,7 +193,7 @@ describe('collectSavedObjects()', () => {
const error = { type: 'unsupported_type' };
const { title } = obj1.attributes;
const errors = [{ error, type: obj1.type, id: obj1.id, title, meta: { title } }];
- expect(result).toEqual({ collectedObjects: [], errors, importIdMap: new Map() });
+ expect(result).toEqual({ collectedObjects: [], errors, importStateMap: new Map() });
});
test('does not filter out objects when result === true', async () => {
@@ -187,11 +208,15 @@ describe('collectSavedObjects()', () => {
});
const collectedObjects = [{ ...obj2, migrationVersion: {} }];
- const importIdMap = new Map([[`${obj2.type}:${obj2.id}`, {}]]);
+ const importStateMap = new Map([
+ // a:1 was filtered out due to an unsupported type; a:1 is not included because there are no other references to a:1
+ [`b:2`, {}], // b:2 is included because it is present in the collected objects
+ [`c:3`, { isOnlyReference: true }], // c:3 is included because b:2 has a reference to c:3, but this is marked as `isOnlyReference` because c:3 is not present in the collected objects
+ ]);
const error = { type: 'unsupported_type' };
const { title } = obj1.attributes;
const errors = [{ error, type: obj1.type, id: obj1.id, title, meta: { title } }];
- expect(result).toEqual({ collectedObjects, errors, importIdMap });
+ expect(result).toEqual({ collectedObjects, errors, importStateMap });
});
});
});
diff --git a/src/core/server/saved_objects/import/lib/collect_saved_objects.ts b/src/core/server/saved_objects/import/lib/collect_saved_objects.ts
index 58c7a759cf0bbb..209ae5ecf283e4 100644
--- a/src/core/server/saved_objects/import/lib/collect_saved_objects.ts
+++ b/src/core/server/saved_objects/import/lib/collect_saved_objects.ts
@@ -19,6 +19,7 @@ import { SavedObjectsImportFailure } from '../types';
import { SavedObjectsImportError } from '../errors';
import { getNonUniqueEntries } from './get_non_unique_entries';
import { createLimitStream } from './create_limit_stream';
+import type { ImportStateMap } from './types';
interface CollectSavedObjectsOptions {
readStream: Readable;
@@ -35,7 +36,7 @@ export async function collectSavedObjects({
}: CollectSavedObjectsOptions) {
const errors: SavedObjectsImportFailure[] = [];
const entries: Array<{ type: string; id: string }> = [];
- const importIdMap = new Map();
+ const importStateMap: ImportStateMap = new Map();
const collectedObjects: Array> = await createPromiseFromStreams([
readStream,
createLimitStream(objectLimit),
@@ -58,7 +59,13 @@ export async function collectSavedObjects({
}),
createFilterStream((obj) => (filter ? filter(obj) : true)),
createMapStream((obj: SavedObject) => {
- importIdMap.set(`${obj.type}:${obj.id}`, {});
+ importStateMap.set(`${obj.type}:${obj.id}`, {});
+ for (const ref of obj.references ?? []) {
+ const key = `${ref.type}:${ref.id}`;
+ if (!importStateMap.has(key)) {
+ importStateMap.set(key, { isOnlyReference: true });
+ }
+ }
// Ensure migrations execute on every saved object
return Object.assign({ migrationVersion: {} }, obj);
}),
@@ -74,6 +81,6 @@ export async function collectSavedObjects({
return {
errors,
collectedObjects,
- importIdMap,
+ importStateMap,
};
}
diff --git a/src/core/server/saved_objects/import/lib/create_saved_objects.test.ts b/src/core/server/saved_objects/import/lib/create_saved_objects.test.ts
index 38372e8fad6fd4..7f8b67406773ef 100644
--- a/src/core/server/saved_objects/import/lib/create_saved_objects.test.ts
+++ b/src/core/server/saved_objects/import/lib/create_saved_objects.test.ts
@@ -23,8 +23,8 @@ const createObject = (type: string, id: string, originId?: string): SavedObject
attributes: {},
references: [
{ name: 'name-1', type: 'other-type', id: 'other-id' }, // object that is not present
- { name: 'name-2', type: MULTI_NS_TYPE, id: 'id-1' }, // object that is present, but does not have an importIdMap entry
- { name: 'name-3', type: MULTI_NS_TYPE, id: 'id-3' }, // object that is present and has an importIdMap entry
+ { name: 'name-2', type: MULTI_NS_TYPE, id: 'id-1' }, // object that is present, but does not have an importStateMap entry
+ { name: 'name-3', type: MULTI_NS_TYPE, id: 'id-3' }, // object that is present and has an importStateMap entry
],
...(originId && { originId }),
});
@@ -52,10 +52,10 @@ const obj13 = createObject(OTHER_TYPE, 'id-13'); // -> conflict
const importId3 = 'id-foo';
const importId4 = 'id-bar';
const importId8 = 'id-baz';
-const importIdMap = new Map([
- [`${obj3.type}:${obj3.id}`, { id: importId3, omitOriginId: true }],
- [`${obj4.type}:${obj4.id}`, { id: importId4 }],
- [`${obj8.type}:${obj8.id}`, { id: importId8 }],
+const importStateMap = new Map([
+ [`${obj3.type}:${obj3.id}`, { destinationId: importId3, omitOriginId: true }],
+ [`${obj4.type}:${obj4.id}`, { destinationId: importId4 }],
+ [`${obj8.type}:${obj8.id}`, { destinationId: importId8 }],
]);
describe('#createSavedObjects', () => {
@@ -74,7 +74,7 @@ describe('#createSavedObjects', () => {
}): CreateSavedObjectsParams => {
savedObjectsClient = savedObjectsClientMock.create();
bulkCreate = savedObjectsClient.bulkCreate;
- return { accumulatedErrors: [], ...partial, savedObjectsClient, importIdMap };
+ return { accumulatedErrors: [], ...partial, savedObjectsClient, importStateMap };
};
const getExpectedBulkCreateArgsObjects = (objects: SavedObject[], retry?: boolean) =>
@@ -84,8 +84,8 @@ describe('#createSavedObjects', () => {
attributes,
references: [
{ name: 'name-1', type: 'other-type', id: 'other-id' }, // object that is not present
- { name: 'name-2', type: MULTI_NS_TYPE, id: 'id-1' }, // object that is present, but does not have an importIdMap entry
- { name: 'name-3', type: MULTI_NS_TYPE, id: 'id-foo' }, // object that is present and has an importIdMap entry
+ { name: 'name-2', type: MULTI_NS_TYPE, id: 'id-1' }, // object that is present, but does not have an importStateMap entry
+ { name: 'name-3', type: MULTI_NS_TYPE, id: 'id-foo' }, // object that is present and has an importStateMap entry
],
// if the import object had an originId, and/or if we regenerated the id, expect an originId to be included in the create args
...((originId || retry) && { originId: originId || id }),
@@ -245,7 +245,7 @@ describe('#createSavedObjects', () => {
await createSavedObjects(options);
expect(bulkCreate).toHaveBeenCalledTimes(1);
- // these three objects are transformed before being created, because they are included in the `importIdMap`
+ // these three objects are transformed before being created, because they are included in the `importStateMap`
const x3 = { ...obj3, id: importId3, originId: undefined }; // this import object already has an originId, but the entry has omitOriginId=true
const x4 = { ...obj4, id: importId4 }; // this import object already has an originId
const x8 = { ...obj8, id: importId8, originId: obj8.id }; // this import object doesn't have an originId, so it is set before create
diff --git a/src/core/server/saved_objects/import/lib/create_saved_objects.ts b/src/core/server/saved_objects/import/lib/create_saved_objects.ts
index 66792642ea24e3..bf58b2bb4b00e0 100644
--- a/src/core/server/saved_objects/import/lib/create_saved_objects.ts
+++ b/src/core/server/saved_objects/import/lib/create_saved_objects.ts
@@ -9,16 +9,17 @@
import { SavedObject, SavedObjectsClientContract, SavedObjectsImportFailure } from '../../types';
import { extractErrors } from './extract_errors';
import { CreatedObject } from '../types';
+import type { ImportStateMap } from './types';
-interface CreateSavedObjectsParams {
+export interface CreateSavedObjectsParams {
objects: Array>;
accumulatedErrors: SavedObjectsImportFailure[];
savedObjectsClient: SavedObjectsClientContract;
- importIdMap: Map;
+ importStateMap: ImportStateMap;
namespace?: string;
overwrite?: boolean;
}
-interface CreateSavedObjectsResult {
+export interface CreateSavedObjectsResult {
createdObjects: Array>;
errors: SavedObjectsImportFailure[];
}
@@ -31,7 +32,7 @@ export const createSavedObjects = async ({
objects,
accumulatedErrors,
savedObjectsClient,
- importIdMap,
+ importStateMap,
namespace,
overwrite,
}: CreateSavedObjectsParams): Promise> => {
@@ -58,19 +59,24 @@ export const createSavedObjects = async ({
// use the import ID map to ensure that each reference is being created with the correct ID
const references = object.references?.map((reference) => {
const { type, id } = reference;
- const importIdEntry = importIdMap.get(`${type}:${id}`);
- if (importIdEntry?.id) {
- return { ...reference, id: importIdEntry.id };
+ const importStateValue = importStateMap.get(`${type}:${id}`);
+ if (importStateValue?.destinationId) {
+ return { ...reference, id: importStateValue.destinationId };
}
return reference;
});
// use the import ID map to ensure that each object is being created with the correct ID, also ensure that the `originId` is set on
// the created object if it did not have one (or is omitted if specified)
- const importIdEntry = importIdMap.get(`${object.type}:${object.id}`);
- if (importIdEntry?.id) {
- objectIdMap.set(`${object.type}:${importIdEntry.id}`, object);
- const originId = importIdEntry.omitOriginId ? undefined : object.originId ?? object.id;
- return { ...object, id: importIdEntry.id, originId, ...(references && { references }) };
+ const importStateValue = importStateMap.get(`${object.type}:${object.id}`);
+ if (importStateValue?.destinationId) {
+ objectIdMap.set(`${object.type}:${importStateValue.destinationId}`, object);
+ const originId = importStateValue.omitOriginId ? undefined : object.originId ?? object.id;
+ return {
+ ...object,
+ id: importStateValue.destinationId,
+ originId,
+ ...(references && { references }),
+ };
}
return { ...object, ...(references && { references }) };
});
diff --git a/src/core/server/saved_objects/import/lib/execute_import_hooks.ts b/src/core/server/saved_objects/import/lib/execute_import_hooks.ts
index 1595d52ca8c0ed..c0b5ae0437b2c8 100644
--- a/src/core/server/saved_objects/import/lib/execute_import_hooks.ts
+++ b/src/core/server/saved_objects/import/lib/execute_import_hooks.ts
@@ -9,7 +9,7 @@
import { SavedObject } from '../../types';
import { SavedObjectsImportHook, SavedObjectsImportWarning } from '../types';
-interface ExecuteImportHooksOptions {
+export interface ExecuteImportHooksOptions {
objects: SavedObject[];
importHooks: Record;
}
diff --git a/src/core/server/saved_objects/import/lib/get_import_state_map_for_retries.test.ts b/src/core/server/saved_objects/import/lib/get_import_state_map_for_retries.test.ts
new file mode 100644
index 00000000000000..af5aca10ba289d
--- /dev/null
+++ b/src/core/server/saved_objects/import/lib/get_import_state_map_for_retries.test.ts
@@ -0,0 +1,68 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import type { SavedObject } from '../../types';
+import type { SavedObjectsImportRetry } from '../types';
+import { getImportStateMapForRetries } from './get_import_state_map_for_retries';
+
+describe('#getImportStateMapForRetries', () => {
+ const createRetry = (
+ { type, id }: { type: string; id: string },
+ params: { destinationId?: string; createNewCopy?: boolean } = {}
+ ): SavedObjectsImportRetry => {
+ const { destinationId, createNewCopy } = params;
+ return { type, id, overwrite: false, destinationId, replaceReferences: [], createNewCopy };
+ };
+
+ test('throws an error if retry is not found for an object', async () => {
+ const obj1 = { type: 'type-1', id: 'id-1' };
+ const obj2 = { type: 'type-2', id: 'id-2' };
+ const objects = [obj1, obj2] as SavedObject[];
+ const retries = [createRetry(obj1)];
+ const params = { objects, retries, createNewCopies: false };
+
+ expect(() => getImportStateMapForRetries(params)).toThrowErrorMatchingInlineSnapshot(
+ `"Retry was expected for \\"type-2:id-2\\" but not found"`
+ );
+ });
+
+ test('returns expected results', async () => {
+ const obj1 = { type: 'type-1', id: 'id-1' };
+ const obj2 = { type: 'type-2', id: 'id-2' };
+ const obj3 = { type: 'type-3', id: 'id-3' };
+ const obj4 = { type: 'type-4', id: 'id-4' };
+ const objects = [obj1, obj2, obj3, obj4] as SavedObject[];
+ const retries = [
+ createRetry(obj1), // retries that do not have `destinationId` specified are ignored
+ createRetry(obj2, { destinationId: obj2.id }), // retries that have `id` that matches `destinationId` are ignored
+ createRetry(obj3, { destinationId: 'id-X' }), // this retry will get added to the `importStateMap`!
+ createRetry(obj4, { destinationId: 'id-Y', createNewCopy: true }), // this retry will get added to the `importStateMap`!
+ ];
+ const params = { objects, retries, createNewCopies: false };
+
+ const result = await getImportStateMapForRetries(params);
+ expect(result).toEqual(
+ new Map([
+ [`${obj3.type}:${obj3.id}`, { destinationId: 'id-X', omitOriginId: false }],
+ [`${obj4.type}:${obj4.id}`, { destinationId: 'id-Y', omitOriginId: true }],
+ ])
+ );
+ });
+
+ test('omits origin ID in `importStateMap` entries when createNewCopies=true', async () => {
+ const obj1 = { type: 'type-1', id: 'id-1' };
+ const objects = [obj1] as SavedObject[];
+ const retries = [createRetry(obj1, { destinationId: 'id-X' })];
+ const params = { objects, retries, createNewCopies: true };
+
+ const result = await getImportStateMapForRetries(params);
+ expect(result).toEqual(
+ new Map([[`${obj1.type}:${obj1.id}`, { destinationId: 'id-X', omitOriginId: true }]])
+ );
+ });
+});
diff --git a/src/core/server/saved_objects/import/lib/get_import_state_map_for_retries.ts b/src/core/server/saved_objects/import/lib/get_import_state_map_for_retries.ts
new file mode 100644
index 00000000000000..3066ae72738a48
--- /dev/null
+++ b/src/core/server/saved_objects/import/lib/get_import_state_map_for_retries.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
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { SavedObject, SavedObjectsImportRetry } from '../../types';
+import type { ImportStateMap } from './types';
+
+interface GetImportStateMapForRetriesParams {
+ objects: SavedObject[];
+ retries: SavedObjectsImportRetry[];
+ createNewCopies: boolean;
+}
+
+/**
+ * Assume that all objects exist in the `retries` map (due to filtering at the beginning of `resolveSavedObjectsImportErrors`).
+ */
+export function getImportStateMapForRetries(params: GetImportStateMapForRetriesParams) {
+ const { objects, retries, createNewCopies } = params;
+
+ const retryMap = retries.reduce(
+ (acc, cur) => acc.set(`${cur.type}:${cur.id}`, cur),
+ new Map()
+ );
+ const importStateMap: ImportStateMap = new Map();
+
+ objects.forEach(({ type, id }) => {
+ const retry = retryMap.get(`${type}:${id}`);
+ if (!retry) {
+ throw new Error(`Retry was expected for "${type}:${id}" but not found`);
+ }
+ const { destinationId } = retry;
+ const omitOriginId = createNewCopies || Boolean(retry.createNewCopy);
+ if (destinationId && destinationId !== id) {
+ importStateMap.set(`${type}:${id}`, { destinationId, omitOriginId });
+ }
+ });
+
+ return importStateMap;
+}
diff --git a/src/core/server/saved_objects/import/lib/index.ts b/src/core/server/saved_objects/import/lib/index.ts
index ab1c34b2032df2..7d0c2fb2147e35 100644
--- a/src/core/server/saved_objects/import/lib/index.ts
+++ b/src/core/server/saved_objects/import/lib/index.ts
@@ -7,15 +7,18 @@
*/
export { checkConflicts } from './check_conflicts';
-export { checkOriginConflicts, getImportIdMapForRetries } from './check_origin_conflicts';
+export { checkReferenceOrigins } from './check_reference_origins';
+export { checkOriginConflicts } from './check_origin_conflicts';
export { collectSavedObjects } from './collect_saved_objects';
export { createLimitStream } from './create_limit_stream';
export { createObjectsFilter } from './create_objects_filter';
export { createSavedObjects } from './create_saved_objects';
export { extractErrors } from './extract_errors';
+export { getImportStateMapForRetries } from './get_import_state_map_for_retries';
export { getNonUniqueEntries } from './get_non_unique_entries';
export { regenerateIds } from './regenerate_ids';
export { splitOverwrites } from './split_overwrites';
-export { getNonExistingReferenceAsKeys, validateReferences } from './validate_references';
+export { validateReferences } from './validate_references';
export { validateRetries } from './validate_retries';
export { executeImportHooks } from './execute_import_hooks';
+export type { ImportStateMap, ImportStateValue } from './types';
diff --git a/src/core/server/saved_objects/import/lib/regenerate_ids.test.ts b/src/core/server/saved_objects/import/lib/regenerate_ids.test.ts
index 2696a52e0554f3..d22b9431367d46 100644
--- a/src/core/server/saved_objects/import/lib/regenerate_ids.test.ts
+++ b/src/core/server/saved_objects/import/lib/regenerate_ids.test.ts
@@ -6,37 +6,31 @@
* Side Public License, v 1.
*/
-import { mockUuidv4 } from './__mocks__';
import { regenerateIds } from './regenerate_ids';
import { SavedObject } from '../../types';
+jest.mock('uuid', () => ({
+ v4: jest
+ .fn()
+ .mockReturnValueOnce('uuidv4 #1')
+ .mockReturnValueOnce('uuidv4 #2')
+ .mockReturnValueOnce('uuidv4 #3'),
+}));
+
describe('#regenerateIds', () => {
const objects = [
{ type: 'foo', id: '1' },
{ type: 'bar', id: '2' },
{ type: 'baz', id: '3' },
- ] as any as SavedObject[];
+ ] as SavedObject[];
test('returns expected values', () => {
- mockUuidv4
- .mockReturnValueOnce('uuidv4 #1')
- .mockReturnValueOnce('uuidv4 #2')
- .mockReturnValueOnce('uuidv4 #3');
- expect(regenerateIds(objects)).toMatchInlineSnapshot(`
- Map {
- "foo:1" => Object {
- "id": "uuidv4 #1",
- "omitOriginId": true,
- },
- "bar:2" => Object {
- "id": "uuidv4 #2",
- "omitOriginId": true,
- },
- "baz:3" => Object {
- "id": "uuidv4 #3",
- "omitOriginId": true,
- },
- }
- `);
+ expect(regenerateIds(objects)).toEqual(
+ new Map([
+ ['foo:1', { destinationId: 'uuidv4 #1', omitOriginId: true }],
+ ['bar:2', { destinationId: 'uuidv4 #2', omitOriginId: true }],
+ ['baz:3', { destinationId: 'uuidv4 #3', omitOriginId: true }],
+ ])
+ );
});
});
diff --git a/src/core/server/saved_objects/import/lib/regenerate_ids.ts b/src/core/server/saved_objects/import/lib/regenerate_ids.ts
index 01ce8bd93c01a1..174658555aaf1e 100644
--- a/src/core/server/saved_objects/import/lib/regenerate_ids.ts
+++ b/src/core/server/saved_objects/import/lib/regenerate_ids.ts
@@ -8,15 +8,17 @@
import { v4 as uuidv4 } from 'uuid';
import { SavedObject } from '../../types';
+import type { ImportStateMap } from './types';
/**
- * Takes an array of saved objects and returns an importIdMap of randomly-generated new IDs.
+ * Takes an array of saved objects and returns an importStateMap of randomly-generated new IDs.
*
* @param objects The saved objects to generate new IDs for.
*/
export const regenerateIds = (objects: SavedObject[]) => {
- const importIdMap = objects.reduce((acc, object) => {
- return acc.set(`${object.type}:${object.id}`, { id: uuidv4(), omitOriginId: true });
- }, new Map());
- return importIdMap;
+ const importStateMap: ImportStateMap = new Map();
+ for (const { type, id } of objects) {
+ importStateMap.set(`${type}:${id}`, { destinationId: uuidv4(), omitOriginId: true });
+ }
+ return importStateMap;
};
diff --git a/src/core/server/saved_objects/import/lib/types.ts b/src/core/server/saved_objects/import/lib/types.ts
new file mode 100644
index 00000000000000..ccc0373a80de57
--- /dev/null
+++ b/src/core/server/saved_objects/import/lib/types.ts
@@ -0,0 +1,35 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+/**
+ * This map contains entries for objects that are included in the import operation. The entry key is the object's `type:id`, and the entry
+ * value contains optional attributes which change how that object is created. The initial map that is created by the collectSavedObjects
+ * module contains one entry with an empty value for each object that is being imported.
+ *
+ * This map is meant to function as a sort of accumulator; each module that is called during the import process can emit new entries that
+ * will override those from the initial map.
+ */
+export type ImportStateMap = Map;
+
+/**
+ * The value of an import state entry, which contains optional attributes that change how the object is created.
+ */
+export interface ImportStateValue {
+ /**
+ * This attribute indicates that the object for this entry is *only* a reference, it does not exist in the import file.
+ */
+ isOnlyReference?: boolean;
+ /**
+ * This attribute indicates that the object should have this ID instead of what was specified in the import file.
+ */
+ destinationId?: string;
+ /**
+ * This attribute indicates that the object's originId should be cleared.
+ */
+ omitOriginId?: boolean;
+}
diff --git a/src/core/server/saved_objects/import/lib/utils.test.ts b/src/core/server/saved_objects/import/lib/utils.test.ts
new file mode 100644
index 00000000000000..19ecd38283b423
--- /dev/null
+++ b/src/core/server/saved_objects/import/lib/utils.test.ts
@@ -0,0 +1,26 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { createOriginQuery } from './utils';
+
+describe('createOriginQuery', () => {
+ it('returns expected simple query string', () => {
+ const result = createOriginQuery('a', 'b');
+ expect(result).toEqual('"a:b" | "b"');
+ });
+
+ it('escapes double quotes', () => {
+ const result = createOriginQuery('a"', 'b"');
+ expect(result).toEqual('"a\\":b\\"" | "b\\""');
+ });
+
+ it('escapes backslashes', () => {
+ const result = createOriginQuery('a\\', 'b\\');
+ expect(result).toEqual('"a\\\\:b\\\\" | "b\\\\"');
+ });
+});
diff --git a/src/core/server/saved_objects/import/lib/utils.ts b/src/core/server/saved_objects/import/lib/utils.ts
new file mode 100644
index 00000000000000..7b4f188f895799
--- /dev/null
+++ b/src/core/server/saved_objects/import/lib/utils.ts
@@ -0,0 +1,26 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+function createOriginQueryTerm(input: string) {
+ return input.replace(/\\/g, '\\\\').replace(/\"/g, '\\"');
+}
+
+/**
+ * @internal
+ * Constructs a simple query string for an object that will match any existing objects with the same origin.
+ * This matches based on the object's raw document ID (_id) or the object's originId.
+ *
+ * @param type a saved object type
+ * @param id a saved object ID to check; this should be the object's originId if present, otherwise it should be the object's ID
+ * @returns a simple query string
+ */
+export function createOriginQuery(type: string, id: string) {
+ // 1st query term will match raw object IDs (_id), 2nd query term will match originId
+ // we intentionally do not include a namespace prefix for the raw object IDs, because this search is only for multi-namespace object types
+ return `"${createOriginQueryTerm(`${type}:${id}`)}" | "${createOriginQueryTerm(id)}"`;
+}
diff --git a/src/core/server/saved_objects/import/lib/validate_references.test.ts b/src/core/server/saved_objects/import/lib/validate_references.test.ts
index c6cbc2cacc759d..2e6f1a5e0a9a27 100644
--- a/src/core/server/saved_objects/import/lib/validate_references.test.ts
+++ b/src/core/server/saved_objects/import/lib/validate_references.test.ts
@@ -6,549 +6,251 @@
* Side Public License, v 1.
*/
-import { getNonExistingReferenceAsKeys, validateReferences } from './validate_references';
+import type { ValidateReferencesParams } from './validate_references';
+import { validateReferences } from './validate_references';
import { savedObjectsClientMock } from '../../../mocks';
import { SavedObjectsErrorHelpers } from '../../service';
-describe('getNonExistingReferenceAsKeys()', () => {
+function setup({
+ objects = [],
+ namespace,
+ importStateMap = new Map(),
+ retries,
+}: Partial> = {}) {
const savedObjectsClient = savedObjectsClientMock.create();
+ return { objects, savedObjectsClient, namespace, importStateMap, retries };
+}
- beforeEach(() => {
- jest.resetAllMocks();
- });
-
- test('returns empty response when no objects exist', async () => {
- const result = await getNonExistingReferenceAsKeys([], savedObjectsClient);
- expect(result).toEqual([]);
- expect(savedObjectsClient.bulkGet).toHaveBeenCalledTimes(0);
- });
+function createNotFoundError({ type, id }: { type: string; id: string }) {
+ const error = SavedObjectsErrorHelpers.createGenericNotFoundError(type, id).output.payload;
+ return { type, id, error, attributes: {}, references: [] };
+}
- test('skips objects when ignoreMissingReferences is included in retry', async () => {
- const savedObjects = [
- {
- id: '2',
- type: 'visualization',
- attributes: {},
- references: [{ name: 'ref_0', type: 'index-pattern', id: '1' }],
- },
- ];
- const retries = [
- {
- type: 'visualization',
- id: '2',
- overwrite: false,
- replaceReferences: [],
- ignoreMissingReferences: true,
- },
- ];
- const result = await getNonExistingReferenceAsKeys(
- savedObjects,
- savedObjectsClient,
- undefined,
- retries
- );
- expect(result).toEqual([]);
- expect(savedObjectsClient.bulkGet).not.toHaveBeenCalled();
- });
+describe('validateReferences()', () => {
+ test('does not call cluster and returns empty when no objects are passed in', async () => {
+ const params = setup();
- test('removes references that exist within savedObjects', async () => {
- const savedObjects = [
- {
- id: '1',
- type: 'index-pattern',
- attributes: {},
- references: [],
- },
- {
- id: '2',
- type: 'visualization',
- attributes: {},
- references: [
- {
- name: 'ref_0',
- type: 'index-pattern',
- id: '1',
- },
- ],
- },
- ];
- const result = await getNonExistingReferenceAsKeys(savedObjects, savedObjectsClient);
+ const result = await validateReferences(params);
expect(result).toEqual([]);
- expect(savedObjectsClient.bulkGet).toHaveBeenCalledTimes(0);
+ expect(params.savedObjectsClient.bulkGet).not.toHaveBeenCalled();
});
- test('removes references that exist within es', async () => {
- const savedObjects = [
- {
- id: '2',
- type: 'visualization',
- attributes: {},
- references: [
- {
- name: 'ref_0',
- type: 'index-pattern',
- id: '1',
- },
- ],
- },
- ];
- savedObjectsClient.bulkGet.mockResolvedValueOnce({
- saved_objects: [
+ test('returns errors when references are missing', async () => {
+ const params = setup({
+ objects: [
{
id: '1',
- type: 'index-pattern',
+ type: 'visualization',
attributes: {},
references: [],
},
+ {
+ id: '2',
+ type: 'visualization',
+ attributes: { title: 'My Visualization 2' },
+ references: [{ name: 'ref_0', type: 'index-pattern', id: '3' }],
+ },
+ {
+ id: '4',
+ type: 'visualization',
+ attributes: {},
+ references: [
+ { name: 'ref_0', type: 'index-pattern', id: '5' },
+ { name: 'ref_1', type: 'index-pattern', id: '6' },
+ { name: 'ref_2', type: 'search', id: '7' },
+ { name: 'ref_3', type: 'search', id: '8' },
+ ],
+ },
+ ],
+ });
+ params.savedObjectsClient.bulkGet.mockResolvedValue({
+ saved_objects: [
+ createNotFoundError({ type: 'index-pattern', id: '3' }),
+ createNotFoundError({ type: 'index-pattern', id: '5' }),
+ createNotFoundError({ type: 'index-pattern', id: '6' }),
+ createNotFoundError({ type: 'search', id: '7' }),
+ { id: '8', type: 'search', attributes: {}, references: [] },
],
});
- const result = await getNonExistingReferenceAsKeys(savedObjects, savedObjectsClient);
- expect(result).toEqual([]);
- expect(savedObjectsClient.bulkGet).toMatchInlineSnapshot(`
- [MockFunction] {
- "calls": Array [
- Array [
- Array [
- Object {
- "fields": Array [
- "id",
- ],
- "id": "1",
- "type": "index-pattern",
- },
- ],
- Object {
- "namespace": undefined,
- },
- ],
- ],
- "results": Array [
- Object {
- "type": "return",
- "value": Promise {},
- },
- ],
- }
- `);
- });
- test(`doesn't handle saved object types outside of ENFORCED_TYPES`, async () => {
- const savedObjects = [
- {
+ const result = await validateReferences(params);
+ expect(result).toEqual([
+ expect.objectContaining({
+ type: 'visualization',
id: '2',
+ error: {
+ type: 'missing_references',
+ references: [{ type: 'index-pattern', id: '3' }],
+ },
+ }),
+ expect.objectContaining({
type: 'visualization',
- attributes: {},
- references: [
- {
- name: 'ref_0',
- type: 'foo',
- id: '1',
- },
- ],
- },
- ];
- const result = await getNonExistingReferenceAsKeys(savedObjects, savedObjectsClient);
- expect(result).toEqual([]);
- expect(savedObjectsClient.bulkGet).toHaveBeenCalledTimes(0);
+ id: '4',
+ error: {
+ type: 'missing_references',
+ references: [
+ { type: 'index-pattern', id: '5' },
+ { type: 'index-pattern', id: '6' },
+ { type: 'search', id: '7' },
+ ],
+ },
+ }),
+ ]);
+ expect(params.savedObjectsClient.bulkGet).toHaveBeenCalledTimes(1);
+ expect(params.savedObjectsClient.bulkGet).toHaveBeenCalledWith(
+ [
+ { type: 'index-pattern', id: '3', fields: ['id'] },
+ { type: 'index-pattern', id: '5', fields: ['id'] },
+ { type: 'index-pattern', id: '6', fields: ['id'] },
+ { type: 'search', id: '7', fields: ['id'] },
+ { type: 'search', id: '8', fields: ['id'] },
+ ],
+ { namespace: undefined }
+ );
});
- test('returns references within ENFORCED_TYPES when they are missing', async () => {
- const savedObjects = [
- {
- id: '2',
- type: 'visualization',
- attributes: {},
- references: [
- {
- name: 'ref_0',
- type: 'index-pattern',
- id: '1',
- },
- {
- name: 'ref_1',
- type: 'search',
- id: '3',
- },
- {
- name: 'ref_2',
- type: 'foo',
- id: '4',
- },
- ],
- },
- ];
- savedObjectsClient.bulkGet.mockResolvedValueOnce({
- saved_objects: [
+ test(`skips checking references when ignoreMissingReferences is included in retry`, async () => {
+ const params = setup({
+ objects: [
{
- id: '1',
- type: 'index-pattern',
- error: SavedObjectsErrorHelpers.createGenericNotFoundError('index-pattern', '1').output
- .payload,
+ id: '2',
+ type: 'visualization',
attributes: {},
- references: [],
+ references: [{ name: 'ref_0', type: 'index-pattern', id: '1' }],
},
+ ],
+ retries: [
{
- id: '3',
- type: 'search',
- error: SavedObjectsErrorHelpers.createGenericNotFoundError('search', '3').output.payload,
- attributes: {},
- references: [],
+ type: 'visualization',
+ id: '2',
+ overwrite: false,
+ replaceReferences: [],
+ ignoreMissingReferences: true,
},
],
});
- const result = await getNonExistingReferenceAsKeys(savedObjects, savedObjectsClient);
- expect(result).toEqual(['index-pattern:1', 'search:3']);
- expect(savedObjectsClient.bulkGet).toMatchInlineSnapshot(`
- [MockFunction] {
- "calls": Array [
- Array [
- Array [
- Object {
- "fields": Array [
- "id",
- ],
- "id": "1",
- "type": "index-pattern",
- },
- Object {
- "fields": Array [
- "id",
- ],
- "id": "3",
- "type": "search",
- },
- ],
- Object {
- "namespace": undefined,
- },
- ],
- ],
- "results": Array [
- Object {
- "type": "return",
- "value": Promise {},
- },
- ],
- }
- `);
- });
-});
-
-describe('validateReferences()', () => {
- const savedObjectsClient = savedObjectsClientMock.create();
-
- beforeEach(() => {
- jest.resetAllMocks();
- });
- test('returns empty when no objects are passed in', async () => {
- const result = await validateReferences([], savedObjectsClient);
+ const result = await validateReferences(params);
expect(result).toEqual([]);
- expect(savedObjectsClient.bulkGet).toHaveBeenCalledTimes(0);
+ expect(params.savedObjectsClient.bulkGet).not.toHaveBeenCalled();
});
- test('returns errors when references are missing', async () => {
- savedObjectsClient.bulkGet.mockResolvedValue({
- saved_objects: [
+ test(`doesn't return errors when references exist in Elasticsearch`, async () => {
+ const params = setup({
+ objects: [
{
- type: 'index-pattern',
- id: '3',
- error: SavedObjectsErrorHelpers.createGenericNotFoundError('index-pattern', '3').output
- .payload,
+ id: '2',
+ type: 'visualization',
attributes: {},
- references: [],
- },
- {
- type: 'index-pattern',
- id: '5',
- error: SavedObjectsErrorHelpers.createGenericNotFoundError('index-pattern', '5').output
- .payload,
- attributes: {},
- references: [],
+ references: [{ name: 'ref_0', type: 'index-pattern', id: '1' }],
},
+ ],
+ });
+ params.savedObjectsClient.bulkGet.mockResolvedValue({
+ saved_objects: [{ id: '1', type: 'index-pattern', attributes: {}, references: [] }],
+ });
+
+ const result = await validateReferences(params);
+ expect(result).toEqual([]);
+ expect(params.savedObjectsClient.bulkGet).toHaveBeenCalledTimes(1);
+ expect(params.savedObjectsClient.bulkGet).toHaveBeenCalledWith(
+ [{ type: 'index-pattern', id: '1', fields: ['id'] }],
+ { namespace: undefined }
+ );
+ });
+
+ test(`skips checking references that exist within the saved objects`, async () => {
+ const params = setup({
+ objects: [
{
+ id: '1',
type: 'index-pattern',
- id: '6',
- error: SavedObjectsErrorHelpers.createGenericNotFoundError('index-pattern', '6').output
- .payload,
- attributes: {},
- references: [],
- },
- {
- type: 'search',
- id: '7',
- error: SavedObjectsErrorHelpers.createGenericNotFoundError('search', '7').output.payload,
attributes: {},
references: [],
},
{
- id: '8',
- type: 'search',
+ id: '2',
+ type: 'visualization',
attributes: {},
- references: [],
+ references: [{ name: 'ref_0', type: 'index-pattern', id: '1' }],
},
],
});
- const savedObjects = [
- {
- id: '1',
- type: 'visualization',
- attributes: {},
- references: [],
- },
- {
- id: '2',
- type: 'visualization',
- attributes: {
- title: 'My Visualization 2',
- },
- references: [
- {
- name: 'ref_0',
- type: 'index-pattern',
- id: '3',
- },
- ],
- },
- {
- id: '4',
- type: 'visualization',
- attributes: {
- title: 'My Visualization 4',
- },
- references: [
- {
- name: 'ref_0',
- type: 'index-pattern',
- id: '5',
- },
- {
- name: 'ref_1',
- type: 'index-pattern',
- id: '6',
- },
- {
- name: 'ref_2',
- type: 'search',
- id: '7',
- },
- {
- name: 'ref_3',
- type: 'search',
- id: '8',
- },
- ],
- },
- ];
- const result = await validateReferences(savedObjects, savedObjectsClient);
- expect(result).toMatchInlineSnapshot(`
- Array [
- Object {
- "error": Object {
- "references": Array [
- Object {
- "id": "3",
- "type": "index-pattern",
- },
- ],
- "type": "missing_references",
- },
- "id": "2",
- "meta": Object {
- "title": "My Visualization 2",
- },
- "title": "My Visualization 2",
- "type": "visualization",
- },
- Object {
- "error": Object {
- "references": Array [
- Object {
- "id": "5",
- "type": "index-pattern",
- },
- Object {
- "id": "6",
- "type": "index-pattern",
- },
- Object {
- "id": "7",
- "type": "search",
- },
- ],
- "type": "missing_references",
- },
- "id": "4",
- "meta": Object {
- "title": "My Visualization 4",
- },
- "title": "My Visualization 4",
- "type": "visualization",
- },
- ]
- `);
- expect(savedObjectsClient.bulkGet).toMatchInlineSnapshot(`
- [MockFunction] {
- "calls": Array [
- Array [
- Array [
- Object {
- "fields": Array [
- "id",
- ],
- "id": "3",
- "type": "index-pattern",
- },
- Object {
- "fields": Array [
- "id",
- ],
- "id": "5",
- "type": "index-pattern",
- },
- Object {
- "fields": Array [
- "id",
- ],
- "id": "6",
- "type": "index-pattern",
- },
- Object {
- "fields": Array [
- "id",
- ],
- "id": "7",
- "type": "search",
- },
- Object {
- "fields": Array [
- "id",
- ],
- "id": "8",
- "type": "search",
- },
- ],
- Object {
- "namespace": undefined,
- },
- ],
- ],
- "results": Array [
- Object {
- "type": "return",
- "value": Promise {},
- },
- ],
- }
- `);
- });
- test(`doesn't return errors when ignoreMissingReferences is included in retry`, async () => {
- const savedObjects = [
- {
- id: '2',
- type: 'visualization',
- attributes: {},
- references: [{ name: 'ref_0', type: 'index-pattern', id: '1' }],
- },
- ];
- const retries = [
- {
- type: 'visualization',
- id: '2',
- overwrite: false,
- replaceReferences: [],
- ignoreMissingReferences: true,
- },
- ];
- const result = await validateReferences(savedObjects, savedObjectsClient, undefined, retries);
+ const result = await validateReferences(params);
+ expect(result).toEqual([]);
expect(result).toEqual([]);
- expect(savedObjectsClient.bulkGet).not.toHaveBeenCalled();
+ expect(params.savedObjectsClient.bulkGet).not.toHaveBeenCalled();
});
- test(`doesn't return errors when references exist in Elasticsearch`, async () => {
- savedObjectsClient.bulkGet.mockResolvedValue({
- saved_objects: [
+ test(`skips checking references that are not part of ENFORCED_TYPES`, async () => {
+ // this test case intentionally includes a mix of references that *will* be checked, and references that *won't* be checked
+ const params = setup({
+ objects: [
{
- id: '1',
- type: 'index-pattern',
+ id: '2',
+ type: 'visualization',
attributes: {},
- references: [],
+ references: [
+ { name: 'ref_0', type: 'index-pattern', id: '1' },
+ { name: 'ref_2', type: 'foo', id: '2' },
+ { name: 'ref_1', type: 'search', id: '3' },
+ ],
},
],
});
- const savedObjects = [
- {
- id: '2',
- type: 'visualization',
- attributes: {},
- references: [
- {
- name: 'ref_0',
- type: 'index-pattern',
- id: '1',
- },
- ],
- },
- ];
- const result = await validateReferences(savedObjects, savedObjectsClient);
- expect(result).toEqual([]);
- expect(savedObjectsClient.bulkGet).toHaveBeenCalledTimes(1);
- });
+ params.savedObjectsClient.bulkGet.mockResolvedValueOnce({
+ saved_objects: [
+ { type: 'index-pattern', id: '1', attributes: {}, references: [] },
+ { type: 'search', id: '3', attributes: {}, references: [] },
+ ],
+ });
- test(`doesn't return errors when references exist within the saved objects`, async () => {
- const savedObjects = [
- {
- id: '1',
- type: 'index-pattern',
- attributes: {},
- references: [],
- },
- {
- id: '2',
- type: 'visualization',
- attributes: {},
- references: [
- {
- name: 'ref_0',
- type: 'index-pattern',
- id: '1',
- },
- ],
- },
- ];
- const result = await validateReferences(savedObjects, savedObjectsClient);
+ const result = await validateReferences(params);
expect(result).toEqual([]);
- expect(savedObjectsClient.bulkGet).toHaveBeenCalledTimes(0);
+ expect(params.savedObjectsClient.bulkGet).toHaveBeenCalledTimes(1);
+ expect(params.savedObjectsClient.bulkGet).toHaveBeenCalledWith(
+ [
+ { type: 'index-pattern', id: '1', fields: ['id'] },
+ // foo:2 is not included in the cluster call
+ { type: 'search', id: '3', fields: ['id'] },
+ ],
+ { namespace: undefined }
+ );
});
- test(`doesn't validate references on types not part of ENFORCED_TYPES`, async () => {
- const savedObjects = [
- {
- id: '1',
- type: 'dashboard',
- attributes: {},
- references: [
- {
- name: 'ref_0',
- type: 'visualization',
- id: '2',
- },
- {
- name: 'ref_1',
- type: 'other-type',
- id: '3',
- },
- ],
- },
- ];
- const result = await validateReferences(savedObjects, savedObjectsClient);
+ test('skips checking references when an importStateMap entry indicates that we have already found an origin match with a different ID', async () => {
+ const params = setup({
+ objects: [
+ {
+ id: '2',
+ type: 'visualization',
+ attributes: {},
+ references: [{ name: 'ref_0', type: 'index-pattern', id: '1' }],
+ },
+ ],
+ importStateMap: new Map([
+ [`index-pattern:1`, { isOnlyReference: true, destinationId: 'not-1' }],
+ ]),
+ });
+
+ const result = await validateReferences(params);
expect(result).toEqual([]);
- expect(savedObjectsClient.bulkGet).toHaveBeenCalledTimes(0);
+ expect(params.savedObjectsClient.bulkGet).not.toHaveBeenCalled();
});
- test('throws when bulkGet fails', async () => {
- savedObjectsClient.bulkGet.mockResolvedValue({
+ test('throws when bulkGet encounters an unexpected error', async () => {
+ const params = setup({
+ objects: [
+ {
+ id: '2',
+ type: 'visualization',
+ attributes: {},
+ references: [{ name: 'ref_0', type: 'index-pattern', id: '1' }],
+ },
+ ],
+ });
+ params.savedObjectsClient.bulkGet.mockResolvedValue({
saved_objects: [
{
id: '1',
@@ -559,24 +261,9 @@ describe('validateReferences()', () => {
},
],
});
- const savedObjects = [
- {
- id: '2',
- type: 'visualization',
- attributes: {},
- references: [
- {
- name: 'ref_0',
- type: 'index-pattern',
- id: '1',
- },
- ],
- },
- ];
- await expect(
- validateReferences(savedObjects, savedObjectsClient)
- ).rejects.toThrowErrorMatchingInlineSnapshot(
- `"Error fetching references for imported objects"`
+
+ await expect(() => validateReferences(params)).rejects.toThrowError(
+ 'Error fetching references for imported objects'
);
});
});
diff --git a/src/core/server/saved_objects/import/lib/validate_references.ts b/src/core/server/saved_objects/import/lib/validate_references.ts
index e4c29a5951c27c..69e036cf77a3ad 100644
--- a/src/core/server/saved_objects/import/lib/validate_references.ts
+++ b/src/core/server/saved_objects/import/lib/validate_references.ts
@@ -9,6 +9,7 @@
import { SavedObject, SavedObjectsClientContract } from '../../types';
import { SavedObjectsImportFailure, SavedObjectsImportRetry } from '../types';
import { SavedObjectsImportError } from '../errors';
+import type { ImportStateMap } from './types';
const REF_TYPES_TO_VALIDATE = ['index-pattern', 'search'];
@@ -22,29 +23,44 @@ const getObjectsToSkip = (retries: SavedObjectsImportRetry[] = []) =>
new Set()
);
-export async function getNonExistingReferenceAsKeys(
- savedObjects: SavedObject[],
- savedObjectsClient: SavedObjectsClientContract,
- namespace?: string,
- retries?: SavedObjectsImportRetry[]
-) {
+export interface ValidateReferencesParams {
+ objects: Array>;
+ savedObjectsClient: SavedObjectsClientContract;
+ namespace: string | undefined;
+ importStateMap: ImportStateMap;
+ retries?: SavedObjectsImportRetry[];
+}
+
+async function getNonExistingReferenceAsKeys({
+ objects,
+ savedObjectsClient,
+ namespace,
+ importStateMap,
+ retries,
+}: ValidateReferencesParams) {
const objectsToSkip = getObjectsToSkip(retries);
const collector = new Map();
// Collect all references within objects
- for (const savedObject of savedObjects) {
- if (objectsToSkip.has(`${savedObject.type}:${savedObject.id}`)) {
- // skip objects with retries that have specified `ignoreMissingReferences`
+ for (const object of objects) {
+ if (objectsToSkip.has(`${object.type}:${object.id}`)) {
+ // skip objects with retries that have specified `ignoreMissingReferences`, or that share an origin with an existing object that has a different ID
continue;
}
- const filteredReferences = (savedObject.references || []).filter(filterReferencesToValidate);
+ const filteredReferences = (object.references || []).filter(filterReferencesToValidate);
for (const { type, id } of filteredReferences) {
+ const key = `${type}:${id}`;
+ const { isOnlyReference, destinationId } = importStateMap.get(key) ?? {};
+ if (isOnlyReference && destinationId) {
+ // We previously searched for this reference and found one with a matching origin, skip validating this
+ continue;
+ }
collector.set(`${type}:${id}`, { type, id });
}
}
// Remove objects that could be references
- for (const savedObject of savedObjects) {
- collector.delete(`${savedObject.type}:${savedObject.id}`);
+ for (const object of objects) {
+ collector.delete(`${object.type}:${object.id}`);
}
if (collector.size === 0) {
return [];
@@ -73,23 +89,14 @@ export async function getNonExistingReferenceAsKeys(
return [...collector.keys()];
}
-export async function validateReferences(
- savedObjects: Array>,
- savedObjectsClient: SavedObjectsClientContract,
- namespace?: string,
- retries?: SavedObjectsImportRetry[]
-) {
+export async function validateReferences(params: ValidateReferencesParams) {
+ const { objects, retries } = params;
const objectsToSkip = getObjectsToSkip(retries);
const errorMap: { [key: string]: SavedObjectsImportFailure } = {};
- const nonExistingReferenceKeys = await getNonExistingReferenceAsKeys(
- savedObjects,
- savedObjectsClient,
- namespace,
- retries
- );
+ const nonExistingReferenceKeys = await getNonExistingReferenceAsKeys(params);
// Filter out objects with missing references, add to error object
- savedObjects.forEach(({ type, id, references, attributes }) => {
+ objects.forEach(({ type, id, references, attributes }) => {
if (objectsToSkip.has(`${type}:${id}`)) {
// skip objects with retries that have specified `ignoreMissingReferences`
return;
diff --git a/src/core/server/saved_objects/import/resolve_import_errors.test.mock.ts b/src/core/server/saved_objects/import/resolve_import_errors.test.mock.ts
new file mode 100644
index 00000000000000..3cf4de850f4df7
--- /dev/null
+++ b/src/core/server/saved_objects/import/resolve_import_errors.test.mock.ts
@@ -0,0 +1,78 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import type { checkReferenceOrigins } from './lib/check_reference_origins';
+import type { validateRetries } from './lib/validate_retries';
+import type { createObjectsFilter } from './lib/create_objects_filter';
+import type { collectSavedObjects } from './lib/collect_saved_objects';
+import type { regenerateIds } from './lib/regenerate_ids';
+import type { validateReferences } from './lib/validate_references';
+import type { checkConflicts } from './lib/check_conflicts';
+import type { getImportStateMapForRetries } from './lib/get_import_state_map_for_retries';
+import type { splitOverwrites } from './lib/split_overwrites';
+import type { createSavedObjects } from './lib/create_saved_objects';
+import type { executeImportHooks } from './lib/execute_import_hooks';
+
+export const mockCheckReferenceOrigins = jest.fn() as jest.MockedFunction<
+ typeof checkReferenceOrigins
+>;
+jest.mock('./lib/check_reference_origins', () => ({
+ checkReferenceOrigins: mockCheckReferenceOrigins,
+}));
+
+export const mockValidateRetries = jest.fn() as jest.MockedFunction;
+jest.mock('./lib/validate_retries', () => ({
+ validateRetries: mockValidateRetries,
+}));
+
+export const mockCreateObjectsFilter = jest.fn() as jest.MockedFunction;
+jest.mock('./lib/create_objects_filter', () => ({
+ createObjectsFilter: mockCreateObjectsFilter,
+}));
+
+export const mockCollectSavedObjects = jest.fn() as jest.MockedFunction;
+jest.mock('./lib/collect_saved_objects', () => ({
+ collectSavedObjects: mockCollectSavedObjects,
+}));
+
+export const mockRegenerateIds = jest.fn() as jest.MockedFunction;
+jest.mock('./lib/regenerate_ids', () => ({
+ regenerateIds: mockRegenerateIds,
+}));
+
+export const mockValidateReferences = jest.fn() as jest.MockedFunction;
+jest.mock('./lib/validate_references', () => ({
+ validateReferences: mockValidateReferences,
+}));
+
+export const mockCheckConflicts = jest.fn() as jest.MockedFunction;
+jest.mock('./lib/check_conflicts', () => ({
+ checkConflicts: mockCheckConflicts,
+}));
+
+export const mockGetImportStateMapForRetries = jest.fn() as jest.MockedFunction<
+ typeof getImportStateMapForRetries
+>;
+jest.mock('./lib/get_import_state_map_for_retries', () => ({
+ getImportStateMapForRetries: mockGetImportStateMapForRetries,
+}));
+
+export const mockSplitOverwrites = jest.fn() as jest.MockedFunction;
+jest.mock('./lib/split_overwrites', () => ({
+ splitOverwrites: mockSplitOverwrites,
+}));
+
+export const mockCreateSavedObjects = jest.fn() as jest.MockedFunction;
+jest.mock('./lib/create_saved_objects', () => ({
+ createSavedObjects: mockCreateSavedObjects,
+}));
+
+export const mockExecuteImportHooks = jest.fn() as jest.MockedFunction;
+jest.mock('./lib/execute_import_hooks', () => ({
+ executeImportHooks: mockExecuteImportHooks,
+}));
diff --git a/src/core/server/saved_objects/import/resolve_import_errors.test.ts b/src/core/server/saved_objects/import/resolve_import_errors.test.ts
index d7d7544baafcb5..d950545de54f92 100644
--- a/src/core/server/saved_objects/import/resolve_import_errors.test.ts
+++ b/src/core/server/saved_objects/import/resolve_import_errors.test.ts
@@ -6,6 +6,20 @@
* Side Public License, v 1.
*/
+import {
+ mockCheckReferenceOrigins,
+ mockValidateRetries,
+ mockCreateObjectsFilter,
+ mockCollectSavedObjects,
+ mockRegenerateIds,
+ mockValidateReferences,
+ mockCheckConflicts,
+ mockGetImportStateMapForRetries,
+ mockSplitOverwrites,
+ mockCreateSavedObjects,
+ mockExecuteImportHooks,
+} from './resolve_import_errors.test.mock';
+
import { Readable } from 'stream';
import { v4 as uuidv4 } from 'uuid';
import {
@@ -25,58 +39,32 @@ import {
ResolveSavedObjectsImportErrorsOptions,
} from './resolve_import_errors';
-import {
- validateRetries,
- collectSavedObjects,
- regenerateIds,
- validateReferences,
- checkConflicts,
- getImportIdMapForRetries,
- splitOverwrites,
- createSavedObjects,
- createObjectsFilter,
- executeImportHooks,
-} from './lib';
-
-jest.mock('./lib/validate_retries');
-jest.mock('./lib/create_objects_filter');
-jest.mock('./lib/collect_saved_objects');
-jest.mock('./lib/regenerate_ids');
-jest.mock('./lib/validate_references');
-jest.mock('./lib/check_conflicts');
-jest.mock('./lib/check_origin_conflicts');
-jest.mock('./lib/split_overwrites');
-jest.mock('./lib/create_saved_objects');
-jest.mock('./lib/execute_import_hooks');
-
-const getMockFn = any, U>(fn: (...args: Parameters) => U) =>
- fn as jest.MockedFunction<(...args: Parameters) => U>;
-
describe('#importSavedObjectsFromStream', () => {
beforeEach(() => {
jest.clearAllMocks();
// mock empty output of each of these mocked modules so the import doesn't throw an error
- getMockFn(createObjectsFilter).mockReturnValue(() => false);
- getMockFn(collectSavedObjects).mockResolvedValue({
+ mockCreateObjectsFilter.mockReturnValue(() => false);
+ mockCollectSavedObjects.mockResolvedValue({
errors: [],
collectedObjects: [],
- importIdMap: new Map(),
+ importStateMap: new Map(),
});
- getMockFn(regenerateIds).mockReturnValue(new Map());
- getMockFn(validateReferences).mockResolvedValue([]);
- getMockFn(checkConflicts).mockResolvedValue({
+ mockCheckReferenceOrigins.mockResolvedValue({ importStateMap: new Map() });
+ mockRegenerateIds.mockReturnValue(new Map());
+ mockValidateReferences.mockResolvedValue([]);
+ mockCheckConflicts.mockResolvedValue({
errors: [],
filteredObjects: [],
- importIdMap: new Map(),
+ importStateMap: new Map(),
pendingOverwrites: new Set(), // not used by resolveImportErrors, but is a required return type
});
- getMockFn(getImportIdMapForRetries).mockReturnValue(new Map());
- getMockFn(splitOverwrites).mockReturnValue({
+ mockGetImportStateMapForRetries.mockReturnValue(new Map());
+ mockSplitOverwrites.mockReturnValue({
objectsToOverwrite: [],
objectsToNotOverwrite: [],
});
- getMockFn(createSavedObjects).mockResolvedValue({ errors: [], createdObjects: [] });
- getMockFn(executeImportHooks).mockResolvedValue([]);
+ mockCreateSavedObjects.mockResolvedValue({ errors: [], createdObjects: [] });
+ mockExecuteImportHooks.mockResolvedValue([]);
});
let readStream: Readable;
@@ -153,7 +141,7 @@ describe('#importSavedObjectsFromStream', () => {
/**
* These tests use minimal mocks which don't look realistic, but are sufficient to exercise the code paths correctly. For example, for an
* object to be imported successfully it would need to be obtained from `collectSavedObjects`, passed to `validateReferences`, passed to
- * `getImportIdMapForRetries`, passed to `createSavedObjects`, and returned from that. However, for each of the tests below, we skip the
+ * `getImportStateMapForRetries`, passed to `createSavedObjects`, and returned from that. However, for each of the tests below, we skip the
* intermediate steps in the interest of brevity.
*/
describe('module calls', () => {
@@ -162,7 +150,7 @@ describe('#importSavedObjectsFromStream', () => {
const options = setupOptions({ retries: [retry] });
await resolveSavedObjectsImportErrors(options);
- expect(validateRetries).toHaveBeenCalledWith([retry]);
+ expect(mockValidateRetries).toHaveBeenCalledWith([retry]);
});
test('creates objects filter', async () => {
@@ -170,7 +158,7 @@ describe('#importSavedObjectsFromStream', () => {
const options = setupOptions({ retries: [retry] });
await resolveSavedObjectsImportErrors(options);
- expect(createObjectsFilter).toHaveBeenCalledWith([retry]);
+ expect(mockCreateObjectsFilter).toHaveBeenCalledWith([retry]);
});
test('collects saved objects from stream', async () => {
@@ -182,28 +170,62 @@ describe('#importSavedObjectsFromStream', () => {
await resolveSavedObjectsImportErrors(options);
expect(typeRegistry.getImportableAndExportableTypes).toHaveBeenCalled();
- const filter = getMockFn(createObjectsFilter).mock.results[0].value;
- const collectSavedObjectsOptions = { readStream, objectLimit, filter, supportedTypes };
- expect(collectSavedObjects).toHaveBeenCalledWith(collectSavedObjectsOptions);
+ const filter = mockCreateObjectsFilter.mock.results[0].value;
+ const mockCollectSavedObjectsOptions = { readStream, objectLimit, filter, supportedTypes };
+ expect(mockCollectSavedObjects).toHaveBeenCalledWith(mockCollectSavedObjectsOptions);
});
- test('validates references', async () => {
+ test('checks reference origins', async () => {
const retries = [createRetry()];
const options = setupOptions({ retries });
const collectedObjects = [createObject()];
- getMockFn(collectSavedObjects).mockResolvedValue({
+ const importStateMap = new Map([
+ [`${collectedObjects[0].type}:${collectedObjects[0].id}`, {}],
+ [`foo:bar`, { isOnlyReference: true }],
+ ]);
+ mockCollectSavedObjects.mockResolvedValue({
errors: [],
collectedObjects,
- importIdMap: new Map(), // doesn't matter
+ importStateMap,
});
await resolveSavedObjectsImportErrors(options);
- expect(validateReferences).toHaveBeenCalledWith(
+ expect(mockCheckReferenceOrigins).toHaveBeenCalledWith({
+ savedObjectsClient,
+ typeRegistry,
+ namespace,
+ importStateMap,
+ });
+ });
+
+ test('validates references', async () => {
+ const retries = [createRetry()];
+ const options = setupOptions({ retries });
+ const collectedObjects = [createObject()];
+ mockCollectSavedObjects.mockResolvedValue({
+ errors: [],
collectedObjects,
+ importStateMap: new Map([
+ [`${collectedObjects[0].type}:${collectedObjects[0].id}`, {}],
+ [`foo:bar`, { isOnlyReference: true }],
+ ]),
+ });
+ mockCheckReferenceOrigins.mockResolvedValue({
+ importStateMap: new Map([[`foo:bar`, { isOnlyReference: true, id: 'baz' }]]),
+ });
+
+ await resolveSavedObjectsImportErrors(options);
+ expect(mockValidateReferences).toHaveBeenCalledWith({
+ objects: collectedObjects,
savedObjectsClient,
namespace,
- retries
- );
+ importStateMap: new Map([
+ // This importStateMap is a combination of the other two
+ [`${collectedObjects[0].type}:${collectedObjects[0].id}`, {}],
+ [`foo:bar`, { isOnlyReference: true, id: 'baz' }],
+ ]),
+ retries,
+ });
});
test('execute import hooks', async () => {
@@ -212,19 +234,19 @@ describe('#importSavedObjectsFromStream', () => {
};
const options = setupOptions({ importHooks });
const collectedObjects = [createObject()];
- getMockFn(collectSavedObjects).mockResolvedValue({
+ mockCollectSavedObjects.mockResolvedValue({
errors: [],
collectedObjects,
- importIdMap: new Map(),
+ importStateMap: new Map(),
});
- getMockFn(createSavedObjects).mockResolvedValueOnce({
+ mockCreateSavedObjects.mockResolvedValueOnce({
errors: [],
createdObjects: collectedObjects,
});
await resolveSavedObjectsImportErrors(options);
- expect(executeImportHooks).toHaveBeenCalledWith({
+ expect(mockExecuteImportHooks).toHaveBeenCalledWith({
objects: collectedObjects,
importHooks,
});
@@ -239,23 +261,25 @@ describe('#importSavedObjectsFromStream', () => {
}),
];
const options = setupOptions({ retries });
- getMockFn(collectSavedObjects).mockResolvedValue({
+ mockCollectSavedObjects.mockResolvedValue({
errors: [],
collectedObjects: [object],
- importIdMap: new Map(), // doesn't matter
+ importStateMap: new Map(), // doesn't matter
});
+ // mockCheckReferenceOrigins returns an empty importStateMap by default
await resolveSavedObjectsImportErrors(options);
const objectWithReplacedReferences = {
...object,
references: [{ ...object.references[0], id: 'def' }],
};
- expect(validateReferences).toHaveBeenCalledWith(
- [objectWithReplacedReferences],
+ expect(mockValidateReferences).toHaveBeenCalledWith({
+ objects: [objectWithReplacedReferences],
savedObjectsClient,
namespace,
- retries
- );
+ importStateMap: new Map(), // doesn't matter
+ retries,
+ });
});
test('checks conflicts', async () => {
@@ -263,10 +287,10 @@ describe('#importSavedObjectsFromStream', () => {
const retries = [createRetry()];
const options = setupOptions({ retries, createNewCopies });
const collectedObjects = [createObject()];
- getMockFn(collectSavedObjects).mockResolvedValue({
+ mockCollectSavedObjects.mockResolvedValue({
errors: [],
collectedObjects,
- importIdMap: new Map(), // doesn't matter
+ importStateMap: new Map(), // doesn't matter
});
await resolveSavedObjectsImportErrors(options);
@@ -277,7 +301,7 @@ describe('#importSavedObjectsFromStream', () => {
retries,
createNewCopies,
};
- expect(checkConflicts).toHaveBeenCalledWith(checkConflictsParams);
+ expect(mockCheckConflicts).toHaveBeenCalledWith(checkConflictsParams);
});
test('gets import ID map for retries', async () => {
@@ -285,76 +309,82 @@ describe('#importSavedObjectsFromStream', () => {
const createNewCopies = Symbol() as unknown as boolean;
const options = setupOptions({ retries, createNewCopies });
const filteredObjects = [createObject()];
- getMockFn(checkConflicts).mockResolvedValue({
+ mockCheckConflicts.mockResolvedValue({
errors: [],
filteredObjects,
- importIdMap: new Map(),
+ importStateMap: new Map(),
pendingOverwrites: new Set(), // not used by resolveImportErrors, but is a required return type
});
await resolveSavedObjectsImportErrors(options);
- const getImportIdMapForRetriesParams = { objects: filteredObjects, retries, createNewCopies };
- expect(getImportIdMapForRetries).toHaveBeenCalledWith(getImportIdMapForRetriesParams);
+ const getImportStateMapForRetriesParams = {
+ objects: filteredObjects,
+ retries,
+ createNewCopies,
+ };
+ expect(mockGetImportStateMapForRetries).toHaveBeenCalledWith(
+ getImportStateMapForRetriesParams
+ );
});
test('splits objects to overwrite from those not to overwrite', async () => {
const retries = [createRetry()];
const options = setupOptions({ retries });
const collectedObjects = [createObject()];
- getMockFn(collectSavedObjects).mockResolvedValue({
+ mockCollectSavedObjects.mockResolvedValue({
errors: [],
collectedObjects,
- importIdMap: new Map(), // doesn't matter
+ importStateMap: new Map(), // doesn't matter
});
await resolveSavedObjectsImportErrors(options);
- expect(splitOverwrites).toHaveBeenCalledWith(collectedObjects, retries);
+ expect(mockSplitOverwrites).toHaveBeenCalledWith(collectedObjects, retries);
});
describe('with createNewCopies disabled', () => {
test('does not regenerate object IDs', async () => {
const options = setupOptions();
const collectedObjects = [createObject()];
- getMockFn(collectSavedObjects).mockResolvedValue({
+ mockCollectSavedObjects.mockResolvedValue({
errors: [],
collectedObjects,
- importIdMap: new Map(), // doesn't matter
+ importStateMap: new Map(), // doesn't matter
});
await resolveSavedObjectsImportErrors(options);
- expect(regenerateIds).not.toHaveBeenCalled();
+ expect(mockRegenerateIds).not.toHaveBeenCalled();
});
test('creates saved objects', async () => {
const options = setupOptions();
const errors = [createError(), createError(), createError()];
- getMockFn(collectSavedObjects).mockResolvedValue({
+ mockCollectSavedObjects.mockResolvedValue({
errors: [errors[0]],
collectedObjects: [], // doesn't matter
- importIdMap: new Map(), // doesn't matter
+ importStateMap: new Map(), // doesn't matter
});
- getMockFn(validateReferences).mockResolvedValue([errors[1]]);
- getMockFn(checkConflicts).mockResolvedValue({
+ mockValidateReferences.mockResolvedValue([errors[1]]);
+ mockCheckConflicts.mockResolvedValue({
errors: [errors[2]],
filteredObjects: [],
- importIdMap: new Map([['foo', { id: 'someId' }]]),
+ importStateMap: new Map([['foo', { destinationId: 'someId' }]]),
pendingOverwrites: new Set(), // not used by resolveImportErrors, but is a required return type
});
- getMockFn(getImportIdMapForRetries).mockReturnValue(
+ mockGetImportStateMapForRetries.mockReturnValue(
new Map([
- ['foo', { id: 'newId' }],
- ['bar', { id: 'anotherNewId' }],
+ ['foo', { destinationId: 'newId' }],
+ ['bar', { destinationId: 'anotherNewId' }],
])
);
- const importIdMap = new Map([
- ['foo', { id: 'someId' }],
- ['bar', { id: 'anotherNewId' }],
+ const importStateMap = new Map([
+ ['foo', { destinationId: 'someId' }],
+ ['bar', { destinationId: 'anotherNewId' }],
]);
const objectsToOverwrite = [createObject()];
const objectsToNotOverwrite = [createObject()];
- getMockFn(splitOverwrites).mockReturnValue({ objectsToOverwrite, objectsToNotOverwrite });
- getMockFn(createSavedObjects).mockResolvedValueOnce({
- errors: [createError()], // this error will NOT be passed to the second `createSavedObjects` call
+ mockSplitOverwrites.mockReturnValue({ objectsToOverwrite, objectsToNotOverwrite });
+ mockCreateSavedObjects.mockResolvedValueOnce({
+ errors: [createError()], // this error will NOT be passed to the second `mockCreateSavedObjects` call
createdObjects: [],
});
@@ -362,15 +392,15 @@ describe('#importSavedObjectsFromStream', () => {
const partialCreateSavedObjectsParams = {
accumulatedErrors: errors,
savedObjectsClient,
- importIdMap,
+ importStateMap,
namespace,
};
- expect(createSavedObjects).toHaveBeenNthCalledWith(1, {
+ expect(mockCreateSavedObjects).toHaveBeenNthCalledWith(1, {
...partialCreateSavedObjectsParams,
objects: objectsToOverwrite,
overwrite: true,
});
- expect(createSavedObjects).toHaveBeenNthCalledWith(2, {
+ expect(mockCreateSavedObjects).toHaveBeenNthCalledWith(2, {
...partialCreateSavedObjectsParams,
objects: objectsToNotOverwrite,
});
@@ -381,54 +411,65 @@ describe('#importSavedObjectsFromStream', () => {
test('regenerates object IDs', async () => {
const options = setupOptions({ createNewCopies: true });
const collectedObjects = [createObject()];
- getMockFn(collectSavedObjects).mockResolvedValue({
+ mockCollectSavedObjects.mockResolvedValue({
errors: [],
collectedObjects,
- importIdMap: new Map(), // doesn't matter
+ importStateMap: new Map(), // doesn't matter
});
await resolveSavedObjectsImportErrors(options);
- expect(regenerateIds).toHaveBeenCalledWith(collectedObjects);
+ expect(mockRegenerateIds).toHaveBeenCalledWith(collectedObjects);
});
test('creates saved objects', async () => {
const options = setupOptions({ createNewCopies: true });
const errors = [createError(), createError(), createError()];
- getMockFn(collectSavedObjects).mockResolvedValue({
+ mockCollectSavedObjects.mockResolvedValue({
errors: [errors[0]],
collectedObjects: [], // doesn't matter
- importIdMap: new Map(), // doesn't matter
+ importStateMap: new Map([
+ ['foo', {}],
+ ['bar', {}],
+ ['baz', {}],
+ ['qux', { isOnlyReference: true }],
+ ]),
});
- getMockFn(validateReferences).mockResolvedValue([errors[1]]);
- getMockFn(regenerateIds).mockReturnValue(
+ mockCheckReferenceOrigins.mockResolvedValue({
+ importStateMap: new Map([['qux', { isOnlyReference: true, destinationId: 'newId1' }]]),
+ });
+ mockValidateReferences.mockResolvedValue([errors[1]]);
+ mockRegenerateIds.mockReturnValue(
new Map([
- ['foo', { id: 'randomId1' }],
- ['bar', { id: 'randomId2' }],
- ['baz', { id: 'randomId3' }],
+ ['foo', { destinationId: 'randomId1' }],
+ ['bar', { destinationId: 'randomId2' }],
+ ['baz', { destinationId: 'randomId3' }],
])
);
- getMockFn(checkConflicts).mockResolvedValue({
+ mockCheckConflicts.mockResolvedValue({
errors: [errors[2]],
filteredObjects: [],
- importIdMap: new Map([['bar', { id: 'someId' }]]),
+ importStateMap: new Map([['bar', { destinationId: 'someId' }]]),
pendingOverwrites: new Set(), // not used by resolveImportErrors, but is a required return type
});
- getMockFn(getImportIdMapForRetries).mockReturnValue(
+ mockGetImportStateMapForRetries.mockReturnValue(
new Map([
- ['bar', { id: 'newId' }],
- ['baz', { id: 'anotherNewId' }],
+ ['bar', { destinationId: 'newId2' }], // this is overridden by the checkConflicts result
+ ['baz', { destinationId: 'newId3' }],
])
);
- const importIdMap = new Map([
- ['foo', { id: 'randomId1' }],
- ['bar', { id: 'someId' }],
- ['baz', { id: 'anotherNewId' }],
+
+ // assert that the importStateMap is correctly composed of the results from the five modules
+ const importStateMap = new Map([
+ ['foo', { destinationId: 'randomId1' }],
+ ['bar', { destinationId: 'someId' }],
+ ['baz', { destinationId: 'newId3' }],
+ ['qux', { isOnlyReference: true, destinationId: 'newId1' }],
]);
const objectsToOverwrite = [createObject()];
const objectsToNotOverwrite = [createObject()];
- getMockFn(splitOverwrites).mockReturnValue({ objectsToOverwrite, objectsToNotOverwrite });
- getMockFn(createSavedObjects).mockResolvedValueOnce({
- errors: [createError()], // this error will NOT be passed to the second `createSavedObjects` call
+ mockSplitOverwrites.mockReturnValue({ objectsToOverwrite, objectsToNotOverwrite });
+ mockCreateSavedObjects.mockResolvedValueOnce({
+ errors: [createError()], // this error will NOT be passed to the second `mockCreateSavedObjects` call
createdObjects: [],
});
@@ -436,15 +477,15 @@ describe('#importSavedObjectsFromStream', () => {
const partialCreateSavedObjectsParams = {
accumulatedErrors: errors,
savedObjectsClient,
- importIdMap,
+ importStateMap,
namespace,
};
- expect(createSavedObjects).toHaveBeenNthCalledWith(1, {
+ expect(mockCreateSavedObjects).toHaveBeenNthCalledWith(1, {
...partialCreateSavedObjectsParams,
objects: objectsToOverwrite,
overwrite: true,
});
- expect(createSavedObjects).toHaveBeenNthCalledWith(2, {
+ expect(mockCreateSavedObjects).toHaveBeenNthCalledWith(2, {
...partialCreateSavedObjectsParams,
objects: objectsToNotOverwrite,
});
@@ -462,10 +503,10 @@ describe('#importSavedObjectsFromStream', () => {
test('returns success=false if an error occurred', async () => {
const options = setupOptions();
- getMockFn(collectSavedObjects).mockResolvedValue({
+ mockCollectSavedObjects.mockResolvedValue({
errors: [createError()],
collectedObjects: [],
- importIdMap: new Map(), // doesn't matter
+ importStateMap: new Map(), // doesn't matter
});
const result = await resolveSavedObjectsImportErrors(options);
@@ -480,17 +521,17 @@ describe('#importSavedObjectsFromStream', () => {
test('executes import hooks', async () => {
const options = setupOptions();
const collectedObjects = [createObject()];
- getMockFn(collectSavedObjects).mockResolvedValue({
+ mockCollectSavedObjects.mockResolvedValue({
errors: [],
collectedObjects,
- importIdMap: new Map(),
+ importStateMap: new Map(),
});
- getMockFn(createSavedObjects).mockResolvedValueOnce({
+ mockCreateSavedObjects.mockResolvedValueOnce({
errors: [],
createdObjects: collectedObjects,
});
const warnings: SavedObjectsImportWarning[] = [{ type: 'simple', message: 'foo' }];
- getMockFn(executeImportHooks).mockResolvedValue(warnings);
+ mockExecuteImportHooks.mockResolvedValue(warnings);
const result = await resolveSavedObjectsImportErrors(options);
@@ -507,11 +548,11 @@ describe('#importSavedObjectsFromStream', () => {
const tmp = createObject();
const obj2 = { ...tmp, destinationId: 'some-destinationId', originId: tmp.id };
const obj3 = { ...createObject(), destinationId: 'another-destinationId' }; // empty originId; this is a new copy
- getMockFn(createSavedObjects).mockResolvedValueOnce({
+ mockCreateSavedObjects.mockResolvedValueOnce({
errors: [error1],
createdObjects: [obj1],
});
- getMockFn(createSavedObjects).mockResolvedValueOnce({
+ mockCreateSavedObjects.mockResolvedValueOnce({
errors: [error2],
createdObjects: [obj2, obj3],
});
@@ -569,13 +610,13 @@ describe('#importSavedObjectsFromStream', () => {
},
});
- getMockFn(checkConflicts).mockResolvedValue({
+ mockCheckConflicts.mockResolvedValue({
errors: [],
filteredObjects: [],
- importIdMap: new Map(),
+ importStateMap: new Map(),
pendingOverwrites: new Set(),
});
- getMockFn(createSavedObjects)
+ mockCreateSavedObjects
.mockResolvedValueOnce({ errors: [], createdObjects: [obj1, obj2] })
.mockResolvedValueOnce({ errors: [], createdObjects: [] });
@@ -607,17 +648,17 @@ describe('#importSavedObjectsFromStream', () => {
test('accumulates multiple errors', async () => {
const options = setupOptions();
const errors = [createError(), createError(), createError(), createError()];
- getMockFn(collectSavedObjects).mockResolvedValue({
+ mockCollectSavedObjects.mockResolvedValue({
errors: [errors[0]],
collectedObjects: [],
- importIdMap: new Map(), // doesn't matter
+ importStateMap: new Map(), // doesn't matter
});
- getMockFn(validateReferences).mockResolvedValue([errors[1]]);
- getMockFn(createSavedObjects).mockResolvedValueOnce({
+ mockValidateReferences.mockResolvedValue([errors[1]]);
+ mockCreateSavedObjects.mockResolvedValueOnce({
errors: [errors[2]],
createdObjects: [],
});
- getMockFn(createSavedObjects).mockResolvedValueOnce({
+ mockCreateSavedObjects.mockResolvedValueOnce({
errors: [errors[3]],
createdObjects: [],
});
diff --git a/src/core/server/saved_objects/import/resolve_import_errors.ts b/src/core/server/saved_objects/import/resolve_import_errors.ts
index 25382965e845bd..61fbde5bb9d874 100644
--- a/src/core/server/saved_objects/import/resolve_import_errors.ts
+++ b/src/core/server/saved_objects/import/resolve_import_errors.ts
@@ -20,10 +20,11 @@ import {
createObjectsFilter,
splitOverwrites,
regenerateIds,
+ checkReferenceOrigins,
validateReferences,
validateRetries,
createSavedObjects,
- getImportIdMapForRetries,
+ getImportStateMapForRetries,
checkConflicts,
executeImportHooks,
} from './lib';
@@ -71,20 +72,20 @@ export async function resolveSavedObjectsImportErrors({
let successCount = 0;
let errorAccumulator: SavedObjectsImportFailure[] = [];
- let importIdMap: Map = new Map();
const supportedTypes = typeRegistry.getImportableAndExportableTypes().map((type) => type.name);
const filter = createObjectsFilter(retries);
// Get the objects to resolve errors
- const { errors: collectorErrors, collectedObjects: objectsToResolve } = await collectSavedObjects(
- {
- readStream,
- objectLimit,
- filter,
- supportedTypes,
- }
- );
- errorAccumulator = [...errorAccumulator, ...collectorErrors];
+ const collectSavedObjectsResult = await collectSavedObjects({
+ readStream,
+ objectLimit,
+ filter,
+ supportedTypes,
+ });
+ // Map of all IDs for objects that we are attempting to import, and any references that are not included in the read stream;
+ // each value is empty by default
+ let importStateMap = collectSavedObjectsResult.importStateMap;
+ errorAccumulator = [...errorAccumulator, ...collectSavedObjectsResult.errors];
// Create a map of references to replace for each object to avoid iterating through
// retries for every object to resolve
@@ -98,7 +99,7 @@ export async function resolveSavedObjectsImportErrors({
}
// Replace references
- for (const savedObject of objectsToResolve) {
+ for (const savedObject of collectSavedObjectsResult.collectedObjects) {
const refMap = retriesReferencesMap.get(`${savedObject.type}:${savedObject.id}`);
if (!refMap) {
continue;
@@ -106,28 +107,42 @@ export async function resolveSavedObjectsImportErrors({
for (const reference of savedObject.references || []) {
if (refMap[`${reference.type}:${reference.id}`]) {
reference.id = refMap[`${reference.type}:${reference.id}`];
+ // Any reference ID changed here will supersede the results of checkReferenceOrigins below; this is intentional.
}
}
}
+ // Check any references that aren't included in the import file and retries, to see if they have a match with a different origin
+ const checkReferenceOriginsResult = await checkReferenceOrigins({
+ savedObjectsClient,
+ typeRegistry,
+ namespace,
+ importStateMap,
+ });
+ importStateMap = new Map([...importStateMap, ...checkReferenceOriginsResult.importStateMap]);
+
// Validate references
- const validateReferencesResult = await validateReferences(
- objectsToResolve,
+ const validateReferencesResult = await validateReferences({
+ objects: collectSavedObjectsResult.collectedObjects,
savedObjectsClient,
namespace,
- retries
- );
+ importStateMap,
+ retries,
+ });
errorAccumulator = [...errorAccumulator, ...validateReferencesResult];
if (createNewCopies) {
// In case any missing reference errors were resolved, ensure that we regenerate those object IDs as well
// This is because a retry to resolve a missing reference error may not necessarily specify a destinationId
- importIdMap = regenerateIds(objectsToResolve);
+ importStateMap = new Map([
+ ...importStateMap, // preserve any entries for references that aren't included in collectedObjects
+ ...regenerateIds(collectSavedObjectsResult.collectedObjects),
+ ]);
}
// Check single-namespace objects for conflicts in this namespace, and check multi-namespace objects for conflicts across all namespaces
const checkConflictsParams = {
- objects: objectsToResolve,
+ objects: collectSavedObjectsResult.collectedObjects,
savedObjectsClient,
namespace,
retries,
@@ -137,16 +152,16 @@ export async function resolveSavedObjectsImportErrors({
errorAccumulator = [...errorAccumulator, ...checkConflictsResult.errors];
// Check multi-namespace object types for regular conflicts and ambiguous conflicts
- const getImportIdMapForRetriesParams = {
+ const getImportStateMapForRetriesParams = {
objects: checkConflictsResult.filteredObjects,
retries,
createNewCopies,
};
- const importIdMapForRetries = getImportIdMapForRetries(getImportIdMapForRetriesParams);
- importIdMap = new Map([
- ...importIdMap,
- ...importIdMapForRetries,
- ...checkConflictsResult.importIdMap, // this importIdMap takes precedence over the others
+ const importStateMapForRetries = getImportStateMapForRetries(getImportStateMapForRetriesParams);
+ importStateMap = new Map([
+ ...importStateMap,
+ ...importStateMapForRetries,
+ ...checkConflictsResult.importStateMap, // this importStateMap takes precedence over the others
]);
// Bulk create in two batches, overwrites and non-overwrites
@@ -161,7 +176,7 @@ export async function resolveSavedObjectsImportErrors({
objects,
accumulatedErrors,
savedObjectsClient,
- importIdMap,
+ importStateMap,
namespace,
overwrite,
};
@@ -191,7 +206,10 @@ export async function resolveSavedObjectsImportErrors({
}),
];
};
- const { objectsToOverwrite, objectsToNotOverwrite } = splitOverwrites(objectsToResolve, retries);
+ const { objectsToOverwrite, objectsToNotOverwrite } = splitOverwrites(
+ collectSavedObjectsResult.collectedObjects,
+ retries
+ );
await bulkCreateObjects(objectsToOverwrite, true);
await bulkCreateObjects(objectsToNotOverwrite);
diff --git a/src/core/server/saved_objects/routes/integration_tests/import.test.ts b/src/core/server/saved_objects/routes/integration_tests/import.test.ts
index 64c79b34243767..d5f994a3e01eaf 100644
--- a/src/core/server/saved_objects/routes/integration_tests/import.test.ts
+++ b/src/core/server/saved_objects/routes/integration_tests/import.test.ts
@@ -6,7 +6,8 @@
* Side Public License, v 1.
*/
-import { mockUuidv4 } from '../../import/lib/__mocks__';
+jest.mock('uuid');
+
import supertest from 'supertest';
import { registerImportRoute } from '../import';
import { savedObjectsClientMock } from '../../../../../core/server/mocks';
@@ -19,7 +20,6 @@ import { SavedObjectsErrorHelpers, SavedObjectsImporter } from '../..';
type SetupServerReturn = Awaited>;
-const { v4: uuidv4 } = jest.requireActual('uuid');
const allowedTypes = ['index-pattern', 'visualization', 'dashboard'];
const config = { maxImportPayloadBytes: 26214400, maxImportExportSize: 10000 } as SavedObjectConfig;
let coreUsageStatsClient: jest.Mocked;
@@ -46,8 +46,6 @@ describe(`POST ${URL}`, () => {
};
beforeEach(async () => {
- mockUuidv4.mockReset();
- mockUuidv4.mockImplementation(() => uuidv4());
({ server, httpSetup, handlerContext } = await setupServer());
handlerContext.savedObjects.typeRegistry.getImportableAndExportableTypes.mockReturnValue(
allowedTypes.map(createExportableType)
@@ -487,7 +485,9 @@ describe(`POST ${URL}`, () => {
describe('createNewCopies enabled', () => {
it('imports objects, regenerating all IDs/reference IDs present, and resetting all origin IDs', async () => {
- mockUuidv4
+ const mockUuid = jest.requireMock('uuid');
+ mockUuid.v4 = jest
+ .fn()
.mockReturnValueOnce('foo') // a uuid.v4() is generated for the request.id
.mockReturnValueOnce('foo') // another uuid.v4() is used for the request.uuid
.mockReturnValueOnce('new-id-1')
diff --git a/src/core/server/saved_objects/routes/integration_tests/resolve_import_errors.test.ts b/src/core/server/saved_objects/routes/integration_tests/resolve_import_errors.test.ts
index 99139d82821c5b..a7f1b1e304aa7a 100644
--- a/src/core/server/saved_objects/routes/integration_tests/resolve_import_errors.test.ts
+++ b/src/core/server/saved_objects/routes/integration_tests/resolve_import_errors.test.ts
@@ -6,7 +6,8 @@
* Side Public License, v 1.
*/
-import { mockUuidv4 } from '../../import/lib/__mocks__';
+jest.mock('uuid');
+
import supertest from 'supertest';
import { registerResolveImportErrorsRoute } from '../resolve_import_errors';
import { savedObjectsClientMock } from '../../../../../core/server/mocks';
@@ -19,7 +20,6 @@ import { SavedObjectsImporter } from '../..';
type SetupServerReturn = Awaited>;
-const { v4: uuidv4 } = jest.requireActual('uuid');
const allowedTypes = ['index-pattern', 'visualization', 'dashboard'];
const config = { maxImportPayloadBytes: 26214400, maxImportExportSize: 10000 } as SavedObjectConfig;
let coreUsageStatsClient: jest.Mocked;
@@ -51,8 +51,6 @@ describe(`POST ${URL}`, () => {
};
beforeEach(async () => {
- mockUuidv4.mockReset();
- mockUuidv4.mockImplementation(() => uuidv4());
({ server, httpSetup, handlerContext } = await setupServer());
handlerContext.savedObjects.typeRegistry.getImportableAndExportableTypes.mockReturnValue(
allowedTypes.map(createExportableType)
@@ -335,7 +333,8 @@ describe(`POST ${URL}`, () => {
describe('createNewCopies enabled', () => {
it('imports objects, regenerating all IDs/reference IDs present, and resetting all origin IDs', async () => {
- mockUuidv4.mockReturnValue('new-id-1');
+ const mockUuid = jest.requireMock('uuid');
+ mockUuid.v4 = jest.fn().mockReturnValue('new-id-1');
savedObjectsClient.bulkGet.mockResolvedValueOnce({ saved_objects: [mockIndexPattern] });
const obj1 = {
type: 'visualization',
diff --git a/src/dev/build/tasks/os_packages/create_os_package_tasks.ts b/src/dev/build/tasks/os_packages/create_os_package_tasks.ts
index b4724085c51848..88240429856d11 100644
--- a/src/dev/build/tasks/os_packages/create_os_package_tasks.ts
+++ b/src/dev/build/tasks/os_packages/create_os_package_tasks.ts
@@ -119,6 +119,7 @@ export const CreateDockerContexts: Task = {
async run(config, log, build) {
await runDockerGenerator(config, log, build, {
+ ubuntu: true,
context: true,
image: false,
dockerBuildDate,
diff --git a/src/plugins/dashboard/public/application/embeddable/dashboard_container.test.tsx b/src/plugins/dashboard/public/application/embeddable/dashboard_container.test.tsx
index d5eef0c05129d0..5f50cfd842b67a 100644
--- a/src/plugins/dashboard/public/application/embeddable/dashboard_container.test.tsx
+++ b/src/plugins/dashboard/public/application/embeddable/dashboard_container.test.tsx
@@ -41,6 +41,7 @@ import { uiActionsPluginMock } from '../../../../ui_actions/public/mocks';
import { getStubPluginServices } from '../../../../presentation_util/public';
const presentationUtil = getStubPluginServices();
+const theme = coreMock.createStart().theme;
const options: DashboardContainerServices = {
// TODO: clean up use of any
@@ -55,7 +56,7 @@ const options: DashboardContainerServices = {
uiActions: {} as any,
uiSettings: uiSettingsServiceMock.createStartContract(),
http: coreMock.createStart().http,
- theme: coreMock.createStart().theme,
+ theme,
presentationUtil,
};
@@ -251,6 +252,7 @@ test('DashboardContainer in edit mode shows edit mode actions', async () => {
overlays={{} as any}
inspector={inspector}
SavedObjectFinder={() => null}
+ theme={theme}
/>
diff --git a/src/plugins/dashboard/public/application/listing/__snapshots__/dashboard_listing.test.tsx.snap b/src/plugins/dashboard/public/application/listing/__snapshots__/dashboard_listing.test.tsx.snap
index 2f383adb3f5c3b..598254ad2173fa 100644
--- a/src/plugins/dashboard/public/application/listing/__snapshots__/dashboard_listing.test.tsx.snap
+++ b/src/plugins/dashboard/public/application/listing/__snapshots__/dashboard_listing.test.tsx.snap
@@ -96,6 +96,14 @@ exports[`after fetch When given a title that matches multiple dashboards, filter
]
}
tableListTitle="Dashboards"
+ theme={
+ Object {
+ "theme$": Observable {
+ "_isScalar": false,
+ "_subscribe": [Function],
+ },
+ }
+ }
toastNotifications={
Object {
"add": [MockFunction],
@@ -208,6 +216,14 @@ exports[`after fetch initialFilter 1`] = `
]
}
tableListTitle="Dashboards"
+ theme={
+ Object {
+ "theme$": Observable {
+ "_isScalar": false,
+ "_subscribe": [Function],
+ },
+ }
+ }
toastNotifications={
Object {
"add": [MockFunction],
@@ -319,6 +335,14 @@ exports[`after fetch renders all table rows 1`] = `
]
}
tableListTitle="Dashboards"
+ theme={
+ Object {
+ "theme$": Observable {
+ "_isScalar": false,
+ "_subscribe": [Function],
+ },
+ }
+ }
toastNotifications={
Object {
"add": [MockFunction],
@@ -430,6 +454,14 @@ exports[`after fetch renders call to action when no dashboards exist 1`] = `
]
}
tableListTitle="Dashboards"
+ theme={
+ Object {
+ "theme$": Observable {
+ "_isScalar": false,
+ "_subscribe": [Function],
+ },
+ }
+ }
toastNotifications={
Object {
"add": [MockFunction],
@@ -552,6 +584,14 @@ exports[`after fetch renders call to action with continue when no dashboards exi
]
}
tableListTitle="Dashboards"
+ theme={
+ Object {
+ "theme$": Observable {
+ "_isScalar": false,
+ "_subscribe": [Function],
+ },
+ }
+ }
toastNotifications={
Object {
"add": [MockFunction],
@@ -663,6 +703,14 @@ exports[`after fetch renders warning when listingLimit is exceeded 1`] = `
]
}
tableListTitle="Dashboards"
+ theme={
+ Object {
+ "theme$": Observable {
+ "_isScalar": false,
+ "_subscribe": [Function],
+ },
+ }
+ }
toastNotifications={
Object {
"add": [MockFunction],
@@ -744,6 +792,14 @@ exports[`after fetch showWriteControls 1`] = `
]
}
tableListTitle="Dashboards"
+ theme={
+ Object {
+ "theme$": Observable {
+ "_isScalar": false,
+ "_subscribe": [Function],
+ },
+ }
+ }
toastNotifications={
Object {
"add": [MockFunction],
diff --git a/src/plugins/dashboard/public/application/listing/dashboard_listing.tsx b/src/plugins/dashboard/public/application/listing/dashboard_listing.tsx
index deb8671edb97d9..5b53fc47e06a41 100644
--- a/src/plugins/dashboard/public/application/listing/dashboard_listing.tsx
+++ b/src/plugins/dashboard/public/application/listing/dashboard_listing.tsx
@@ -297,6 +297,7 @@ export const DashboardListing = ({
listingLimit,
tableColumns,
}}
+ theme={core.theme}
>
{
return {
@@ -22,6 +23,8 @@ jest.mock('@kbn/i18n', () => {
};
});
+const theme = themeServiceMock.createStartContract();
+
describe('handleResponse', () => {
const notifications = notificationServiceMock.createStartContract();
@@ -37,7 +40,7 @@ describe('handleResponse', () => {
timed_out: true,
},
} as IKibanaSearchResponse;
- const result = handleResponse(request, response);
+ const result = handleResponse(request, response, theme);
expect(result).toBe(response);
expect(notifications.toasts.addWarning).toBeCalled();
expect((notifications.toasts.addWarning as jest.Mock).mock.calls[0][0].title).toMatch(
@@ -57,7 +60,7 @@ describe('handleResponse', () => {
},
},
} as IKibanaSearchResponse;
- const result = handleResponse(request, response);
+ const result = handleResponse(request, response, theme);
expect(result).toBe(response);
expect(notifications.toasts.addWarning).toBeCalled();
expect((notifications.toasts.addWarning as jest.Mock).mock.calls[0][0].title).toMatch(
@@ -70,7 +73,7 @@ describe('handleResponse', () => {
const response = {
rawResponse: {},
} as IKibanaSearchResponse;
- const result = handleResponse(request, response);
+ const result = handleResponse(request, response, theme);
expect(result).toBe(response);
});
});
diff --git a/src/plugins/data/public/search/fetch/handle_response.tsx b/src/plugins/data/public/search/fetch/handle_response.tsx
index 10b2f69a2a3202..618efcb702ec45 100644
--- a/src/plugins/data/public/search/fetch/handle_response.tsx
+++ b/src/plugins/data/public/search/fetch/handle_response.tsx
@@ -11,11 +11,16 @@ import { i18n } from '@kbn/i18n';
import { EuiSpacer } from '@elastic/eui';
import { IKibanaSearchResponse } from 'src/plugins/data/common';
import { ShardFailureOpenModalButton } from '../../ui/shard_failure_modal';
+import { ThemeServiceStart } from '../../../../../core/public';
import { toMountPoint } from '../../../../kibana_react/public';
import { getNotifications } from '../../services';
import type { SearchRequest } from '..';
-export function handleResponse(request: SearchRequest, response: IKibanaSearchResponse) {
+export function handleResponse(
+ request: SearchRequest,
+ response: IKibanaSearchResponse,
+ theme: ThemeServiceStart
+) {
const { rawResponse } = response;
if (rawResponse.timed_out) {
@@ -45,8 +50,14 @@ export function handleResponse(request: SearchRequest, response: IKibanaSearchRe
<>
{description}
-
- >
+
+ >,
+ { theme$: theme.theme$ }
);
getNotifications().toasts.addWarning({ title, text });
diff --git a/src/plugins/data/public/search/search_interceptor/search_interceptor.test.ts b/src/plugins/data/public/search/search_interceptor/search_interceptor.test.ts
index 142fa94c961622..968dd870489fe0 100644
--- a/src/plugins/data/public/search/search_interceptor/search_interceptor.test.ts
+++ b/src/plugins/data/public/search/search_interceptor/search_interceptor.test.ts
@@ -8,7 +8,7 @@
import type { MockedKeys } from '@kbn/utility-types/jest';
import { CoreSetup, CoreStart } from '../../../../../core/public';
-import { coreMock } from '../../../../../core/public/mocks';
+import { coreMock, themeServiceMock } from '../../../../../core/public/mocks';
import { IEsSearchRequest } from '../../../common/search';
import { SearchInterceptor } from './search_interceptor';
import { AbortError } from '../../../../kibana_utils/public';
@@ -120,6 +120,7 @@ describe('SearchInterceptor', () => {
uiSettings: mockCoreSetup.uiSettings,
http: mockCoreSetup.http,
session: sessionService,
+ theme: themeServiceMock.createSetupContract(),
});
});
diff --git a/src/plugins/data/public/search/search_interceptor/search_interceptor.ts b/src/plugins/data/public/search/search_interceptor/search_interceptor.ts
index 9e968c9bae8a08..8c7bfe68fd54b7 100644
--- a/src/plugins/data/public/search/search_interceptor/search_interceptor.ts
+++ b/src/plugins/data/public/search/search_interceptor/search_interceptor.ts
@@ -21,7 +21,7 @@ import {
tap,
} from 'rxjs/operators';
import { PublicMethodsOf } from '@kbn/utility-types';
-import { CoreSetup, CoreStart, ToastsSetup } from 'kibana/public';
+import { CoreSetup, CoreStart, ThemeServiceSetup, ToastsSetup } from 'kibana/public';
import { i18n } from '@kbn/i18n';
import { BatchedFunc, BfetchPublicSetup } from 'src/plugins/bfetch/public';
import {
@@ -60,6 +60,7 @@ export interface SearchInterceptorDeps {
toasts: ToastsSetup;
usageCollector?: SearchUsageCollector;
session: ISessionService;
+ theme: ThemeServiceSetup;
}
const MAX_CACHE_ITEMS = 50;
@@ -377,7 +378,7 @@ export class SearchInterceptor {
private showTimeoutErrorToast = (e: SearchTimeoutError, sessionId?: string) => {
this.deps.toasts.addDanger({
title: 'Timed out',
- text: toMountPoint(e.getErrorMessage(this.application)),
+ text: toMountPoint(e.getErrorMessage(this.application), { theme$: this.deps.theme.theme$ }),
});
};
@@ -392,7 +393,9 @@ export class SearchInterceptor {
this.deps.toasts.addWarning(
{
title: 'Your search session is still running',
- text: toMountPoint(SearchSessionIncompleteWarning(this.docLinks)),
+ text: toMountPoint(SearchSessionIncompleteWarning(this.docLinks), {
+ theme$: this.deps.theme.theme$,
+ }),
},
{
toastLifeTimeMs: 60000,
@@ -423,14 +426,14 @@ export class SearchInterceptor {
title: i18n.translate('data.search.esErrorTitle', {
defaultMessage: 'Cannot retrieve search results',
}),
- text: toMountPoint(e.getErrorMessage(this.application)),
+ text: toMountPoint(e.getErrorMessage(this.application), { theme$: this.deps.theme.theme$ }),
});
} else if (e.constructor.name === 'HttpFetchError') {
this.deps.toasts.addDanger({
title: i18n.translate('data.search.httpErrorTitle', {
defaultMessage: 'Cannot retrieve your data',
}),
- text: toMountPoint(getHttpError(e.message)),
+ text: toMountPoint(getHttpError(e.message), { theme$: this.deps.theme.theme$ }),
});
} else {
this.deps.toasts.addError(e, {
diff --git a/src/plugins/data/public/search/search_service.ts b/src/plugins/data/public/search/search_service.ts
index 76aae8582287dd..311a863a749330 100644
--- a/src/plugins/data/public/search/search_service.ts
+++ b/src/plugins/data/public/search/search_service.ts
@@ -46,7 +46,7 @@ import {
esRawResponse,
} from '../../common/search';
import { AggsService, AggsStartDependencies } from './aggs';
-import { IndexPatternsContract } from '..';
+import { IKibanaSearchResponse, IndexPatternsContract, SearchRequest } from '..';
import { ISearchInterceptor, SearchInterceptor } from './search_interceptor';
import { SearchUsageCollector, createUsageCollector } from './collectors';
import { UsageCollectionSetup } from '../../../usage_collection/public';
@@ -88,7 +88,7 @@ export class SearchService implements Plugin {
constructor(private initializerContext: PluginInitializerContext) {}
public setup(
- { http, getStartServices, notifications, uiSettings }: CoreSetup,
+ { http, getStartServices, notifications, uiSettings, theme }: CoreSetup,
{ bfetch, expressions, usageCollection, nowProvider }: SearchServiceSetupDependencies
): ISearchSetup {
this.usageCollector = createUsageCollector(getStartServices, usageCollection);
@@ -112,6 +112,7 @@ export class SearchService implements Plugin {
startServices: getStartServices(),
usageCollector: this.usageCollector!,
session: this.sessionService,
+ theme,
});
expressions.registerFunction(
@@ -173,7 +174,7 @@ export class SearchService implements Plugin {
}
public start(
- { http, uiSettings }: CoreStart,
+ { http, theme, uiSettings }: CoreStart,
{ fieldFormats, indexPatterns }: SearchServiceStartDependencies
): ISearchStart {
const search = ((request, options = {}) => {
@@ -186,7 +187,8 @@ export class SearchService implements Plugin {
const searchSourceDependencies: SearchSourceDependencies = {
getConfig: uiSettings.get.bind(uiSettings),
search,
- onResponse: handleResponse,
+ onResponse: (request: SearchRequest, response: IKibanaSearchResponse) =>
+ handleResponse(request, response, theme),
};
return {
diff --git a/src/plugins/data/public/services.ts b/src/plugins/data/public/services.ts
index c1a0ae1ac1b536..5c52a1e695359c 100644
--- a/src/plugins/data/public/services.ts
+++ b/src/plugins/data/public/services.ts
@@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
-import { NotificationsStart, CoreStart } from 'src/core/public';
+import { NotificationsStart, CoreStart, ThemeServiceStart } from 'src/core/public';
import { createGetterSetter } from '../../kibana_utils/public';
import { IndexPatternsContract } from './data_views';
import { DataPublicPluginStart } from './types';
@@ -24,3 +24,5 @@ export const [getIndexPatterns, setIndexPatterns] =
export const [getSearchService, setSearchService] =
createGetterSetter('Search');
+
+export const [getTheme, setTheme] = createGetterSetter('Theme');
diff --git a/src/plugins/data/public/ui/query_string_input/query_bar_top_row.tsx b/src/plugins/data/public/ui/query_string_input/query_bar_top_row.tsx
index bb5e61bdb19467..e5da2bb9f089db 100644
--- a/src/plugins/data/public/ui/query_string_input/query_bar_top_row.tsx
+++ b/src/plugins/data/public/ui/query_string_input/query_bar_top_row.tsx
@@ -24,7 +24,6 @@ import {
EuiSuperUpdateButton,
OnRefreshProps,
} from '@elastic/eui';
-
import { IDataPluginServices, IIndexPattern, TimeRange, TimeHistoryContract, Query } from '../..';
import { useKibana, withKibana } from '../../../../kibana_react/public';
import QueryStringInputUI from './query_string_input';
diff --git a/src/plugins/data/public/ui/query_string_input/query_string_input.tsx b/src/plugins/data/public/ui/query_string_input/query_string_input.tsx
index a0b214d1be8c7f..6464f02dd7cb72 100644
--- a/src/plugins/data/public/ui/query_string_input/query_string_input.tsx
+++ b/src/plugins/data/public/ui/query_string_input/query_string_input.tsx
@@ -40,6 +40,7 @@ import type { SuggestionsListSize } from '../typeahead/suggestions_component';
import { SuggestionsComponent } from '..';
import { getFieldSubtypeNested, KIBANA_USER_QUERY_LANGUAGE_KEY } from '../../../common';
import { onRaf } from '../utils';
+import { getTheme } from '../../services';
export interface QueryStringInputProps {
indexPatterns: Array;
@@ -487,7 +488,8 @@ export default class QueryStringInputUI extends PureComponent {
-
+ ,
+ { theme$: getTheme().theme$ }
),
});
}
diff --git a/src/plugins/data/public/ui/shard_failure_modal/shard_failure_open_modal_button.test.tsx b/src/plugins/data/public/ui/shard_failure_modal/shard_failure_open_modal_button.test.tsx
index 28822cbd71ca77..b8289bc23cf016 100644
--- a/src/plugins/data/public/ui/shard_failure_modal/shard_failure_open_modal_button.test.tsx
+++ b/src/plugins/data/public/ui/shard_failure_modal/shard_failure_open_modal_button.test.tsx
@@ -8,18 +8,22 @@
import { openModal } from './shard_failure_open_modal_button.test.mocks';
import React from 'react';
+import { themeServiceMock } from 'src/core/public/mocks';
import { mountWithIntl } from '@kbn/test/jest';
import ShardFailureOpenModalButton from './shard_failure_open_modal_button';
import { shardFailureRequest } from './__mocks__/shard_failure_request';
import { shardFailureResponse } from './__mocks__/shard_failure_response';
import { findTestSubject } from '@elastic/eui/lib/test';
+const theme = themeServiceMock.createStartContract();
+
describe('ShardFailureOpenModalButton', () => {
it('triggers the openModal function when "Show details" button is clicked', () => {
const component = mountWithIntl(
);
diff --git a/src/plugins/data/public/ui/shard_failure_modal/shard_failure_open_modal_button.tsx b/src/plugins/data/public/ui/shard_failure_modal/shard_failure_open_modal_button.tsx
index 32ebd83aa47f09..585268824fb93e 100644
--- a/src/plugins/data/public/ui/shard_failure_modal/shard_failure_open_modal_button.tsx
+++ b/src/plugins/data/public/ui/shard_failure_modal/shard_failure_open_modal_button.tsx
@@ -12,6 +12,7 @@ import { EuiButton, EuiTextAlign } from '@elastic/eui';
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { getOverlays } from '../../services';
+import { ThemeServiceStart } from '../../../../../core/public';
import { toMountPoint } from '../../../../kibana_react/public';
import { ShardFailureModal } from './shard_failure_modal';
import { ShardFailureRequest } from './shard_failure_types';
@@ -20,6 +21,7 @@ import { ShardFailureRequest } from './shard_failure_types';
export interface ShardFailureOpenModalButtonProps {
request: ShardFailureRequest;
response: estypes.SearchResponse;
+ theme: ThemeServiceStart;
title: string;
}
@@ -28,6 +30,7 @@ export interface ShardFailureOpenModalButtonProps {
export default function ShardFailureOpenModalButton({
request,
response,
+ theme,
title,
}: ShardFailureOpenModalButtonProps) {
function onClick() {
@@ -38,7 +41,8 @@ export default function ShardFailureOpenModalButton({
response={response}
title={title}
onClose={() => modal.close()}
- />
+ />,
+ { theme$: theme.theme$ }
),
{
className: 'shardFailureModal',
diff --git a/src/plugins/data_view_editor/public/open_editor.tsx b/src/plugins/data_view_editor/public/open_editor.tsx
index 98843d6d1698ac..fcf0fad5a32b08 100644
--- a/src/plugins/data_view_editor/public/open_editor.tsx
+++ b/src/plugins/data_view_editor/public/open_editor.tsx
@@ -79,7 +79,8 @@ export const getEditorOpener =
requireTimestampField={requireTimestampField}
/>
-
+ ,
+ { theme$: core.theme.theme$ }
),
{
hideCloseButton: true,
diff --git a/src/plugins/data_view_field_editor/public/open_delete_modal.tsx b/src/plugins/data_view_field_editor/public/open_delete_modal.tsx
index 84e3885ddb605a..f44367d16d08d1 100644
--- a/src/plugins/data_view_field_editor/public/open_delete_modal.tsx
+++ b/src/plugins/data_view_field_editor/public/open_delete_modal.tsx
@@ -75,7 +75,8 @@ export const getFieldDeleteModalOpener =
fieldsToDelete={fieldsToDelete}
closeModal={closeModal}
confirmDelete={onConfirmDelete}
- />
+ />,
+ { theme$: core.theme.theme$ }
)
);
diff --git a/src/plugins/data_view_field_editor/public/open_editor.tsx b/src/plugins/data_view_field_editor/public/open_editor.tsx
index 277d7f5c549ae9..c66e8183b9ab64 100644
--- a/src/plugins/data_view_field_editor/public/open_editor.tsx
+++ b/src/plugins/data_view_field_editor/public/open_editor.tsx
@@ -128,7 +128,8 @@ export const getFieldEditorOpener =
fieldFormats={fieldFormats}
uiSettings={uiSettings}
/>
-
+ ,
+ { theme$: core.theme.theme$ }
),
{
className: euiFlyoutClassname,
diff --git a/src/plugins/data_view_management/public/components/edit_index_pattern/indexed_fields_table/components/table/table.test.tsx b/src/plugins/data_view_management/public/components/edit_index_pattern/indexed_fields_table/components/table/table.test.tsx
index dd78b00f9775e7..f85f7bb2548263 100644
--- a/src/plugins/data_view_management/public/components/edit_index_pattern/indexed_fields_table/components/table/table.test.tsx
+++ b/src/plugins/data_view_management/public/components/edit_index_pattern/indexed_fields_table/components/table/table.test.tsx
@@ -11,7 +11,9 @@ import { shallow } from 'enzyme';
import { IndexPattern } from 'src/plugins/data/public';
import { IndexedFieldItem } from '../../types';
import { Table, renderFieldName, getConflictModalContent } from './table';
-import { overlayServiceMock } from 'src/core/public/mocks';
+import { overlayServiceMock, themeServiceMock } from 'src/core/public/mocks';
+
+const theme = themeServiceMock.createStartContract();
const indexPattern = {
timeFieldName: 'timestamp',
@@ -89,6 +91,7 @@ const renderTable = (
editField={editField}
deleteField={() => {}}
openModal={overlayServiceMock.createStartContract().openModal}
+ theme={theme}
/>
);
diff --git a/src/plugins/data_view_management/public/components/edit_index_pattern/indexed_fields_table/components/table/table.tsx b/src/plugins/data_view_management/public/components/edit_index_pattern/indexed_fields_table/components/table/table.tsx
index 6a82d0380629c0..7e915e3c930a57 100644
--- a/src/plugins/data_view_management/public/components/edit_index_pattern/indexed_fields_table/components/table/table.tsx
+++ b/src/plugins/data_view_management/public/components/edit_index_pattern/indexed_fields_table/components/table/table.tsx
@@ -7,7 +7,7 @@
*/
import React, { PureComponent } from 'react';
-import { OverlayModalStart } from 'src/core/public';
+import { OverlayModalStart, ThemeServiceStart } from 'src/core/public';
import {
EuiIcon,
@@ -179,6 +179,7 @@ interface IndexedFieldProps {
editField: (field: IndexedFieldItem) => void;
deleteField: (fieldName: string) => void;
openModal: OverlayModalStart['open'];
+ theme: ThemeServiceStart;
}
const getItems = (conflictDescriptions: IndexedFieldItem['conflictDescriptions']) => {
@@ -311,7 +312,8 @@ export const getConflictModalContent = ({
const getConflictBtn = (
fieldName: string,
conflictDescriptions: IndexedFieldItem['conflictDescriptions'],
- openModal: IndexedFieldProps['openModal']
+ openModal: IndexedFieldProps['openModal'],
+ theme: ThemeServiceStart
) => {
const onClick = () => {
const overlayRef = openModal(
@@ -322,7 +324,8 @@ const getConflictBtn = (
},
fieldName,
conflictDescriptions,
- })
+ }),
+ { theme$: theme.theme$ }
)
);
};
@@ -355,7 +358,12 @@ export class Table extends PureComponent {
{type === 'conflict' && conflictDescription ? '' : type}
{field.conflictDescriptions
- ? getConflictBtn(field.name, field.conflictDescriptions, this.props.openModal)
+ ? getConflictBtn(
+ field.name,
+ field.conflictDescriptions,
+ this.props.openModal,
+ this.props.theme
+ )
: ''}
);
diff --git a/src/plugins/data_view_management/public/components/edit_index_pattern/indexed_fields_table/indexed_fields_table.tsx b/src/plugins/data_view_management/public/components/edit_index_pattern/indexed_fields_table/indexed_fields_table.tsx
index a72c87655fd63b..29b8d82a997045 100644
--- a/src/plugins/data_view_management/public/components/edit_index_pattern/indexed_fields_table/indexed_fields_table.tsx
+++ b/src/plugins/data_view_management/public/components/edit_index_pattern/indexed_fields_table/indexed_fields_table.tsx
@@ -8,7 +8,7 @@
import React, { Component } from 'react';
import { createSelector } from 'reselect';
-import { OverlayStart } from 'src/core/public';
+import { OverlayStart, ThemeServiceStart } from 'src/core/public';
import { IndexPatternField, IndexPattern } from '../../../../../../plugins/data/public';
import { useKibana } from '../../../../../../plugins/kibana_react/public';
import { Table } from './components/table';
@@ -28,6 +28,7 @@ interface IndexedFieldsTableProps {
fieldWildcardMatcher: (filters: any[]) => (val: any) => boolean;
userEditPermission: boolean;
openModal: OverlayStart['openModal'];
+ theme: ThemeServiceStart;
}
interface IndexedFieldsTableState {
@@ -129,6 +130,7 @@ class IndexedFields extends Component this.props.helpers.editField(field.name)}
deleteField={(fieldName) => this.props.helpers.deleteField(fieldName)}
openModal={this.props.openModal}
+ theme={this.props.theme}
/>
);
diff --git a/src/plugins/data_view_management/public/components/edit_index_pattern/tabs/tabs.tsx b/src/plugins/data_view_management/public/components/edit_index_pattern/tabs/tabs.tsx
index b5940fa8d1bb0d..58b064fa79893d 100644
--- a/src/plugins/data_view_management/public/components/edit_index_pattern/tabs/tabs.tsx
+++ b/src/plugins/data_view_management/public/components/edit_index_pattern/tabs/tabs.tsx
@@ -80,7 +80,7 @@ export function Tabs({
location,
refreshFields,
}: TabsProps) {
- const { application, uiSettings, docLinks, dataViewFieldEditor, overlays } =
+ const { application, uiSettings, docLinks, dataViewFieldEditor, overlays, theme } =
useKibana().services;
const [fieldFilter, setFieldFilter] = useState('');
const [indexedFieldTypeFilter, setIndexedFieldTypeFilter] = useState('');
@@ -236,6 +236,7 @@ export function Tabs({
getFieldInfo,
}}
openModal={overlays.openModal}
+ theme={theme}
/>
)}
@@ -295,6 +296,7 @@ export function Tabs({
DeleteRuntimeFieldProvider,
refreshFields,
overlays,
+ theme,
]
);
diff --git a/src/plugins/data_view_management/public/management_app/mount_management_section.tsx b/src/plugins/data_view_management/public/management_app/mount_management_section.tsx
index 6e0e7ffc9091d6..4bc0a204f68a1c 100644
--- a/src/plugins/data_view_management/public/management_app/mount_management_section.tsx
+++ b/src/plugins/data_view_management/public/management_app/mount_management_section.tsx
@@ -14,7 +14,7 @@ import { i18n } from '@kbn/i18n';
import { I18nProvider } from '@kbn/i18n-react';
import { StartServicesAccessor } from 'src/core/public';
-import { KibanaContextProvider } from '../../../kibana_react/public';
+import { KibanaContextProvider, KibanaThemeProvider } from '../../../kibana_react/public';
import { ManagementAppMountParams } from '../../../management/public';
import {
IndexPatternTableWithRouter,
@@ -39,7 +39,7 @@ export async function mountManagementSection(
params: ManagementAppMountParams
) {
const [
- { chrome, application, uiSettings, notifications, overlays, http, docLinks },
+ { chrome, application, uiSettings, notifications, overlays, http, docLinks, theme },
{ data, dataViewFieldEditor, dataViewEditor },
indexPatternManagementStart,
] = await getStartServices();
@@ -67,25 +67,27 @@ export async function mountManagementSection(
ReactDOM.render(
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
,
params.element
);
diff --git a/src/plugins/data_views/public/data_views/redirect_no_index_pattern.tsx b/src/plugins/data_views/public/data_views/redirect_no_index_pattern.tsx
index 086cd92a92d82d..fc7b8c9eb42b67 100644
--- a/src/plugins/data_views/public/data_views/redirect_no_index_pattern.tsx
+++ b/src/plugins/data_views/public/data_views/redirect_no_index_pattern.tsx
@@ -18,7 +18,8 @@ export const onRedirectNoIndexPattern =
(
capabilities: CoreStart['application']['capabilities'],
navigateToApp: CoreStart['application']['navigateToApp'],
- overlays: CoreStart['overlays']
+ overlays: CoreStart['overlays'],
+ theme: CoreStart['theme']
) =>
() => {
const canManageIndexPatterns = capabilities.management.kibana.indexPatterns;
@@ -38,7 +39,9 @@ export const onRedirectNoIndexPattern =
// give them a friendly info message instead of a terse error message
bannerId = overlays.banners.replace(
bannerId,
- toMountPoint( )
+ toMountPoint( , {
+ theme$: theme.theme$,
+ })
);
// hide the message after the user has had a chance to acknowledge it -- so it doesn't permanently stick around
diff --git a/src/plugins/data_views/public/plugin.ts b/src/plugins/data_views/public/plugin.ts
index 4a00ea91a47bd1..bf092d3fae1771 100644
--- a/src/plugins/data_views/public/plugin.ts
+++ b/src/plugins/data_views/public/plugin.ts
@@ -45,7 +45,7 @@ export class DataViewsPublicPlugin
core: CoreStart,
{ fieldFormats }: DataViewsPublicStartDependencies
): DataViewsPublicPluginStart {
- const { uiSettings, http, notifications, savedObjects, overlays, application } = core;
+ const { uiSettings, http, notifications, savedObjects, theme, overlays, application } = core;
return new DataViewsService({
uiSettings: new UiSettingsPublicToCommon(uiSettings),
@@ -59,7 +59,8 @@ export class DataViewsPublicPlugin
onRedirectNoIndexPattern: onRedirectNoIndexPattern(
application.capabilities,
application.navigateToApp,
- overlays
+ overlays,
+ theme
),
getCanSave: () => Promise.resolve(application.capabilities.indexPatterns.save === true),
});
diff --git a/src/plugins/discover/public/application/main/discover_main_route.tsx b/src/plugins/discover/public/application/main/discover_main_route.tsx
index b2576a3b5d582e..dd1d036b811a20 100644
--- a/src/plugins/discover/public/application/main/discover_main_route.tsx
+++ b/src/plugins/discover/public/application/main/discover_main_route.tsx
@@ -122,6 +122,7 @@ export function DiscoverMainRoute({ services, history }: DiscoverMainProps) {
onBeforeRedirect() {
getUrlTracker().setTrackedUrl('/');
},
+ theme: core.theme,
})(e);
}
}
@@ -139,6 +140,7 @@ export function DiscoverMainRoute({ services, history }: DiscoverMainProps) {
id,
services,
toastNotifications,
+ core.theme,
]);
useEffect(() => {
diff --git a/src/plugins/embeddable/public/lib/embeddables/error_embeddable.tsx b/src/plugins/embeddable/public/lib/embeddables/error_embeddable.tsx
index f4c650507add9f..70c30d314fc824 100644
--- a/src/plugins/embeddable/public/lib/embeddables/error_embeddable.tsx
+++ b/src/plugins/embeddable/public/lib/embeddables/error_embeddable.tsx
@@ -9,10 +9,11 @@
import { EuiText, EuiIcon, EuiSpacer } from '@elastic/eui';
import React from 'react';
import ReactDOM from 'react-dom';
-import { Markdown } from '../../../../kibana_react/public';
+import { KibanaThemeProvider, Markdown } from '../../../../kibana_react/public';
import { Embeddable } from './embeddable';
import { EmbeddableInput, EmbeddableOutput, IEmbeddable } from './i_embeddable';
import { IContainer } from '../containers';
+import { getTheme } from '../../services';
export const ERROR_EMBEDDABLE_TYPE = 'error';
@@ -37,8 +38,13 @@ export class ErrorEmbeddable extends Embeddable
@@ -49,9 +55,16 @@ export class ErrorEmbeddable extends Embeddable
- ,
- dom
+
);
+ const content =
+ theme && theme.theme$ ? (
+ {node}
+ ) : (
+ node
+ );
+
+ ReactDOM.render(content, dom);
}
public destroy() {
diff --git a/src/plugins/embeddable/public/lib/panel/embeddable_panel.test.tsx b/src/plugins/embeddable/public/lib/panel/embeddable_panel.test.tsx
index 78bd337b21e526..8d313030556c62 100644
--- a/src/plugins/embeddable/public/lib/panel/embeddable_panel.test.tsx
+++ b/src/plugins/embeddable/public/lib/panel/embeddable_panel.test.tsx
@@ -31,7 +31,7 @@ import {
import { inspectorPluginMock } from '../../../../inspector/public/mocks';
import { EuiBadge } from '@elastic/eui';
import { embeddablePluginMock } from '../../mocks';
-import { applicationServiceMock } from '../../../../../core/public/mocks';
+import { applicationServiceMock, themeServiceMock } from '../../../../../core/public/mocks';
const actionRegistry = new Map();
const triggerRegistry = new Map();
@@ -44,6 +44,7 @@ const trigger: Trigger = {
};
const embeddableFactory = new ContactCardEmbeddableFactory((() => null) as any, {} as any);
const applicationMock = applicationServiceMock.createStartContract();
+const theme = themeServiceMock.createStartContract();
actionRegistry.set(editModeAction.id, editModeAction);
triggerRegistry.set(trigger.id, trigger);
@@ -152,6 +153,7 @@ test('HelloWorldContainer in view mode hides edit mode actions', async () => {
overlays={{} as any}
inspector={inspector}
SavedObjectFinder={() => null}
+ theme={theme}
/>
);
@@ -191,6 +193,7 @@ const renderInEditModeAndOpenContextMenu = async (
application={applicationMock}
inspector={inspector}
SavedObjectFinder={() => null}
+ theme={theme}
/>
);
@@ -298,6 +301,7 @@ test('HelloWorldContainer in edit mode shows edit mode actions', async () => {
application={applicationMock}
inspector={inspector}
SavedObjectFinder={() => null}
+ theme={theme}
/>
);
@@ -360,6 +364,7 @@ test('Panel title customize link does not exist in view mode', async () => {
application={applicationMock}
inspector={inspector}
SavedObjectFinder={() => null}
+ theme={theme}
/>
);
@@ -395,6 +400,7 @@ test('Runs customize panel action on title click when in edit mode', async () =>
application={applicationMock}
inspector={inspector}
SavedObjectFinder={() => null}
+ theme={theme}
/>
);
@@ -443,6 +449,7 @@ test('Updates when hidePanelTitles is toggled', async () => {
application={applicationMock}
inspector={inspector}
SavedObjectFinder={() => null}
+ theme={theme}
/>
);
@@ -497,6 +504,7 @@ test('Check when hide header option is false', async () => {
inspector={inspector}
SavedObjectFinder={() => null}
hideHeader={false}
+ theme={theme}
/>
);
@@ -535,6 +543,7 @@ test('Check when hide header option is true', async () => {
inspector={inspector}
SavedObjectFinder={() => null}
hideHeader={true}
+ theme={theme}
/>
);
@@ -567,6 +576,7 @@ test('Should work in minimal way rendering only the inspector action', async ()
getActions={() => Promise.resolve([])}
inspector={inspector}
hideHeader={false}
+ theme={theme}
/>
);
diff --git a/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx b/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx
index 6748e9f3b1d083..2e501984dfa763 100644
--- a/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx
+++ b/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx
@@ -12,7 +12,7 @@ import React from 'react';
import { Subscription } from 'rxjs';
import deepEqual from 'fast-deep-equal';
import { buildContextMenuForActions, UiActionsService, Action } from '../ui_actions';
-import { CoreStart, OverlayStart } from '../../../../../core/public';
+import { CoreStart, OverlayStart, ThemeServiceStart } from '../../../../../core/public';
import { toMountPoint } from '../../../../kibana_react/public';
import { UsageCollectionStart } from '../../../../usage_collection/public';
@@ -83,6 +83,7 @@ interface Props {
showBadges?: boolean;
showNotifications?: boolean;
containerContext?: EmbeddableContainerContext;
+ theme: ThemeServiceStart;
}
interface State {
@@ -347,8 +348,7 @@ export class EmbeddablePanel extends React.Component {
) {
return actions;
}
-
- const createGetUserData = (overlays: OverlayStart) =>
+ const createGetUserData = (overlays: OverlayStart, theme: ThemeServiceStart) =>
async function getUserData(context: { embeddable: IEmbeddable }) {
return new Promise<{ title: string | undefined; hideTitle?: boolean }>((resolve) => {
const session = overlays.openModal(
@@ -360,7 +360,8 @@ export class EmbeddablePanel extends React.Component {
resolve({ title, hideTitle });
}}
cancel={() => session.close()}
- />
+ />,
+ { theme$: theme.theme$ }
),
{
'data-test-subj': 'customizePanel',
@@ -373,13 +374,16 @@ export class EmbeddablePanel extends React.Component {
// registry.
return {
...actions,
- customizePanelTitle: new CustomizePanelTitleAction(createGetUserData(this.props.overlays)),
+ customizePanelTitle: new CustomizePanelTitleAction(
+ createGetUserData(this.props.overlays, this.props.theme)
+ ),
addPanel: new AddPanelAction(
this.props.getEmbeddableFactory,
this.props.getAllEmbeddableFactories,
this.props.overlays,
this.props.notifications,
this.props.SavedObjectFinder,
+ this.props.theme,
this.props.reportUiCounter
),
removePanel: new RemovePanelAction(),
diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_action.test.tsx b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_action.test.tsx
index 224cb80478769d..fe6a9ea3c22b3a 100644
--- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_action.test.tsx
+++ b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_action.test.tsx
@@ -16,7 +16,7 @@ import {
} from '../../../../test_samples/embeddables/filterable_embeddable';
import { FilterableEmbeddableFactory } from '../../../../test_samples/embeddables/filterable_embeddable_factory';
import { FilterableContainer } from '../../../../test_samples/embeddables/filterable_container';
-import { coreMock } from '../../../../../../../../core/public/mocks';
+import { coreMock, themeServiceMock } from '../../../../../../../../core/public/mocks';
import { ContactCardEmbeddable } from '../../../../test_samples';
import { EmbeddableStart } from '../../../../../plugin';
import { embeddablePluginMock } from '../../../../../mocks';
@@ -25,6 +25,7 @@ import { defaultTrigger } from '../../../../../../../ui_actions/public/triggers'
const { setup, doStart } = embeddablePluginMock.createInstance();
setup.registerEmbeddableFactory(FILTERABLE_EMBEDDABLE, new FilterableEmbeddableFactory());
const getFactory = doStart().getEmbeddableFactory;
+const theme = themeServiceMock.createStartContract();
let container: FilterableContainer;
let embeddable: FilterableEmbeddable;
@@ -37,7 +38,8 @@ beforeEach(async () => {
() => [] as any,
start.overlays,
start.notifications,
- () => null
+ () => null,
+ theme
);
const derivedFilter: MockFilter = {
@@ -72,7 +74,8 @@ test('Is not compatible when container is in view mode', async () => {
() => [] as any,
start.overlays,
start.notifications,
- () => null
+ () => null,
+ theme
);
container.updateInput({ viewMode: ViewMode.VIEW });
expect(
diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_action.ts b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_action.ts
index 49be1c3ce01233..d766c509782a01 100644
--- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_action.ts
+++ b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_action.ts
@@ -8,7 +8,7 @@
import { i18n } from '@kbn/i18n';
import { Action, ActionExecutionContext } from 'src/plugins/ui_actions/public';
-import { NotificationsStart, OverlayStart } from 'src/core/public';
+import { NotificationsStart, OverlayStart, ThemeServiceStart } from 'src/core/public';
import { EmbeddableStart } from 'src/plugins/embeddable/public/plugin';
import { ViewMode } from '../../../../types';
import { openAddPanelFlyout } from './open_add_panel_flyout';
@@ -31,6 +31,7 @@ export class AddPanelAction implements Action {
private readonly overlays: OverlayStart,
private readonly notifications: NotificationsStart,
private readonly SavedObjectFinder: React.ComponentType,
+ private readonly theme: ThemeServiceStart,
private readonly reportUiCounter?: UsageCollectionStart['reportUiCounter']
) {}
@@ -63,6 +64,7 @@ export class AddPanelAction implements Action {
notifications: this.notifications,
SavedObjectFinder: this.SavedObjectFinder,
reportUiCounter: this.reportUiCounter,
+ theme: this.theme,
});
}
}
diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/open_add_panel_flyout.tsx b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/open_add_panel_flyout.tsx
index fe54b3d134aa0b..00c6f99abda092 100644
--- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/open_add_panel_flyout.tsx
+++ b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/open_add_panel_flyout.tsx
@@ -7,7 +7,7 @@
*/
import React from 'react';
-import { NotificationsStart, OverlayRef, OverlayStart } from 'src/core/public';
+import { NotificationsStart, OverlayRef, OverlayStart, ThemeServiceStart } from 'src/core/public';
import { EmbeddableStart } from '../../../../../plugin';
import { toMountPoint } from '../../../../../../../kibana_react/public';
import { IContainer } from '../../../../containers';
@@ -23,6 +23,7 @@ export function openAddPanelFlyout(options: {
SavedObjectFinder: React.ComponentType;
showCreateNewMenu?: boolean;
reportUiCounter?: UsageCollectionStart['reportUiCounter'];
+ theme: ThemeServiceStart;
}): OverlayRef {
const {
embeddable,
@@ -33,6 +34,7 @@ export function openAddPanelFlyout(options: {
SavedObjectFinder,
showCreateNewMenu,
reportUiCounter,
+ theme,
} = options;
const flyoutSession = overlays.openFlyout(
toMountPoint(
@@ -49,7 +51,8 @@ export function openAddPanelFlyout(options: {
reportUiCounter={reportUiCounter}
SavedObjectFinder={SavedObjectFinder}
showCreateNewMenu={showCreateNewMenu}
- />
+ />,
+ { theme$: theme.theme$ }
),
{
'data-test-subj': 'dashboardAddPanel',
diff --git a/src/plugins/embeddable/public/mocks.tsx b/src/plugins/embeddable/public/mocks.tsx
index 94eb5e5cc6a029..44d2b395a48c35 100644
--- a/src/plugins/embeddable/public/mocks.tsx
+++ b/src/plugins/embeddable/public/mocks.tsx
@@ -20,7 +20,7 @@ import {
ReferenceOrValueEmbeddable,
} from '.';
import { EmbeddablePublicPlugin } from './plugin';
-import { coreMock } from '../../../core/public/mocks';
+import { coreMock, themeServiceMock } from '../../../core/public/mocks';
import { UiActionsService } from './lib/ui_actions';
import { CoreStart } from '../../../core/public';
import { Start as InspectorStart } from '../../inspector/public';
@@ -43,6 +43,8 @@ interface CreateEmbeddablePanelMockArgs {
SavedObjectFinder: React.ComponentType;
}
+const theme = themeServiceMock.createStartContract();
+
export const createEmbeddablePanelMock = ({
getActions,
getEmbeddableFactory,
@@ -64,6 +66,7 @@ export const createEmbeddablePanelMock = ({
overlays={overlays || ({} as any)}
inspector={inspector || ({} as any)}
SavedObjectFinder={SavedObjectFinder || (() => null)}
+ theme={theme}
/>
);
};
diff --git a/src/plugins/embeddable/public/plugin.tsx b/src/plugins/embeddable/public/plugin.tsx
index 465c5d741d5a91..041207f2f23803 100644
--- a/src/plugins/embeddable/public/plugin.tsx
+++ b/src/plugins/embeddable/public/plugin.tsx
@@ -52,6 +52,7 @@ import {
getTelemetryFunction,
} from '../common/lib';
import { getAllMigrations } from '../common/lib/get_all_migrations';
+import { setTheme } from './services';
export interface EmbeddableSetupDependencies {
uiActions: UiActionsSetup;
@@ -119,6 +120,7 @@ export class EmbeddablePublicPlugin implements Plugin
);
diff --git a/src/plugins/embeddable/public/services.ts b/src/plugins/embeddable/public/services.ts
new file mode 100644
index 00000000000000..96088e086a7718
--- /dev/null
+++ b/src/plugins/embeddable/public/services.ts
@@ -0,0 +1,12 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { ThemeServiceSetup } from 'src/core/public';
+import { createGetterSetter } from '../../kibana_utils/public';
+
+export const [getTheme, setTheme] = createGetterSetter('Theme');
diff --git a/src/plugins/inspector/public/plugin.tsx b/src/plugins/inspector/public/plugin.tsx
index e561a9719b3fbc..14a141f7c2ec17 100644
--- a/src/plugins/inspector/public/plugin.tsx
+++ b/src/plugins/inspector/public/plugin.tsx
@@ -106,7 +106,8 @@ export class InspectorPublicPlugin implements Plugin {
uiSettings: core.uiSettings,
share: startDeps.share,
}}
- />
+ />,
+ { theme$: core.theme.theme$ }
),
{
'data-test-subj': 'inspectorPanel',
diff --git a/src/plugins/kibana_react/public/notifications/create_notifications.tsx b/src/plugins/kibana_react/public/notifications/create_notifications.tsx
index 2e59e611fc4217..8eb16a5580ab37 100644
--- a/src/plugins/kibana_react/public/notifications/create_notifications.tsx
+++ b/src/plugins/kibana_react/public/notifications/create_notifications.tsx
@@ -24,8 +24,8 @@ export const createNotifications = (services: KibanaServices): KibanaReactNotifi
throw new TypeError('Could not show notification as notifications service is not available.');
}
services.notifications!.toasts.add({
- title: toMountPoint(title),
- text: toMountPoint(<>{body || null}>),
+ title: toMountPoint(title, { theme$: services.theme?.theme$ }),
+ text: toMountPoint(<>{body || null}>, { theme$: services.theme?.theme$ }),
color,
iconType,
toastLifeTimeMs,
diff --git a/src/plugins/kibana_react/public/overlays/create_react_overlays.tsx b/src/plugins/kibana_react/public/overlays/create_react_overlays.tsx
index 3274699e4bd69e..4349e39d04fd57 100644
--- a/src/plugins/kibana_react/public/overlays/create_react_overlays.tsx
+++ b/src/plugins/kibana_react/public/overlays/create_react_overlays.tsx
@@ -20,12 +20,18 @@ export const createReactOverlays = (services: KibanaServices): KibanaReactOverla
const openFlyout: KibanaReactOverlays['openFlyout'] = (node, options?) => {
checkCoreService();
- return services.overlays!.openFlyout(toMountPoint(<>{node}>), options);
+ return services.overlays!.openFlyout(
+ toMountPoint(<>{node}>, { theme$: services.theme?.theme$ }),
+ options
+ );
};
const openModal: KibanaReactOverlays['openModal'] = (node, options?) => {
checkCoreService();
- return services.overlays!.openModal(toMountPoint(<>{node}>), options);
+ return services.overlays!.openModal(
+ toMountPoint(<>{node}>, { theme$: services.theme?.theme$ }),
+ options
+ );
};
const overlays: KibanaReactOverlays = {
diff --git a/src/plugins/kibana_react/public/table_list_view/table_list_view.test.tsx b/src/plugins/kibana_react/public/table_list_view/table_list_view.test.tsx
index 3663f156c69cb8..bdc5ca30216bc4 100644
--- a/src/plugins/kibana_react/public/table_list_view/table_list_view.test.tsx
+++ b/src/plugins/kibana_react/public/table_list_view/table_list_view.test.tsx
@@ -10,6 +10,7 @@ import { EuiEmptyPrompt } from '@elastic/eui';
import { shallowWithIntl } from '@kbn/test/jest';
import { ToastsStart } from 'kibana/public';
import React from 'react';
+import { themeServiceMock } from '../../../../../src/core/public/mocks';
import { TableListView } from './table_list_view';
const requiredProps = {
@@ -24,6 +25,7 @@ const requiredProps = {
tableCaption: 'test caption',
toastNotifications: {} as ToastsStart,
findItems: jest.fn(() => Promise.resolve({ total: 0, hits: [] })),
+ theme: themeServiceMock.createStartContract(),
};
describe('TableListView', () => {
diff --git a/src/plugins/kibana_react/public/table_list_view/table_list_view.tsx b/src/plugins/kibana_react/public/table_list_view/table_list_view.tsx
index 65c62543538d0f..dd023d522dbb6f 100644
--- a/src/plugins/kibana_react/public/table_list_view/table_list_view.tsx
+++ b/src/plugins/kibana_react/public/table_list_view/table_list_view.tsx
@@ -20,7 +20,7 @@ import {
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
-import { HttpFetchError, ToastsStart } from 'kibana/public';
+import { ThemeServiceStart, HttpFetchError, ToastsStart } from 'kibana/public';
import { debounce, keyBy, sortBy, uniq } from 'lodash';
import React from 'react';
import { KibanaPageTemplate } from '../page_template';
@@ -57,6 +57,7 @@ export interface TableListViewProps {
*/
tableCaption: string;
searchFilters?: SearchFilterConfig[];
+ theme: ThemeServiceStart;
}
export interface TableListViewState {
@@ -177,7 +178,8 @@ class TableListView extends React.Component<
id="kibana-react.tableListView.listing.unableToDeleteDangerMessage"
defaultMessage="Unable to delete {entityName}(s)"
values={{ entityName: this.props.entityName }}
- />
+ />,
+ { theme$: this.props.theme.theme$ }
),
text: `${error}`,
});
diff --git a/src/plugins/kibana_react/public/theme/kibana_theme_provider.tsx b/src/plugins/kibana_react/public/theme/kibana_theme_provider.tsx
index 65d640f34a2cad..56ca7642f1cde7 100644
--- a/src/plugins/kibana_react/public/theme/kibana_theme_provider.tsx
+++ b/src/plugins/kibana_react/public/theme/kibana_theme_provider.tsx
@@ -21,8 +21,9 @@ const defaultTheme: CoreTheme = {
darkMode: false,
};
-// IMPORTANT: This code has been copied to the `interactive_setup` plugin, any changes here should be applied there too.
-// That copy and this comment can be removed once https://github.com/elastic/kibana/issues/119204 is implemented.
+/* IMPORTANT: This code has been copied to the `interactive_setup` plugin, any changes here should be applied there too.
+That copy and this comment can be removed once https://github.com/elastic/kibana/issues/119204 is implemented.*/
+// IMPORTANT: This code has been copied to the `kibana_utils` plugin, to avoid cyclical dependency, any changes here should be applied there too.
export const KibanaThemeProvider: FC = ({ theme$, children }) => {
const theme = useObservable(theme$, defaultTheme);
diff --git a/src/plugins/kibana_react/public/theme/utils.ts b/src/plugins/kibana_react/public/theme/utils.ts
index 161f3a5e36b768..fe8092949d4315 100644
--- a/src/plugins/kibana_react/public/theme/utils.ts
+++ b/src/plugins/kibana_react/public/theme/utils.ts
@@ -10,8 +10,9 @@ import { COLOR_MODES_STANDARD } from '@elastic/eui';
import type { EuiThemeColorModeStandard } from '@elastic/eui';
import type { CoreTheme } from '../../../../core/public';
-// IMPORTANT: This code has been copied to the `interactive_setup` plugin, any changes here should be applied there too.
-// That copy and this comment can be removed once https://github.com/elastic/kibana/issues/119204 is implemented.
+/* IMPORTANT: This code has been copied to the `interactive_setup` plugin, any changes here should be applied there too.
+That copy and this comment can be removed once https://github.com/elastic/kibana/issues/119204 is implemented.*/
+// IMPORTANT: This code has been copied to the `kibana_utils` plugin, to avoid cyclical dependency, any changes here should be applied there too.
export const getColorMode = (theme: CoreTheme): EuiThemeColorModeStandard => {
return theme.darkMode ? COLOR_MODES_STANDARD.dark : COLOR_MODES_STANDARD.light;
diff --git a/src/plugins/kibana_utils/public/history/redirect_when_missing.tsx b/src/plugins/kibana_utils/public/history/redirect_when_missing.tsx
index c64ac35e6f83f2..6913c94a6bb5f1 100644
--- a/src/plugins/kibana_utils/public/history/redirect_when_missing.tsx
+++ b/src/plugins/kibana_utils/public/history/redirect_when_missing.tsx
@@ -13,7 +13,9 @@ import { EuiLoadingSpinner } from '@elastic/eui';
import ReactDOM from 'react-dom';
import { ApplicationStart, HttpStart, ToastsSetup } from 'kibana/public';
+import type { ThemeServiceStart } from '../../../../core/public';
import { SavedObjectNotFound } from '..';
+import { KibanaThemeProvider } from '../theme';
const ReactMarkdown = React.lazy(() => import('react-markdown'));
const ErrorRenderer = (props: { children: string }) => (
@@ -45,6 +47,7 @@ export function redirectWhenMissing({
mapping,
toastNotifications,
onBeforeRedirect,
+ theme,
}: {
history: History;
navigateToApp: ApplicationStart['navigateToApp'];
@@ -62,6 +65,7 @@ export function redirectWhenMissing({
* Optional callback invoked directly before a redirect is triggered
*/
onBeforeRedirect?: (error: SavedObjectNotFound) => void;
+ theme: ThemeServiceStart;
}) {
let localMappingObject: Mapping;
@@ -92,7 +96,12 @@ export function redirectWhenMissing({
defaultMessage: 'Saved object is missing',
}),
text: (element: HTMLElement) => {
- ReactDOM.render({error.message} , element);
+ ReactDOM.render(
+
+ {error.message}
+ ,
+ element
+ );
return () => ReactDOM.unmountComponentAtNode(element);
},
});
diff --git a/src/core/server/saved_objects/import/lib/__mocks__/index.ts b/src/plugins/kibana_utils/public/theme/index.ts
similarity index 73%
rename from src/core/server/saved_objects/import/lib/__mocks__/index.ts
rename to src/plugins/kibana_utils/public/theme/index.ts
index c53fc78c8e8870..165c5ef9195c2a 100644
--- a/src/core/server/saved_objects/import/lib/__mocks__/index.ts
+++ b/src/plugins/kibana_utils/public/theme/index.ts
@@ -6,9 +6,4 @@
* Side Public License, v 1.
*/
-const mockUuidv4 = jest.fn().mockReturnValue('uuidv4');
-jest.mock('uuid', () => ({
- v4: mockUuidv4,
-}));
-
-export { mockUuidv4 };
+export { KibanaThemeProvider } from './kibana_theme_provider';
diff --git a/src/plugins/kibana_utils/public/theme/kibana_theme_provider.test.tsx b/src/plugins/kibana_utils/public/theme/kibana_theme_provider.test.tsx
new file mode 100644
index 00000000000000..21059bd4a8236c
--- /dev/null
+++ b/src/plugins/kibana_utils/public/theme/kibana_theme_provider.test.tsx
@@ -0,0 +1,88 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { useEuiTheme } from '@elastic/eui';
+import type { ReactWrapper } from 'enzyme';
+import type { FC } from 'react';
+import React, { useEffect } from 'react';
+import { act } from 'react-dom/test-utils';
+import { BehaviorSubject, of } from 'rxjs';
+
+import { mountWithIntl } from '@kbn/test/jest';
+import type { CoreTheme } from 'src/core/public';
+
+import { KibanaThemeProvider } from './kibana_theme_provider';
+
+describe('KibanaThemeProvider', () => {
+ let euiTheme: ReturnType | undefined;
+
+ beforeEach(() => {
+ euiTheme = undefined;
+ });
+
+ const flushPromises = async () => {
+ await new Promise(async (resolve, reject) => {
+ try {
+ setImmediate(() => resolve());
+ } catch (error) {
+ reject(error);
+ }
+ });
+ };
+
+ const InnerComponent: FC = () => {
+ const theme = useEuiTheme();
+ useEffect(() => {
+ euiTheme = theme;
+ }, [theme]);
+ return foo
;
+ };
+
+ const refresh = async (wrapper: ReactWrapper) => {
+ await act(async () => {
+ await flushPromises();
+ wrapper.update();
+ });
+ };
+
+ it('exposes the EUI theme provider', async () => {
+ const coreTheme: CoreTheme = { darkMode: true };
+
+ const wrapper = mountWithIntl(
+
+
+
+ );
+
+ await refresh(wrapper);
+
+ expect(euiTheme!.colorMode).toEqual('DARK');
+ });
+
+ it('propagates changes of the coreTheme observable', async () => {
+ const coreTheme$ = new BehaviorSubject({ darkMode: true });
+
+ const wrapper = mountWithIntl(
+
+
+
+ );
+
+ await refresh(wrapper);
+
+ expect(euiTheme!.colorMode).toEqual('DARK');
+
+ await act(async () => {
+ coreTheme$.next({ darkMode: false });
+ });
+
+ await refresh(wrapper);
+
+ expect(euiTheme!.colorMode).toEqual('LIGHT');
+ });
+});
diff --git a/src/plugins/kibana_utils/public/theme/kibana_theme_provider.tsx b/src/plugins/kibana_utils/public/theme/kibana_theme_provider.tsx
new file mode 100644
index 00000000000000..7c7963eff984bb
--- /dev/null
+++ b/src/plugins/kibana_utils/public/theme/kibana_theme_provider.tsx
@@ -0,0 +1,33 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { EuiThemeProvider } from '@elastic/eui';
+import type { FC } from 'react';
+import React, { useMemo } from 'react';
+import useObservable from 'react-use/lib/useObservable';
+import type { Observable } from 'rxjs';
+
+import type { CoreTheme } from '../../../../core/public';
+import { getColorMode } from './utils';
+
+interface KibanaThemeProviderProps {
+ theme$: Observable;
+}
+
+const defaultTheme: CoreTheme = {
+ darkMode: false,
+};
+
+/**
+ * Copied from the `kibana_react` plugin, to avoid cyclical dependency
+ */
+export const KibanaThemeProvider: FC = ({ theme$, children }) => {
+ const theme = useObservable(theme$, defaultTheme);
+ const colorMode = useMemo(() => getColorMode(theme), [theme]);
+ return {children} ;
+};
diff --git a/src/plugins/kibana_utils/public/theme/utils.test.ts b/src/plugins/kibana_utils/public/theme/utils.test.ts
new file mode 100644
index 00000000000000..57b37f4fb2f629
--- /dev/null
+++ b/src/plugins/kibana_utils/public/theme/utils.test.ts
@@ -0,0 +1,19 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { getColorMode } from './utils';
+
+describe('getColorMode', () => {
+ it('returns the correct `colorMode` when `darkMode` is enabled', () => {
+ expect(getColorMode({ darkMode: true })).toEqual('DARK');
+ });
+
+ it('returns the correct `colorMode` when `darkMode` is disabled', () => {
+ expect(getColorMode({ darkMode: false })).toEqual('LIGHT');
+ });
+});
diff --git a/src/plugins/kibana_utils/public/theme/utils.ts b/src/plugins/kibana_utils/public/theme/utils.ts
new file mode 100644
index 00000000000000..887e4fe61fbe13
--- /dev/null
+++ b/src/plugins/kibana_utils/public/theme/utils.ts
@@ -0,0 +1,19 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { COLOR_MODES_STANDARD } from '@elastic/eui';
+import type { EuiThemeColorModeStandard } from '@elastic/eui';
+
+import type { CoreTheme } from '../../../../core/public';
+
+/**
+ * Copied from the `kibana_react` plugin, to avoid cyclical dependency
+ */
+export const getColorMode = (theme: CoreTheme): EuiThemeColorModeStandard => {
+ return theme.darkMode ? COLOR_MODES_STANDARD.dark : COLOR_MODES_STANDARD.light;
+};
diff --git a/src/plugins/kibana_utils/tsconfig.json b/src/plugins/kibana_utils/tsconfig.json
index 0538b145a5d62f..0fba68be6aa57b 100644
--- a/src/plugins/kibana_utils/tsconfig.json
+++ b/src/plugins/kibana_utils/tsconfig.json
@@ -14,7 +14,5 @@
"index.ts",
"../../../typings/**/*"
],
- "references": [
- { "path": "../../core/tsconfig.json" }
- ]
+ "references": [{ "path": "../../core/tsconfig.json" }]
}
diff --git a/src/plugins/share/kibana.json b/src/plugins/share/kibana.json
index 2e34da1da0287c..08365d895f4b4d 100644
--- a/src/plugins/share/kibana.json
+++ b/src/plugins/share/kibana.json
@@ -8,6 +8,6 @@
"githubTeam": "kibana-app-services"
},
"description": "Adds URL Service and sharing capabilities to Kibana",
- "requiredBundles": ["kibanaUtils"],
+ "requiredBundles": ["kibanaReact", "kibanaUtils"],
"optionalPlugins": []
}
diff --git a/src/plugins/share/public/services/share_menu_manager.tsx b/src/plugins/share/public/services/share_menu_manager.tsx
index 52f000512aa075..237e71009d2052 100644
--- a/src/plugins/share/public/services/share_menu_manager.tsx
+++ b/src/plugins/share/public/services/share_menu_manager.tsx
@@ -11,7 +11,8 @@ import ReactDOM from 'react-dom';
import { I18nProvider } from '@kbn/i18n-react';
import { EuiWrappingPopover } from '@elastic/eui';
-import { CoreStart, HttpStart } from 'kibana/public';
+import { CoreStart, HttpStart, ThemeServiceStart } from 'kibana/public';
+import { KibanaThemeProvider } from '../../../kibana_react/public';
import { ShareContextMenu } from '../components/share_context_menu';
import { ShareMenuItem, ShowShareMenuOptions } from '../types';
import { ShareMenuRegistryStart } from './share_menu_registry';
@@ -42,6 +43,7 @@ export class ShareMenuManager {
post: core.http.post,
basePath: core.http.basePath.get(),
anonymousAccess,
+ theme: core.theme,
});
},
};
@@ -65,12 +67,14 @@ export class ShareMenuManager {
basePath,
embedUrlParamExtensions,
anonymousAccess,
+ theme,
showPublicUrlSwitch,
}: ShowShareMenuOptions & {
menuItems: ShareMenuItem[];
post: HttpStart['post'];
basePath: string;
anonymousAccess: AnonymousAccessServiceContract | undefined;
+ theme: ThemeServiceStart;
}) {
if (this.isOpen) {
this.onClose();
@@ -82,30 +86,32 @@ export class ShareMenuManager {
document.body.appendChild(this.container);
const element = (
-
-
-
+
+
+
+
+
);
ReactDOM.render(element, this.container);
diff --git a/src/plugins/share/public/url_service/redirect/components/page.tsx b/src/plugins/share/public/url_service/redirect/components/page.tsx
index 805213b73fdd06..f6aa4d62767c56 100644
--- a/src/plugins/share/public/url_service/redirect/components/page.tsx
+++ b/src/plugins/share/public/url_service/redirect/components/page.tsx
@@ -9,38 +9,45 @@
import * as React from 'react';
import useObservable from 'react-use/lib/useObservable';
import { EuiPageTemplate } from '@elastic/eui';
+import { ThemeServiceSetup } from 'kibana/public';
import { Error } from './error';
import { RedirectManager } from '../redirect_manager';
import { Spinner } from './spinner';
+import { KibanaThemeProvider } from '../../../../../kibana_react/public';
export interface PageProps {
manager: Pick;
+ theme: ThemeServiceSetup;
}
-export const Page: React.FC = ({ manager }) => {
+export const Page: React.FC = ({ manager, theme }) => {
const error = useObservable(manager.error$);
if (error) {
return (
+
+
+
+
+
+ );
+ }
+
+ return (
+
-
+
- );
- }
-
- return (
-
-
-
+
);
};
diff --git a/src/plugins/share/public/url_service/redirect/redirect_manager.ts b/src/plugins/share/public/url_service/redirect/redirect_manager.ts
index e6f524347e48cf..9d7357eab310c5 100644
--- a/src/plugins/share/public/url_service/redirect/redirect_manager.ts
+++ b/src/plugins/share/public/url_service/redirect/redirect_manager.ts
@@ -29,7 +29,7 @@ export class RedirectManager {
chromeless: true,
mount: async (params) => {
const { render } = await import('./render');
- const unmount = render(params.element, { manager: this });
+ const unmount = render(params.element, { manager: this, theme: core.theme });
this.onMount(params.history.location.search);
return () => {
unmount();
diff --git a/src/plugins/share/tsconfig.json b/src/plugins/share/tsconfig.json
index 1f9c438f03fc40..2633d840895d66 100644
--- a/src/plugins/share/tsconfig.json
+++ b/src/plugins/share/tsconfig.json
@@ -9,6 +9,7 @@
"include": ["common/**/*", "public/**/*", "server/**/*"],
"references": [
{ "path": "../../core/tsconfig.json" },
- { "path": "../kibana_utils/tsconfig.json" },
+ { "path": "../kibana_react/tsconfig.json" },
+ { "path": "../kibana_utils/tsconfig.json" }
]
}
diff --git a/src/plugins/ui_actions/public/context_menu/open_context_menu.tsx b/src/plugins/ui_actions/public/context_menu/open_context_menu.tsx
index 91cb8099e8b3cc..04449d7b656bcd 100644
--- a/src/plugins/ui_actions/public/context_menu/open_context_menu.tsx
+++ b/src/plugins/ui_actions/public/context_menu/open_context_menu.tsx
@@ -11,6 +11,8 @@ import React from 'react';
import { EuiContextMenu, EuiContextMenuPanelDescriptor, EuiPopover } from '@elastic/eui';
import { EventEmitter } from 'events';
import ReactDOM from 'react-dom';
+import { KibanaThemeProvider } from '../../../kibana_react/public';
+import { getTheme } from '../services';
let activeSession: ContextMenuSession | null = null;
@@ -168,20 +170,22 @@ export function openContextMenu(
};
ReactDOM.render(
-
-
- ,
+
+
+
+
+ ,
container
);
diff --git a/src/plugins/ui_actions/public/plugin.ts b/src/plugins/ui_actions/public/plugin.ts
index ea6a7e42815cbd..2a2ad100a53d36 100644
--- a/src/plugins/ui_actions/public/plugin.ts
+++ b/src/plugins/ui_actions/public/plugin.ts
@@ -10,6 +10,7 @@ import { CoreStart, CoreSetup, Plugin, PluginInitializerContext } from 'src/core
import { PublicMethodsOf } from '@kbn/utility-types';
import { UiActionsService } from './service';
import { rowClickTrigger, visualizeFieldTrigger, visualizeGeoFieldTrigger } from './triggers';
+import { setTheme } from './services';
export type UiActionsSetup = Pick<
UiActionsService,
@@ -29,6 +30,7 @@ export class UiActionsPlugin implements Plugin {
constructor(initializerContext: PluginInitializerContext) {}
public setup(core: CoreSetup): UiActionsSetup {
+ setTheme(core.theme);
this.service.registerTrigger(rowClickTrigger);
this.service.registerTrigger(visualizeFieldTrigger);
this.service.registerTrigger(visualizeGeoFieldTrigger);
diff --git a/src/plugins/ui_actions/public/services.ts b/src/plugins/ui_actions/public/services.ts
new file mode 100644
index 00000000000000..96088e086a7718
--- /dev/null
+++ b/src/plugins/ui_actions/public/services.ts
@@ -0,0 +1,12 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { ThemeServiceSetup } from 'src/core/public';
+import { createGetterSetter } from '../../kibana_utils/public';
+
+export const [getTheme, setTheme] = createGetterSetter('Theme');
diff --git a/src/plugins/visualizations/public/visualize_app/components/visualize_listing.tsx b/src/plugins/visualizations/public/visualize_app/components/visualize_listing.tsx
index 24f0a871a12f7d..cf219b1cda117d 100644
--- a/src/plugins/visualizations/public/visualize_app/components/visualize_listing.tsx
+++ b/src/plugins/visualizations/public/visualize_app/components/visualize_listing.tsx
@@ -42,6 +42,7 @@ export const VisualizeListing = () => {
visualizeCapabilities,
dashboardCapabilities,
kbnUrlStateStorage,
+ theme,
},
} = useKibana();
const { pathname } = useLocation();
@@ -201,6 +202,7 @@ export const VisualizeListing = () => {
})}
toastNotifications={toastNotifications}
searchFilters={searchFilters}
+ theme={theme}
>
{dashboardCapabilities.createNew && (
<>
diff --git a/src/plugins/visualizations/public/visualize_app/types.ts b/src/plugins/visualizations/public/visualize_app/types.ts
index cca4d9a48d1045..a414dd2e61762b 100644
--- a/src/plugins/visualizations/public/visualize_app/types.ts
+++ b/src/plugins/visualizations/public/visualize_app/types.ts
@@ -17,6 +17,7 @@ import type {
ToastsStart,
ScopedHistory,
AppMountParameters,
+ ThemeServiceStart,
} from 'kibana/public';
import type {
@@ -105,6 +106,7 @@ export interface VisualizeServices extends CoreStart {
usageCollection?: UsageCollectionStart;
getKibanaVersion: () => string;
spaces?: SpacesPluginStart;
+ theme: ThemeServiceStart;
visEditorsRegistry: VisEditorsRegistry;
}
diff --git a/src/plugins/visualizations/public/visualize_app/utils/utils.ts b/src/plugins/visualizations/public/visualize_app/utils/utils.ts
index a99b756fe8714c..b3257f03354a61 100644
--- a/src/plugins/visualizations/public/visualize_app/utils/utils.ts
+++ b/src/plugins/visualizations/public/visualize_app/utils/utils.ts
@@ -92,5 +92,6 @@ export const redirectToSavedObjectPage = (
onBeforeRedirect() {
setActiveUrl(VisualizeConstants.LANDING_PAGE_PATH);
},
+ theme: services.theme,
})(error);
};
diff --git a/x-pack/examples/reporting_example/kibana.json b/x-pack/examples/reporting_example/kibana.json
index 94780f1df0b36f..489b2bcd9f506c 100644
--- a/x-pack/examples/reporting_example/kibana.json
+++ b/x-pack/examples/reporting_example/kibana.json
@@ -10,6 +10,13 @@
},
"description": "Example integration code for applications to feature reports.",
"optionalPlugins": [],
- "requiredPlugins": ["reporting", "developerExamples", "navigation", "screenshotMode", "share"],
+ "requiredPlugins": [
+ "reporting",
+ "developerExamples",
+ "kibanaReact",
+ "navigation",
+ "screenshotMode",
+ "share"
+ ],
"requiredBundles": ["screenshotting"]
}
diff --git a/x-pack/examples/reporting_example/public/application.tsx b/x-pack/examples/reporting_example/public/application.tsx
index 3e1afd7c517a29..9b044ac801773c 100644
--- a/x-pack/examples/reporting_example/public/application.tsx
+++ b/x-pack/examples/reporting_example/public/application.tsx
@@ -9,6 +9,7 @@ import React from 'react';
import ReactDOM from 'react-dom';
import { Router, Route, Switch } from 'react-router-dom';
import { AppMountParameters, CoreStart } from '../../../../src/core/public';
+import { KibanaThemeProvider } from '../../../../../kibana/src/plugins/kibana_react/public';
import { CaptureTest } from './containers/capture_test';
import { Main } from './containers/main';
import { ApplicationContextProvider } from './application_context';
@@ -23,12 +24,14 @@ export const renderApp = (
) => {
ReactDOM.render(
-
-
- } />
- } />
-
-
+
+
+
+ } />
+ } />
+
+
+
,
element
);
diff --git a/x-pack/examples/reporting_example/tsconfig.json b/x-pack/examples/reporting_example/tsconfig.json
index 4c4016911e0c55..1b097d8e528685 100644
--- a/x-pack/examples/reporting_example/tsconfig.json
+++ b/x-pack/examples/reporting_example/tsconfig.json
@@ -9,15 +9,15 @@
"public/**/*.tsx",
"server/**/*.ts",
"common/**/*.ts",
- "../../../typings/**/*",
+ "../../../typings/**/*"
],
"exclude": [],
"references": [
{ "path": "../../../src/core/tsconfig.json" },
+ { "path": "../../../src/plugins/kibana_react/tsconfig.json" },
{ "path": "../../../src/plugins/navigation/tsconfig.json" },
{ "path": "../../../src/plugins/screenshot_mode/tsconfig.json" },
{ "path": "../../../examples/developer_examples/tsconfig.json" },
- { "path": "../../plugins/reporting/tsconfig.json" },
+ { "path": "../../plugins/reporting/tsconfig.json" }
]
}
-
diff --git a/x-pack/plugins/apm/public/components/shared/service_icons/icon_popover.tsx b/x-pack/plugins/apm/public/components/shared/service_icons/icon_popover.tsx
index 19abd2059c903c..ac7c38dc2f8882 100644
--- a/x-pack/plugins/apm/public/components/shared/service_icons/icon_popover.tsx
+++ b/x-pack/plugins/apm/public/components/shared/service_icons/icon_popover.tsx
@@ -6,13 +6,15 @@
*/
import {
- EuiButtonEmpty,
- EuiIcon,
+ EuiButtonIcon,
EuiLoadingContent,
EuiPopover,
EuiPopoverTitle,
} from '@elastic/eui';
+import { rgba } from 'polished';
import React from 'react';
+import styled from 'styled-components';
+import { PopoverItem } from '.';
import { FETCH_STATUS } from '../../../hooks/use_fetcher';
interface IconPopoverProps {
@@ -22,12 +24,18 @@ interface IconPopoverProps {
onClose: () => void;
detailsFetchStatus: FETCH_STATUS;
isOpen: boolean;
- icon: {
- type?: string;
- size?: 's' | 'm' | 'l';
- color?: string;
- };
+ icon: PopoverItem['icon'];
}
+
+const StyledButtonIcon = styled(EuiButtonIcon)`
+ &.serviceIcon_button {
+ box-shadow: ${({ theme }) => {
+ const shadowColor = theme.eui.euiShadowColor;
+ return `0px 0.7px 1.4px ${rgba(shadowColor, 0.07)},
+ 0px 1.9px 4px ${rgba(shadowColor, 0.05)},
+ 0px 4.5px 10px ${rgba(shadowColor, 0.05)} !important;`;
+ }}
+`;
export function IconPopover({
icon,
title,
@@ -46,13 +54,16 @@ export function IconPopover({
anchorPosition="downCenter"
ownFocus={false}
button={
-
-
-
+
}
isOpen={isOpen}
closePopover={onClose}
diff --git a/x-pack/plugins/apm/public/components/shared/service_icons/index.tsx b/x-pack/plugins/apm/public/components/shared/service_icons/index.tsx
index 77639ea1f6d729..b0c6a66d849d86 100644
--- a/x-pack/plugins/apm/public/components/shared/service_icons/index.tsx
+++ b/x-pack/plugins/apm/public/components/shared/service_icons/index.tsx
@@ -49,11 +49,10 @@ export function getContainerIcon(container?: ContainerType) {
type Icons = 'service' | 'container' | 'cloud' | 'alerts';
-interface PopoverItem {
+export interface PopoverItem {
key: Icons;
icon: {
type?: string;
- color?: string;
size?: 's' | 'm' | 'l';
};
isVisible: boolean;
diff --git a/x-pack/plugins/apm/public/components/shared/service_icons/service_icons.stories.tsx b/x-pack/plugins/apm/public/components/shared/service_icons/service_icons.stories.tsx
new file mode 100644
index 00000000000000..41a63eae56d526
--- /dev/null
+++ b/x-pack/plugins/apm/public/components/shared/service_icons/service_icons.stories.tsx
@@ -0,0 +1,130 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+import { EuiFlexGroup, EuiFlexItem, EuiTitle } from '@elastic/eui';
+import { Meta, Story } from '@storybook/react';
+import React from 'react';
+import { CoreStart } from '../../../../../../../src/core/public';
+import { createKibanaReactContext } from '../../../../../../../src/plugins/kibana_react/public';
+import {
+ APIReturnType,
+ createCallApmApi,
+} from '../../../services/rest/createCallApmApi';
+import { ServiceIcons } from './';
+
+type ServiceDetailsReturnType =
+ APIReturnType<'GET /internal/apm/services/{serviceName}/metadata/details'>;
+type ServiceIconsReturnType =
+ APIReturnType<'GET /internal/apm/services/{serviceName}/metadata/icons'>;
+
+interface Args {
+ serviceName: string;
+ start: string;
+ end: string;
+ icons: ServiceIconsReturnType;
+ details: ServiceDetailsReturnType;
+}
+
+const stories: Meta = {
+ title: 'shared/ServiceIcons',
+ component: ServiceIcons,
+ decorators: [
+ (StoryComponent, { args }) => {
+ const { icons, details, serviceName } = args;
+
+ const coreMock = {
+ http: {
+ get: (endpoint: string) => {
+ switch (endpoint) {
+ case `/internal/apm/services/${serviceName}/metadata/icons`:
+ return icons;
+ default:
+ return details;
+ }
+ },
+ },
+ notifications: { toasts: { add: () => {} } },
+ uiSettings: { get: () => true },
+ } as unknown as CoreStart;
+
+ const KibanaReactContext = createKibanaReactContext(coreMock);
+
+ createCallApmApi(coreMock);
+
+ return (
+
+
+
+ );
+ },
+ ],
+};
+export default stories;
+
+export const Example: Story = ({ serviceName, start, end }) => {
+ return (
+
+
+
+
+
+
+
+
+
+
+ {serviceName}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+Example.args = {
+ serviceName: 'opbeans-java',
+ start: '2021-09-10T13:59:00.000Z',
+ end: '2021-09-10T14:14:04.789Z',
+ icons: {
+ agentName: 'java',
+ containerType: 'Kubernetes',
+ cloudProvider: 'gcp',
+ },
+ details: {
+ service: {
+ versions: ['2021-12-22 17:03:27'],
+ runtime: { name: 'Java', version: '11.0.11' },
+ agent: {
+ name: 'java',
+ version: '1.28.3-SNAPSHOT.UNKNOWN',
+ },
+ },
+ container: {
+ os: 'Linux',
+ type: 'Kubernetes',
+ isContainerized: true,
+ totalNumberInstances: 1,
+ },
+ cloud: {
+ provider: 'gcp',
+ projectName: 'elastic-observability',
+ availabilityZones: ['us-central1-c'],
+ machineTypes: ['n1-standard-4'],
+ },
+ },
+};
diff --git a/x-pack/plugins/apm/public/plugin.ts b/x-pack/plugins/apm/public/plugin.ts
index d62cca4e07d450..77c52e1afeec35 100644
--- a/x-pack/plugins/apm/public/plugin.ts
+++ b/x-pack/plugins/apm/public/plugin.ts
@@ -370,7 +370,7 @@ export class ApmPlugin implements Plugin {
fleet.registerExtension({
package: 'apm',
view: 'package-policy-edit',
- useLatestPackageVersion: true,
+ useLatestPackageVersion: false,
Component: getLazyAPMPolicyEditExtension(),
});
diff --git a/x-pack/plugins/apm/server/routes/fleet/get_apm_package_policy_definition.ts b/x-pack/plugins/apm/server/routes/fleet/get_apm_package_policy_definition.ts
index d5cee57b11a822..939feb7c9e22fe 100644
--- a/x-pack/plugins/apm/server/routes/fleet/get_apm_package_policy_definition.ts
+++ b/x-pack/plugins/apm/server/routes/fleet/get_apm_package_policy_definition.ts
@@ -61,9 +61,10 @@ async function getApmPackageVersion(
) {
if (fleetPluginStart && isPrereleaseVersion(kibanaVersion)) {
try {
- const latestApmPackage = await fleetPluginStart.fetchFindLatestPackage(
- 'apm'
- );
+ const latestApmPackage =
+ await fleetPluginStart.packageService.asInternalUser.fetchFindLatestPackage(
+ 'apm'
+ );
return latestApmPackage.version;
} catch (error) {
return SUPPORTED_APM_PACKAGE_VERSION;
diff --git a/x-pack/plugins/cases/public/components/markdown_editor/plugins/lens/processor.tsx b/x-pack/plugins/cases/public/components/markdown_editor/plugins/lens/processor.tsx
index d0e816a06c7df8..9e39cc5cb82185 100644
--- a/x-pack/plugins/cases/public/components/markdown_editor/plugins/lens/processor.tsx
+++ b/x-pack/plugins/cases/public/components/markdown_editor/plugins/lens/processor.tsx
@@ -49,6 +49,7 @@ const LensMarkDownRendererComponent: React.FC = ({
timeRange={timeRange}
attributes={attributes}
renderMode="view"
+ disableTriggers
/>
diff --git a/x-pack/plugins/cases/server/client/metrics/actions/actions.test.ts b/x-pack/plugins/cases/server/client/metrics/actions/actions.test.ts
index 44a582deed397f..f8336424be17d4 100644
--- a/x-pack/plugins/cases/server/client/metrics/actions/actions.test.ts
+++ b/x-pack/plugins/cases/server/client/metrics/actions/actions.test.ts
@@ -25,6 +25,8 @@ const clientArgs = {
authorization: { getAuthorizationFilter },
} as unknown as CasesClientArgs;
+const constructorOptions = { caseId: 'test-id', casesClient: clientMock, clientArgs };
+
describe('Actions', () => {
beforeAll(() => {
getAuthorizationFilter.mockResolvedValue({});
@@ -37,14 +39,14 @@ describe('Actions', () => {
it('returns empty values when no features set up', async () => {
attachmentService.executeCaseActionsAggregations.mockResolvedValue(undefined);
- const handler = new Actions('', clientMock, clientArgs);
+ const handler = new Actions(constructorOptions);
expect(await handler.compute()).toEqual({});
});
it('returns zero values when aggregation returns undefined', async () => {
attachmentService.executeCaseActionsAggregations.mockResolvedValue(undefined);
- const handler = new Actions('', clientMock, clientArgs);
+ const handler = new Actions(constructorOptions);
handler.setupFeature('actions.isolateHost');
expect(await handler.compute()).toEqual({
@@ -60,7 +62,7 @@ describe('Actions', () => {
it('returns zero values when aggregation returns empty object', async () => {
attachmentService.executeCaseActionsAggregations.mockResolvedValue({});
- const handler = new Actions('', clientMock, clientArgs);
+ const handler = new Actions(constructorOptions);
handler.setupFeature('actions.isolateHost');
expect(await handler.compute()).toEqual({
@@ -78,7 +80,7 @@ describe('Actions', () => {
actions: { buckets: [] },
});
- const handler = new Actions('', clientMock, clientArgs);
+ const handler = new Actions(constructorOptions);
handler.setupFeature('actions.isolateHost');
expect(await handler.compute()).toEqual({
@@ -96,7 +98,7 @@ describe('Actions', () => {
actions: { buckets: [{ key: 'otherAction', doc_count: 10 }] },
});
- const handler = new Actions('', clientMock, clientArgs);
+ const handler = new Actions(constructorOptions);
handler.setupFeature('actions.isolateHost');
expect(await handler.compute()).toEqual({
@@ -117,7 +119,7 @@ describe('Actions', () => {
},
});
- const handler = new Actions('', clientMock, clientArgs);
+ const handler = new Actions(constructorOptions);
handler.setupFeature('actions.isolateHost');
expect(await handler.compute()).toEqual({
diff --git a/x-pack/plugins/cases/server/client/metrics/actions/actions.ts b/x-pack/plugins/cases/server/client/metrics/actions/actions.ts
index afbbd024f7b4ab..d104ae212b83ec 100644
--- a/x-pack/plugins/cases/server/client/metrics/actions/actions.ts
+++ b/x-pack/plugins/cases/server/client/metrics/actions/actions.ts
@@ -9,42 +9,27 @@ import { merge } from 'lodash';
import { CaseMetricsResponse } from '../../../../common/api';
import { Operations } from '../../../authorization';
import { createCaseError } from '../../../common/error';
-import { CasesClient } from '../../client';
-import { CasesClientArgs } from '../../types';
-import { AggregationBuilder, MetricsHandler } from '../types';
+import { AggregationHandler } from '../aggregation_handler';
+import { AggregationBuilder, BaseHandlerCommonOptions } from '../types';
import { IsolateHostActions } from './aggregations/isolate_host';
-export class Actions implements MetricsHandler {
- private aggregators: AggregationBuilder[] = [];
- private readonly featureAggregations = new Map([
- ['actions.isolateHost', new IsolateHostActions()],
- ]);
-
- constructor(
- private readonly caseId: string,
- private readonly casesClient: CasesClient,
- private readonly clientArgs: CasesClientArgs
- ) {}
-
- public getFeatures(): Set {
- return new Set(this.featureAggregations.keys());
- }
-
- public setupFeature(feature: string) {
- const aggregation = this.featureAggregations.get(feature);
- if (aggregation) {
- this.aggregators.push(aggregation);
- }
+export class Actions extends AggregationHandler {
+ constructor(options: BaseHandlerCommonOptions) {
+ super(
+ options,
+ new Map([['actions.isolateHost', new IsolateHostActions()]])
+ );
}
public async compute(): Promise {
const { unsecuredSavedObjectsClient, authorization, attachmentService, logger } =
- this.clientArgs;
+ this.options.clientArgs;
+ const { caseId, casesClient } = this.options;
try {
// This will perform an authorization check to ensure the user has access to the parent case
- const theCase = await this.casesClient.cases.get({
- id: this.caseId,
+ const theCase = await casesClient.cases.get({
+ id: caseId,
includeComments: false,
includeSubCaseComments: false,
});
@@ -53,7 +38,7 @@ export class Actions implements MetricsHandler {
Operations.getAttachmentMetrics
);
- const aggregations = this.aggregators.reduce((aggs, aggregator) => {
+ const aggregations = this.aggregationBuilders.reduce((aggs, aggregator) => {
return { ...aggs, ...aggregator.build() };
}, {});
@@ -64,13 +49,13 @@ export class Actions implements MetricsHandler {
aggregations,
});
- return this.aggregators.reduce(
+ return this.aggregationBuilders.reduce(
(acc, aggregator) => merge(acc, aggregator.formatResponse(response)),
{}
);
} catch (error) {
throw createCaseError({
- message: `Failed to compute actions attached case id: ${this.caseId}: ${error}`,
+ message: `Failed to compute actions attached case id: ${caseId}: ${error}`,
error,
logger,
});
diff --git a/x-pack/plugins/cases/server/client/metrics/aggregation_handler.ts b/x-pack/plugins/cases/server/client/metrics/aggregation_handler.ts
new file mode 100644
index 00000000000000..382faa354db59d
--- /dev/null
+++ b/x-pack/plugins/cases/server/client/metrics/aggregation_handler.ts
@@ -0,0 +1,31 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { BaseHandler } from './base_handler';
+import { AggregationBuilder, BaseHandlerCommonOptions } from './types';
+
+export abstract class AggregationHandler extends BaseHandler {
+ protected aggregationBuilders: AggregationBuilder[] = [];
+
+ constructor(
+ options: BaseHandlerCommonOptions,
+ private readonly aggregations: Map
+ ) {
+ super(options);
+ }
+
+ getFeatures(): Set {
+ return new Set(this.aggregations.keys());
+ }
+
+ public setupFeature(feature: string) {
+ const aggregation = this.aggregations.get(feature);
+ if (aggregation) {
+ this.aggregationBuilders.push(aggregation);
+ }
+ }
+}
diff --git a/x-pack/plugins/cases/server/client/metrics/alerts/count.ts b/x-pack/plugins/cases/server/client/metrics/alerts/count.ts
index 8113f305ad4ba7..8e04b5fc42c858 100644
--- a/x-pack/plugins/cases/server/client/metrics/alerts/count.ts
+++ b/x-pack/plugins/cases/server/client/metrics/alerts/count.ts
@@ -8,29 +8,24 @@
import { CaseMetricsResponse } from '../../../../common/api';
import { Operations } from '../../../authorization';
import { createCaseError } from '../../../common/error';
-import { CasesClient } from '../../client';
-import { CasesClientArgs } from '../../types';
-import { MetricsHandler } from '../types';
-
-export class AlertsCount implements MetricsHandler {
- constructor(
- private readonly caseId: string,
- private readonly casesClient: CasesClient,
- private readonly clientArgs: CasesClientArgs
- ) {}
-
- public getFeatures(): Set {
- return new Set(['alerts.count']);
+import { BaseHandler } from '../base_handler';
+import { BaseHandlerCommonOptions } from '../types';
+
+export class AlertsCount extends BaseHandler {
+ constructor(options: BaseHandlerCommonOptions) {
+ super(options, ['alerts.count']);
}
public async compute(): Promise {
const { unsecuredSavedObjectsClient, authorization, attachmentService, logger } =
- this.clientArgs;
+ this.options.clientArgs;
+
+ const { caseId, casesClient } = this.options;
try {
// This will perform an authorization check to ensure the user has access to the parent case
- const theCase = await this.casesClient.cases.get({
- id: this.caseId,
+ const theCase = await casesClient.cases.get({
+ id: caseId,
includeComments: false,
includeSubCaseComments: false,
});
@@ -52,7 +47,7 @@ export class AlertsCount implements MetricsHandler {
};
} catch (error) {
throw createCaseError({
- message: `Failed to count alerts attached case id: ${this.caseId}: ${error}`,
+ message: `Failed to count alerts attached case id: ${caseId}: ${error}`,
error,
logger,
});
diff --git a/x-pack/plugins/cases/server/client/metrics/alerts/details.test.ts b/x-pack/plugins/cases/server/client/metrics/alerts/details.test.ts
index 0dfcc04c765da0..529590bfbc7dcb 100644
--- a/x-pack/plugins/cases/server/client/metrics/alerts/details.test.ts
+++ b/x-pack/plugins/cases/server/client/metrics/alerts/details.test.ts
@@ -5,35 +5,53 @@
* 2.0.
*/
-import { createCasesClientMock } from '../../mocks';
+import { CasesClientMock, createCasesClientMock } from '../../mocks';
import { CasesClientArgs } from '../../types';
import { loggingSystemMock } from '../../../../../../../src/core/server/mocks';
import { AlertDetails } from './details';
import { mockAlertsService } from '../test_utils/alerts';
+import { BaseHandlerCommonOptions } from '../types';
describe('AlertDetails', () => {
+ let client: CasesClientMock;
+ let mockServices: ReturnType['mockServices'];
+ let clientArgs: ReturnType['clientArgs'];
+ let constructorOptions: BaseHandlerCommonOptions;
+
beforeEach(() => {
+ client = createMockClient();
+ ({ mockServices, clientArgs } = createMockClientArgs());
+ constructorOptions = { caseId: '', casesClient: client, clientArgs };
+ });
+
+ afterEach(() => {
jest.clearAllMocks();
});
it('returns empty alert details metrics when there are no alerts', async () => {
- const client = createCasesClientMock();
client.attachments.getAllAlertsAttachToCase.mockImplementation(async () => {
return [];
});
- const handler = new AlertDetails('', client, {} as CasesClientArgs);
+ const handler = new AlertDetails({
+ caseId: '',
+ casesClient: client,
+ clientArgs: {} as CasesClientArgs,
+ });
expect(await handler.compute()).toEqual({});
});
it('returns the default zero values when there are no alerts but features are requested', async () => {
- const client = createCasesClientMock();
client.attachments.getAllAlertsAttachToCase.mockImplementation(async () => {
return [];
});
- const handler = new AlertDetails('', client, {} as CasesClientArgs);
+ const handler = new AlertDetails({
+ caseId: '',
+ casesClient: client,
+ clientArgs: {} as CasesClientArgs,
+ });
handler.setupFeature('alerts.hosts');
expect(await handler.compute()).toEqual({
@@ -47,11 +65,9 @@ describe('AlertDetails', () => {
});
it('returns the default zero values for hosts when the count aggregation returns undefined', async () => {
- const client = createMockClient();
- const { mockServices, clientArgs } = createMockClientArgs();
mockServices.alertsService.executeAggregations.mockImplementation(async () => ({}));
- const handler = new AlertDetails('', client, clientArgs);
+ const handler = new AlertDetails(constructorOptions);
handler.setupFeature('alerts.hosts');
expect(await handler.compute()).toEqual({
@@ -65,11 +81,9 @@ describe('AlertDetails', () => {
});
it('returns the default zero values for users when the count aggregation returns undefined', async () => {
- const client = createMockClient();
- const { mockServices, clientArgs } = createMockClientArgs();
mockServices.alertsService.executeAggregations.mockImplementation(async () => ({}));
- const handler = new AlertDetails('', client, clientArgs);
+ const handler = new AlertDetails(constructorOptions);
handler.setupFeature('alerts.users');
expect(await handler.compute()).toEqual({
@@ -83,11 +97,9 @@ describe('AlertDetails', () => {
});
it('returns the default zero values for hosts when the top hits aggregation returns undefined', async () => {
- const client = createMockClient();
- const { mockServices, clientArgs } = createMockClientArgs();
mockServices.alertsService.executeAggregations.mockImplementation(async () => ({}));
- const handler = new AlertDetails('', client, clientArgs);
+ const handler = new AlertDetails(constructorOptions);
handler.setupFeature('alerts.hosts');
expect(await handler.compute()).toEqual({
@@ -101,11 +113,9 @@ describe('AlertDetails', () => {
});
it('returns the default zero values for users when the top hits aggregation returns undefined', async () => {
- const client = createMockClient();
- const { mockServices, clientArgs } = createMockClientArgs();
mockServices.alertsService.executeAggregations.mockImplementation(async () => ({}));
- const handler = new AlertDetails('', client, clientArgs);
+ const handler = new AlertDetails(constructorOptions);
handler.setupFeature('alerts.users');
expect(await handler.compute()).toEqual({
@@ -119,30 +129,34 @@ describe('AlertDetails', () => {
});
it('returns empty alert details metrics when no features were setup', async () => {
- const client = createCasesClientMock();
client.attachments.getAllAlertsAttachToCase.mockImplementation(async () => {
return [{ id: '1', index: '2', attached_at: '3' }];
});
- const handler = new AlertDetails('', client, {} as CasesClientArgs);
+ const handler = new AlertDetails({
+ caseId: '',
+ casesClient: client,
+ clientArgs: {} as CasesClientArgs,
+ });
expect(await handler.compute()).toEqual({});
});
it('returns empty alert details metrics when no features were setup when called twice', async () => {
- const client = createCasesClientMock();
client.attachments.getAllAlertsAttachToCase.mockImplementation(async () => {
return [{ id: '1', index: '2', attached_at: '3' }];
});
- const handler = new AlertDetails('', client, {} as CasesClientArgs);
+ const handler = new AlertDetails({
+ caseId: '',
+ casesClient: client,
+ clientArgs: {} as CasesClientArgs,
+ });
expect(await handler.compute()).toEqual({});
expect(await handler.compute()).toEqual({});
});
it('returns host details when the host feature is setup', async () => {
- const client = createMockClient();
- const { clientArgs } = createMockClientArgs();
- const handler = new AlertDetails('', client, clientArgs);
+ const handler = new AlertDetails(constructorOptions);
handler.setupFeature('alerts.hosts');
@@ -157,10 +171,7 @@ describe('AlertDetails', () => {
});
it('returns user details when the user feature is setup', async () => {
- const client = createMockClient();
- const { clientArgs } = createMockClientArgs();
-
- const handler = new AlertDetails('', client, clientArgs);
+ const handler = new AlertDetails(constructorOptions);
handler.setupFeature('alerts.users');
@@ -175,10 +186,7 @@ describe('AlertDetails', () => {
});
it('returns user and host details when the user and host features are setup', async () => {
- const client = createMockClient();
- const { clientArgs } = createMockClientArgs();
-
- const handler = new AlertDetails('', client, clientArgs);
+ const handler = new AlertDetails(constructorOptions);
handler.setupFeature('alerts.users');
handler.setupFeature('alerts.hosts');
diff --git a/x-pack/plugins/cases/server/client/metrics/alerts/details.ts b/x-pack/plugins/cases/server/client/metrics/alerts/details.ts
index 8155fe60961c05..eec21d23c4639e 100644
--- a/x-pack/plugins/cases/server/client/metrics/alerts/details.ts
+++ b/x-pack/plugins/cases/server/client/metrics/alerts/details.ts
@@ -10,56 +10,43 @@ import { merge } from 'lodash';
import { CaseMetricsResponse } from '../../../../common/api';
import { createCaseError } from '../../../common/error';
-import { CasesClient } from '../../client';
-import { CasesClientArgs } from '../../types';
-import { MetricsHandler, AggregationBuilder, AggregationResponse } from '../types';
+import { AggregationHandler } from '../aggregation_handler';
+import { AggregationBuilder, AggregationResponse, BaseHandlerCommonOptions } from '../types';
import { AlertHosts, AlertUsers } from './aggregations';
-export class AlertDetails implements MetricsHandler {
- private aggregationsToBuild: AggregationBuilder[] = [];
- private readonly aggregations = new Map([
- ['alerts.hosts', new AlertHosts()],
- ['alerts.users', new AlertUsers()],
- ]);
-
- constructor(
- private readonly caseId: string,
- private readonly casesClient: CasesClient,
- private readonly clientArgs: CasesClientArgs
- ) {}
-
- public getFeatures(): Set {
- return new Set(this.aggregations.keys());
- }
-
- public setupFeature(feature: string) {
- const aggregation = this.aggregations.get(feature);
- if (aggregation) {
- this.aggregationsToBuild.push(aggregation);
- }
+export class AlertDetails extends AggregationHandler {
+ constructor(options: BaseHandlerCommonOptions) {
+ super(
+ options,
+ new Map([
+ ['alerts.hosts', new AlertHosts()],
+ ['alerts.users', new AlertUsers()],
+ ])
+ );
}
public async compute(): Promise {
- const { alertsService, logger } = this.clientArgs;
+ const { alertsService, logger } = this.options.clientArgs;
+ const { caseId, casesClient } = this.options;
try {
- const alerts = await this.casesClient.attachments.getAllAlertsAttachToCase({
- caseId: this.caseId,
+ const alerts = await casesClient.attachments.getAllAlertsAttachToCase({
+ caseId,
});
- if (alerts.length <= 0 || this.aggregationsToBuild.length <= 0) {
+ if (alerts.length <= 0 || this.aggregationBuilders.length <= 0) {
return this.formatResponse();
}
const aggregationsResponse = await alertsService.executeAggregations({
- aggregationBuilders: this.aggregationsToBuild,
+ aggregationBuilders: this.aggregationBuilders,
alerts,
});
return this.formatResponse(aggregationsResponse);
} catch (error) {
throw createCaseError({
- message: `Failed to retrieve alerts details attached case id: ${this.caseId}: ${error}`,
+ message: `Failed to retrieve alerts details attached case id: ${caseId}: ${error}`,
error,
logger,
});
@@ -67,7 +54,7 @@ export class AlertDetails implements MetricsHandler {
}
private formatResponse(aggregationsResponse?: AggregationResponse): CaseMetricsResponse {
- return this.aggregationsToBuild.reduce(
+ return this.aggregationBuilders.reduce(
(acc, feature) => merge(acc, feature.formatResponse(aggregationsResponse)),
{}
);
diff --git a/x-pack/plugins/cases/server/client/metrics/base_handler.ts b/x-pack/plugins/cases/server/client/metrics/base_handler.ts
new file mode 100644
index 00000000000000..bf76be05f58b37
--- /dev/null
+++ b/x-pack/plugins/cases/server/client/metrics/base_handler.ts
@@ -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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { CaseMetricsResponse } from '../../../common/api';
+import { BaseHandlerCommonOptions, MetricsHandler } from './types';
+
+export abstract class BaseHandler implements MetricsHandler {
+ constructor(
+ protected readonly options: BaseHandlerCommonOptions,
+ private readonly features?: string[]
+ ) {}
+
+ getFeatures(): Set {
+ return new Set(this.features);
+ }
+
+ abstract compute(): Promise;
+}
diff --git a/x-pack/plugins/cases/server/client/metrics/connectors.ts b/x-pack/plugins/cases/server/client/metrics/connectors.ts
index 83e1270baf8467..137bcdd61cdec9 100644
--- a/x-pack/plugins/cases/server/client/metrics/connectors.ts
+++ b/x-pack/plugins/cases/server/client/metrics/connectors.ts
@@ -6,11 +6,12 @@
*/
import { CaseMetricsResponse } from '../../../common/api';
-import { MetricsHandler } from './types';
+import { BaseHandler } from './base_handler';
+import { BaseHandlerCommonOptions } from './types';
-export class Connectors implements MetricsHandler {
- public getFeatures(): Set {
- return new Set(['connectors']);
+export class Connectors extends BaseHandler {
+ constructor(options: BaseHandlerCommonOptions) {
+ super(options, ['connectors']);
}
public async compute(): Promise {
diff --git a/x-pack/plugins/cases/server/client/metrics/get_case_metrics.test.ts b/x-pack/plugins/cases/server/client/metrics/get_case_metrics.test.ts
index 3d503e50feb819..64870b29a385d8 100644
--- a/x-pack/plugins/cases/server/client/metrics/get_case_metrics.test.ts
+++ b/x-pack/plugins/cases/server/client/metrics/get_case_metrics.test.ts
@@ -7,7 +7,7 @@
import { getCaseMetrics } from './get_case_metrics';
import { CaseAttributes, CaseResponse, CaseStatuses } from '../../../common/api';
-import { createCasesClientMock } from '../mocks';
+import { CasesClientMock, createCasesClientMock } from '../mocks';
import { CasesClientArgs } from '../types';
import { createAuthorizationMock } from '../../authorization/mock';
import { loggingSystemMock, savedObjectsClientMock } from '../../../../../../src/core/server/mocks';
@@ -29,14 +29,18 @@ describe('getMetrics', () => {
closed_at: '2021-11-23T19:59:44Z',
};
+ let client: CasesClientMock;
+ let mockServices: ReturnType['mockServices'];
+ let clientArgs: ReturnType['clientArgs'];
+
const openDuration =
inProgressStatusChangeTimestamp.getTime() - new Date(mockCreateCloseInfo.created_at).getTime();
const inProgressDuration = currentTime.getTime() - inProgressStatusChangeTimestamp.getTime();
- const client = createMockClient();
- const { mockServices, clientArgs } = createMockClientArgs();
-
beforeEach(() => {
+ client = createMockClient();
+ ({ mockServices, clientArgs } = createMockClientArgs());
+
jest.clearAllMocks();
jest.useFakeTimers('modern');
jest.setSystemTime(currentTime);
diff --git a/x-pack/plugins/cases/server/client/metrics/get_case_metrics.ts b/x-pack/plugins/cases/server/client/metrics/get_case_metrics.ts
index 554e33e290d3c4..57755c17e65eb9 100644
--- a/x-pack/plugins/cases/server/client/metrics/get_case_metrics.ts
+++ b/x-pack/plugins/cases/server/client/metrics/get_case_metrics.ts
@@ -66,13 +66,9 @@ const buildHandlers = (
casesClient: CasesClient,
clientArgs: CasesClientArgs
): Set => {
- const handlers: MetricsHandler[] = [
- new Lifespan(params.caseId, casesClient, clientArgs),
- new AlertsCount(params.caseId, casesClient, clientArgs),
- new AlertDetails(params.caseId, casesClient, clientArgs),
- new Actions(params.caseId, casesClient, clientArgs),
- new Connectors(),
- ];
+ const handlers: MetricsHandler[] = [AlertsCount, AlertDetails, Actions, Connectors, Lifespan].map(
+ (ClassName) => new ClassName({ caseId: params.caseId, casesClient, clientArgs })
+ );
const uniqueFeatures = new Set(params.features);
const handlerFeatures = new Set();
diff --git a/x-pack/plugins/cases/server/client/metrics/lifespan.ts b/x-pack/plugins/cases/server/client/metrics/lifespan.ts
index b622be44c4ded4..1af9e38050d012 100644
--- a/x-pack/plugins/cases/server/client/metrics/lifespan.ts
+++ b/x-pack/plugins/cases/server/client/metrics/lifespan.ts
@@ -17,27 +17,22 @@ import {
} from '../../../common/api';
import { Operations } from '../../authorization';
import { createCaseError } from '../../common/error';
-import { CasesClient } from '../client';
-import { CasesClientArgs } from '../types';
-import { MetricsHandler } from './types';
-
-export class Lifespan implements MetricsHandler {
- constructor(
- private readonly caseId: string,
- private readonly casesClient: CasesClient,
- private readonly clientArgs: CasesClientArgs
- ) {}
-
- public getFeatures(): Set {
- return new Set(['lifespan']);
+import { BaseHandler } from './base_handler';
+import { BaseHandlerCommonOptions } from './types';
+
+export class Lifespan extends BaseHandler {
+ constructor(options: BaseHandlerCommonOptions) {
+ super(options, ['lifespan']);
}
public async compute(): Promise {
const { unsecuredSavedObjectsClient, authorization, userActionService, logger } =
- this.clientArgs;
+ this.options.clientArgs;
+
+ const { caseId, casesClient } = this.options;
try {
- const caseInfo = await this.casesClient.cases.get({ id: this.caseId });
+ const caseInfo = await casesClient.cases.get({ id: caseId });
const caseOpenTimestamp = new Date(caseInfo.created_at);
if (!isDateValid(caseOpenTimestamp)) {
@@ -52,7 +47,7 @@ export class Lifespan implements MetricsHandler {
const statusUserActions = await userActionService.findStatusChanges({
unsecuredSavedObjectsClient,
- caseId: this.caseId,
+ caseId,
filter: authorizationFilter,
});
@@ -67,7 +62,7 @@ export class Lifespan implements MetricsHandler {
};
} catch (error) {
throw createCaseError({
- message: `Failed to retrieve lifespan metrics for case id: ${this.caseId}: ${error}`,
+ message: `Failed to retrieve lifespan metrics for case id: ${caseId}: ${error}`,
error,
logger,
});
diff --git a/x-pack/plugins/cases/server/client/metrics/types.ts b/x-pack/plugins/cases/server/client/metrics/types.ts
index 68f7a9b58ed93a..6773ab59b0b02f 100644
--- a/x-pack/plugins/cases/server/client/metrics/types.ts
+++ b/x-pack/plugins/cases/server/client/metrics/types.ts
@@ -7,6 +7,8 @@
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { CaseMetricsResponse } from '../../../common/api';
+import { CasesClient } from '../client';
+import { CasesClientArgs } from '../types';
export interface MetricsHandler {
getFeatures(): Set;
@@ -21,3 +23,9 @@ export interface AggregationBuilder {
}
export type AggregationResponse = Record | undefined;
+
+export interface BaseHandlerCommonOptions {
+ caseId: string;
+ casesClient: CasesClient;
+ clientArgs: CasesClientArgs;
+}
diff --git a/x-pack/plugins/data_enhanced/public/plugin.ts b/x-pack/plugins/data_enhanced/public/plugin.ts
index 6ec645c932e053..ee76cce9b9d2b4 100644
--- a/x-pack/plugins/data_enhanced/public/plugin.ts
+++ b/x-pack/plugins/data_enhanced/public/plugin.ts
@@ -81,7 +81,8 @@ export class DataEnhancedPlugin
usageCollector: this.usageCollector,
tourDisabled: plugins.screenshotMode.isScreenshotMode(),
})
- )
+ ),
+ { theme$: core.theme.theme$ }
),
});
}
diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/delete_button.tsx b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/delete_button.tsx
index 3d1a3052e720bf..127a63b647a249 100644
--- a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/delete_button.tsx
+++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/delete_button.tsx
@@ -74,7 +74,8 @@ export const createDeleteActionDescriptor = (
onClick: async () => {
const ref = core.overlays.openModal(
toMountPoint(
- ref?.close()} searchSession={uiSession} api={api} />
+ ref?.close()} searchSession={uiSession} api={api} />,
+ { theme$: core.theme.theme$ }
)
);
await ref.onClose;
diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/extend_button.tsx b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/extend_button.tsx
index 6989caeca359ef..d8b5e9de16688d 100644
--- a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/extend_button.tsx
+++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/extend_button.tsx
@@ -81,7 +81,8 @@ export const createExtendActionDescriptor = (
onClick: async () => {
const ref = core.overlays.openModal(
toMountPoint(
- ref?.close()} searchSession={uiSession} api={api} />
+ ref?.close()} searchSession={uiSession} api={api} />,
+ { theme$: core.theme.theme$ }
)
);
await ref.onClose;
diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/inspect_button.tsx b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/inspect_button.tsx
index 23c010e0fbc67e..2b917c28c4b3b1 100644
--- a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/inspect_button.tsx
+++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/inspect_button.tsx
@@ -97,7 +97,7 @@ export const createInspectActionDescriptor = (
),
onClick: async () => {
const flyout = ;
- const overlay = core.overlays.openFlyout(toMountPoint(flyout));
+ const overlay = core.overlays.openFlyout(toMountPoint(flyout, { theme$: core.theme.theme$ }));
await overlay.onClose;
},
});
diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/rename_button.tsx b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/rename_button.tsx
index beb773e057cb9f..d663d0da5cad70 100644
--- a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/rename_button.tsx
+++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/rename_button.tsx
@@ -113,7 +113,8 @@ export const createRenameActionDescriptor = (
onClick: async () => {
const ref = core.overlays.openModal(
toMountPoint(
- ref?.close()} api={api} searchSession={uiSession} />
+ ref?.close()} api={api} searchSession={uiSession} />,
+ { theme$: core.theme.theme$ }
)
);
await ref.onClose;
diff --git a/x-pack/plugins/event_log/generated/mappings.json b/x-pack/plugins/event_log/generated/mappings.json
index aba23eef79e3f8..0627673ceff931 100644
--- a/x-pack/plugins/event_log/generated/mappings.json
+++ b/x-pack/plugins/event_log/generated/mappings.json
@@ -239,6 +239,9 @@
},
"task": {
"properties": {
+ "id": {
+ "type": "keyword"
+ },
"scheduled": {
"type": "date"
},
@@ -303,6 +306,19 @@
}
}
},
+ "reporting": {
+ "properties": {
+ "id": {
+ "type": "keyword"
+ },
+ "jobType": {
+ "type": "keyword"
+ },
+ "byteSize": {
+ "type": "long"
+ }
+ }
+ },
"saved_objects": {
"type": "nested",
"properties": {
@@ -341,4 +357,4 @@
}
}
}
-}
\ No newline at end of file
+}
diff --git a/x-pack/plugins/event_log/generated/schemas.ts b/x-pack/plugins/event_log/generated/schemas.ts
index e73bafd9cb81ee..b0ec7e78d28757 100644
--- a/x-pack/plugins/event_log/generated/schemas.ts
+++ b/x-pack/plugins/event_log/generated/schemas.ts
@@ -104,6 +104,7 @@ export const EventSchema = schema.maybe(
server_uuid: ecsString(),
task: schema.maybe(
schema.object({
+ id: ecsString(),
scheduled: ecsDate(),
schedule_delay: ecsNumber(),
})
@@ -138,6 +139,13 @@ export const EventSchema = schema.maybe(
),
})
),
+ reporting: schema.maybe(
+ schema.object({
+ id: ecsString(),
+ jobType: ecsString(),
+ byteSize: ecsNumber(),
+ })
+ ),
saved_objects: schema.maybe(
schema.arrayOf(
schema.object({
diff --git a/x-pack/plugins/event_log/scripts/mappings.js b/x-pack/plugins/event_log/scripts/mappings.js
index 231cc225f7c471..d96cb8f1d6dfc1 100644
--- a/x-pack/plugins/event_log/scripts/mappings.js
+++ b/x-pack/plugins/event_log/scripts/mappings.js
@@ -20,6 +20,9 @@ exports.EcsCustomPropertyMappings = {
// task specific fields
task: {
properties: {
+ id: {
+ type: 'keyword',
+ },
scheduled: {
type: 'date',
},
@@ -85,6 +88,20 @@ exports.EcsCustomPropertyMappings = {
},
},
},
+ // reporting specific fields
+ reporting: {
+ properties: {
+ id: {
+ type: 'keyword',
+ },
+ jobType: {
+ type: 'keyword',
+ },
+ byteSize: {
+ type: 'long',
+ },
+ },
+ },
// array of saved object references, for "linking" via search
saved_objects: {
type: 'nested',
diff --git a/x-pack/plugins/fleet/common/constants/routes.ts b/x-pack/plugins/fleet/common/constants/routes.ts
index 69363f37d33e04..81108b15f4aa14 100644
--- a/x-pack/plugins/fleet/common/constants/routes.ts
+++ b/x-pack/plugins/fleet/common/constants/routes.ts
@@ -7,6 +7,8 @@
// Base API paths
+export const INTERNAL_ROOT = `/internal/fleet`;
+
export const API_ROOT = `/api/fleet`;
export const EPM_API_ROOT = `${API_ROOT}/epm`;
export const DATA_STREAM_API_ROOT = `${API_ROOT}/data_streams`;
@@ -133,4 +135,6 @@ export const INSTALL_SCRIPT_API_ROUTES = `${API_ROOT}/install/{osType}`;
// Policy preconfig API routes
export const PRECONFIGURATION_API_ROUTES = {
UPDATE_PATTERN: `${API_ROOT}/setup/preconfiguration`,
+ RESET_PATTERN: `${INTERNAL_ROOT}/reset_preconfigured_agent_policies`,
+ RESET_ONE_PATTERN: `${INTERNAL_ROOT}/reset_preconfigured_agent_policies/{agentPolicyId}`,
};
diff --git a/x-pack/plugins/fleet/server/index.ts b/x-pack/plugins/fleet/server/index.ts
index 8cbfa311081d23..f66bf00a5a054c 100644
--- a/x-pack/plugins/fleet/server/index.ts
+++ b/x-pack/plugins/fleet/server/index.ts
@@ -22,6 +22,7 @@ export type {
AgentClient,
ESIndexPatternService,
PackageService,
+ PackageClient,
AgentPolicyServiceInterface,
ArtifactsClientInterface,
Artifact,
diff --git a/x-pack/plugins/fleet/server/integration_tests/reset_preconfiguration.test.ts b/x-pack/plugins/fleet/server/integration_tests/reset_preconfiguration.test.ts
new file mode 100644
index 00000000000000..4096035f840e08
--- /dev/null
+++ b/x-pack/plugins/fleet/server/integration_tests/reset_preconfiguration.test.ts
@@ -0,0 +1,236 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import Path from 'path';
+
+import * as kbnTestServer from 'src/core/test_helpers/kbn_server';
+
+import type { AgentPolicySOAttributes } from '../types';
+
+const logFilePath = Path.join(__dirname, 'logs.log');
+
+type Root = ReturnType;
+
+const waitForFleetSetup = async (root: Root) => {
+ const isFleetSetupRunning = async () => {
+ const statusApi = kbnTestServer.getSupertest(root, 'get', '/api/status');
+ const resp = await statusApi.send();
+ const fleetStatus = resp.body?.status?.plugins?.fleet;
+ if (fleetStatus?.meta?.error) {
+ throw new Error(`Setup failed: ${JSON.stringify(fleetStatus)}`);
+ }
+
+ return !fleetStatus || fleetStatus?.summary === 'Fleet is setting up';
+ };
+
+ while (await isFleetSetupRunning()) {
+ await new Promise((resolve) => setTimeout(resolve, 2000));
+ }
+};
+
+describe('Fleet preconfiguration rest', () => {
+ let esServer: kbnTestServer.TestElasticsearchUtils;
+ let kbnServer: kbnTestServer.TestKibanaUtils;
+
+ const startServers = async () => {
+ const { startES } = kbnTestServer.createTestServers({
+ adjustTimeout: (t) => jest.setTimeout(t),
+ settings: {
+ es: {
+ license: 'trial',
+ },
+ kbn: {},
+ },
+ });
+
+ esServer = await startES();
+ const startKibana = async () => {
+ const root = kbnTestServer.createRootWithCorePlugins(
+ {
+ xpack: {
+ fleet: {
+ agentPolicies: [
+ {
+ name: 'Elastic Cloud agent policy 0001',
+ description: 'Default agent policy for agents hosted on Elastic Cloud',
+ is_default: false,
+ is_managed: true,
+ id: 'test-12345',
+ namespace: 'default',
+ monitoring_enabled: [],
+ package_policies: [
+ {
+ name: 'fleet_server123456789',
+ package: {
+ name: 'fleet_server',
+ },
+ inputs: [
+ {
+ type: 'fleet-server',
+ keep_enabled: true,
+ vars: [
+ {
+ name: 'host',
+ value: '127.0.0.1',
+ frozen: true,
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ },
+ logging: {
+ appenders: {
+ file: {
+ type: 'file',
+ fileName: logFilePath,
+ layout: {
+ type: 'json',
+ },
+ },
+ },
+ loggers: [
+ {
+ name: 'root',
+ appenders: ['file'],
+ },
+ {
+ name: 'plugins.fleet',
+ level: 'all',
+ },
+ ],
+ },
+ },
+ { oss: false }
+ );
+
+ await root.preboot();
+ const coreSetup = await root.setup();
+ const coreStart = await root.start();
+
+ return {
+ root,
+ coreSetup,
+ coreStart,
+ stop: async () => await root.shutdown(),
+ };
+ };
+ kbnServer = await startKibana();
+ await waitForFleetSetup(kbnServer.root);
+ };
+
+ const stopServers = async () => {
+ if (kbnServer) {
+ await kbnServer.stop();
+ }
+
+ if (esServer) {
+ await esServer.stop();
+ }
+
+ await new Promise((res) => setTimeout(res, 10000));
+ };
+
+ beforeEach(async () => {
+ await startServers();
+ });
+
+ afterEach(async () => {
+ await stopServers();
+ });
+
+ describe('Reset all policy', () => {
+ it('Works and reset all preconfigured policies', async () => {
+ const resetAPI = kbnTestServer.getSupertest(
+ kbnServer.root,
+ 'post',
+ '/internal/fleet/reset_preconfigured_agent_policies'
+ );
+ await resetAPI.set('kbn-sxrf', 'xx').send();
+
+ const agentPolicies = await kbnServer.coreStart.savedObjects
+ .createInternalRepository()
+ .find({
+ type: 'ingest-agent-policies',
+ perPage: 10000,
+ });
+ expect(agentPolicies.saved_objects).toHaveLength(1);
+ expect(agentPolicies.saved_objects.map((ap) => ap.attributes)).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({
+ name: 'Elastic Cloud agent policy 0001',
+ }),
+ ])
+ );
+ });
+ });
+
+ describe('Reset one preconfigured policy', () => {
+ const POLICY_ID = 'test-12345';
+
+ it('Works and reset one preconfigured policies if the policy is already deleted (with a ghost package policy)', async () => {
+ const soClient = kbnServer.coreStart.savedObjects.createInternalRepository();
+
+ await soClient.delete('ingest-agent-policies', POLICY_ID);
+
+ const resetAPI = kbnTestServer.getSupertest(
+ kbnServer.root,
+ 'post',
+ '/internal/fleet/reset_preconfigured_agent_policies/test-12345'
+ );
+ await resetAPI.set('kbn-sxrf', 'xx').send();
+
+ const agentPolicies = await kbnServer.coreStart.savedObjects
+ .createInternalRepository()
+ .find({
+ type: 'ingest-agent-policies',
+ perPage: 10000,
+ });
+ expect(agentPolicies.saved_objects).toHaveLength(1);
+ expect(agentPolicies.saved_objects.map((ap) => ap.attributes)).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({
+ name: 'Elastic Cloud agent policy 0001',
+ }),
+ ])
+ );
+ });
+
+ it('Works if the preconfigured policies already exists with a missing package policy', async () => {
+ const soClient = kbnServer.coreStart.savedObjects.createInternalRepository();
+
+ await soClient.update('ingest-agent-policies', POLICY_ID, {
+ package_policies: [],
+ });
+
+ const resetAPI = kbnTestServer.getSupertest(
+ kbnServer.root,
+ 'post',
+ '/internal/fleet/reset_preconfigured_agent_policies/test-12345'
+ );
+ await resetAPI.set('kbn-sxrf', 'xx').send();
+
+ const agentPolicies = await soClient.find({
+ type: 'ingest-agent-policies',
+ perPage: 10000,
+ });
+ expect(agentPolicies.saved_objects).toHaveLength(1);
+ expect(agentPolicies.saved_objects.map((ap) => ap.attributes)).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({
+ name: 'Elastic Cloud agent policy 0001',
+ package_policies: expect.arrayContaining([expect.stringMatching(/.*/)]),
+ }),
+ ])
+ );
+ });
+ });
+});
diff --git a/x-pack/plugins/fleet/server/mocks/index.ts b/x-pack/plugins/fleet/server/mocks/index.ts
index 7e47c8b59ac7ae..684f84a9b48a5d 100644
--- a/x-pack/plugins/fleet/server/mocks/index.ts
+++ b/x-pack/plugins/fleet/server/mocks/index.ts
@@ -18,12 +18,13 @@ import { licensingMock } from '../../../../plugins/licensing/server/mocks';
import { encryptedSavedObjectsMock } from '../../../encrypted_saved_objects/server/mocks';
import { securityMock } from '../../../security/server/mocks';
import type { PackagePolicyServiceInterface } from '../services/package_policy';
-import type { AgentPolicyServiceInterface, PackageService } from '../services';
+import type { AgentPolicyServiceInterface } from '../services';
import type { FleetAppContext } from '../plugin';
import { createMockTelemetryEventsSender } from '../telemetry/__mocks__';
import { createFleetAuthzMock } from '../../common';
import { agentServiceMock } from '../services/agents/agent_service.mock';
import type { FleetRequestHandlerContext } from '../types';
+import { packageServiceMock } from '../services/epm/package_service.mock';
// Export all mocks from artifacts
export * from '../services/artifacts/mocks';
@@ -142,9 +143,7 @@ export const createMockAgentService = () => agentServiceMock.create();
*/
export const createMockAgentClient = () => agentServiceMock.createClient();
-export const createMockPackageService = (): PackageService => {
- return {
- getInstallation: jest.fn(),
- ensureInstalledPackage: jest.fn(),
- };
-};
+/**
+ * Creates a mock PackageService
+ */
+export const createMockPackageService = () => packageServiceMock.create();
diff --git a/x-pack/plugins/fleet/server/plugin.ts b/x-pack/plugins/fleet/server/plugin.ts
index d719d9d33fa791..51802c96791b14 100644
--- a/x-pack/plugins/fleet/server/plugin.ts
+++ b/x-pack/plugins/fleet/server/plugin.ts
@@ -19,6 +19,7 @@ import type {
KibanaRequest,
ServiceStatus,
ElasticsearchClient,
+ SavedObjectsClientContract,
} from 'kibana/server';
import type { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
@@ -80,15 +81,14 @@ import {
agentPolicyService,
packagePolicyService,
AgentServiceImpl,
+ PackageServiceImpl,
} from './services';
import { registerFleetUsageCollector } from './collectors/register';
-import { getInstallation, ensureInstalledPackage } from './services/epm/packages';
import { getAuthzFromRequest, makeRouterWithFleetAuthz } from './routes/security';
import { FleetArtifactsClient } from './services/artifacts';
import type { FleetRouter } from './types/request_context';
import { TelemetryEventsSender } from './telemetry/sender';
import { setupFleet } from './services/setup';
-import { fetchFindLatestPackage } from './services/epm/registry';
export interface FleetSetupDeps {
licensing: LicensingPluginSetup;
@@ -170,8 +170,6 @@ export interface FleetStartContract {
* @param packageName
*/
createArtifactsClient: (packageName: string) => FleetArtifactsClient;
-
- fetchFindLatestPackage: typeof fetchFindLatestPackage;
}
export class FleetPlugin
@@ -193,6 +191,7 @@ export class FleetPlugin
private readonly fleetStatus$: BehaviorSubject;
private agentService?: AgentService;
+ private packageService?: PackageService;
constructor(private readonly initializerContext: PluginInitializerContext) {
this.config$ = this.initializerContext.config.create();
@@ -407,10 +406,10 @@ export class FleetPlugin
},
fleetSetupCompleted: () => fleetSetupPromise,
esIndexPatternService: new ESIndexPatternSavedObjectService(),
- packageService: {
- getInstallation,
- ensureInstalledPackage,
- },
+ packageService: this.setupPackageService(
+ core.elasticsearch.client.asInternalUser,
+ new SavedObjectsClient(core.savedObjects.createInternalRepository())
+ ),
agentService: this.setupAgentService(core.elasticsearch.client.asInternalUser),
agentPolicyService: {
get: agentPolicyService.get,
@@ -426,7 +425,6 @@ export class FleetPlugin
createArtifactsClient(packageName: string) {
return new FleetArtifactsClient(core.elasticsearch.client.asInternalUser, packageName);
},
- fetchFindLatestPackage,
};
}
@@ -445,4 +443,28 @@ export class FleetPlugin
this.agentService = new AgentServiceImpl(internalEsClient);
return this.agentService;
}
+
+ private setupPackageService(
+ internalEsClient: ElasticsearchClient,
+ internalSoClient: SavedObjectsClientContract
+ ): PackageService {
+ if (this.packageService) {
+ return this.packageService;
+ }
+
+ this.packageService = new PackageServiceImpl(
+ internalEsClient,
+ internalSoClient,
+ this.getLogger()
+ );
+ return this.packageService;
+ }
+
+ private getLogger(): Logger {
+ if (!this.logger) {
+ this.logger = this.initializerContext.logger.get();
+ }
+
+ return this.logger;
+ }
}
diff --git a/x-pack/plugins/fleet/server/routes/preconfiguration/handler.ts b/x-pack/plugins/fleet/server/routes/preconfiguration/handler.ts
new file mode 100644
index 00000000000000..6e2e320db322e7
--- /dev/null
+++ b/x-pack/plugins/fleet/server/routes/preconfiguration/handler.ts
@@ -0,0 +1,77 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import type { TypeOf } from '@kbn/config-schema';
+
+import type { PreconfiguredAgentPolicy } from '../../../common';
+
+import type { FleetRequestHandler } from '../../types';
+import type {
+ PutPreconfigurationSchema,
+ PostResetOnePreconfiguredAgentPolicies,
+} from '../../types';
+import { defaultIngestErrorHandler } from '../../errors';
+import { ensurePreconfiguredPackagesAndPolicies, outputService } from '../../services';
+import { resetPreconfiguredAgentPolicies } from '../../services/preconfiguration/index';
+
+export const updatePreconfigurationHandler: FleetRequestHandler<
+ undefined,
+ undefined,
+ TypeOf
+> = async (context, request, response) => {
+ const soClient = context.core.savedObjects.client;
+ const esClient = context.core.elasticsearch.client.asInternalUser;
+ const defaultOutput = await outputService.ensureDefaultOutput(soClient);
+ const spaceId = context.fleet.spaceId;
+ const { agentPolicies, packages } = request.body;
+
+ try {
+ const body = await ensurePreconfiguredPackagesAndPolicies(
+ soClient,
+ esClient,
+ (agentPolicies as PreconfiguredAgentPolicy[]) ?? [],
+ packages ?? [],
+ defaultOutput,
+ spaceId
+ );
+ return response.ok({ body });
+ } catch (error) {
+ return defaultIngestErrorHandler({ error, response });
+ }
+};
+
+export const resetPreconfigurationHandler: FleetRequestHandler<
+ TypeOf,
+ undefined,
+ undefined
+> = async (context, request, response) => {
+ const soClient = context.core.savedObjects.client;
+ const esClient = context.core.elasticsearch.client.asInternalUser;
+
+ try {
+ await resetPreconfiguredAgentPolicies(soClient, esClient, request.params.agentPolicyid);
+ return response.ok({});
+ } catch (error) {
+ return defaultIngestErrorHandler({ error, response });
+ }
+};
+
+export const resetOnePreconfigurationHandler: FleetRequestHandler<
+ undefined,
+ undefined,
+ undefined
+> = async (context, request, response) => {
+ const soClient = context.core.savedObjects.client;
+ const esClient = context.core.elasticsearch.client.asInternalUser;
+
+ try {
+ await resetPreconfiguredAgentPolicies(soClient, esClient);
+ return response.ok({});
+ } catch (error) {
+ return defaultIngestErrorHandler({ error, response });
+ }
+};
diff --git a/x-pack/plugins/fleet/server/routes/preconfiguration/index.ts b/x-pack/plugins/fleet/server/routes/preconfiguration/index.ts
index 56cbbc9435a57f..ec904e64a18dec 100644
--- a/x-pack/plugins/fleet/server/routes/preconfiguration/index.ts
+++ b/x-pack/plugins/fleet/server/routes/preconfiguration/index.ts
@@ -5,45 +5,38 @@
* 2.0.
*/
-import type { RequestHandler } from 'src/core/server';
-import type { TypeOf } from '@kbn/config-schema';
-
-import type { PreconfiguredAgentPolicy } from '../../../common';
-
import { PRECONFIGURATION_API_ROUTES } from '../../constants';
-import type { FleetRequestHandler } from '../../types';
import { PutPreconfigurationSchema } from '../../types';
-import { defaultIngestErrorHandler } from '../../errors';
-import { ensurePreconfiguredPackagesAndPolicies, outputService } from '../../services';
import type { FleetAuthzRouter } from '../security';
-export const updatePreconfigurationHandler: FleetRequestHandler<
- undefined,
- undefined,
- TypeOf
-> = async (context, request, response) => {
- const soClient = context.core.savedObjects.client;
- const esClient = context.core.elasticsearch.client.asInternalUser;
- const defaultOutput = await outputService.ensureDefaultOutput(soClient);
- const spaceId = context.fleet.spaceId;
- const { agentPolicies, packages } = request.body;
-
- try {
- const body = await ensurePreconfiguredPackagesAndPolicies(
- soClient,
- esClient,
- (agentPolicies as PreconfiguredAgentPolicy[]) ?? [],
- packages ?? [],
- defaultOutput,
- spaceId
- );
- return response.ok({ body });
- } catch (error) {
- return defaultIngestErrorHandler({ error, response });
- }
-};
+import {
+ updatePreconfigurationHandler,
+ resetPreconfigurationHandler,
+ resetOnePreconfigurationHandler,
+} from './handler';
export const registerRoutes = (router: FleetAuthzRouter) => {
+ router.post(
+ {
+ path: PRECONFIGURATION_API_ROUTES.RESET_PATTERN,
+ validate: false,
+ fleetAuthz: {
+ fleet: { all: true },
+ },
+ },
+ resetPreconfigurationHandler
+ );
+ router.post(
+ {
+ path: PRECONFIGURATION_API_ROUTES.RESET_ONE_PATTERN,
+ validate: false,
+ fleetAuthz: {
+ fleet: { all: true },
+ },
+ },
+ resetOnePreconfigurationHandler
+ );
+
router.put(
{
path: PRECONFIGURATION_API_ROUTES.UPDATE_PATTERN,
@@ -52,6 +45,6 @@ export const registerRoutes = (router: FleetAuthzRouter) => {
fleet: { all: true },
},
},
- updatePreconfigurationHandler as RequestHandler
+ updatePreconfigurationHandler
);
};
diff --git a/x-pack/plugins/fleet/server/services/agent_policy.ts b/x-pack/plugins/fleet/server/services/agent_policy.ts
index 20fb4e3c73e77a..041b0a45643e04 100644
--- a/x-pack/plugins/fleet/server/services/agent_policy.ts
+++ b/x-pack/plugins/fleet/server/services/agent_policy.ts
@@ -618,22 +618,23 @@ class AgentPolicyService {
public async delete(
soClient: SavedObjectsClientContract,
esClient: ElasticsearchClient,
- id: string
+ id: string,
+ options?: { force?: boolean; removeFleetServerDocuments?: boolean }
): Promise {
const agentPolicy = await this.get(soClient, id, false);
if (!agentPolicy) {
throw new Error('Agent policy not found');
}
- if (agentPolicy.is_managed) {
+ if (agentPolicy.is_managed && !options?.force) {
throw new HostedAgentPolicyRestrictionRelatedError(`Cannot delete hosted agent policy ${id}`);
}
- if (agentPolicy.is_default) {
+ if (agentPolicy.is_default && !options?.force) {
throw new Error('The default agent policy cannot be deleted');
}
- if (agentPolicy.is_default_fleet_server) {
+ if (agentPolicy.is_default_fleet_server && !options?.force) {
throw new Error('The default fleet server agent policy cannot be deleted');
}
@@ -655,6 +656,7 @@ class AgentPolicyService {
esClient,
agentPolicy.package_policies as string[],
{
+ force: options?.force,
skipUnassignFromAgentPolicies: true,
}
);
@@ -667,7 +669,7 @@ class AgentPolicyService {
}
}
- if (agentPolicy.is_preconfigured) {
+ if (agentPolicy.is_preconfigured && !options?.force) {
await soClient.create(PRECONFIGURATION_DELETION_RECORD_SAVED_OBJECT_TYPE, {
id: String(id),
});
@@ -675,6 +677,11 @@ class AgentPolicyService {
await soClient.delete(SAVED_OBJECT_TYPE, id);
await this.triggerAgentPolicyUpdatedEvent(soClient, esClient, 'deleted', id);
+
+ if (options?.removeFleetServerDocuments) {
+ this.deleteFleetServerPoliciesForPolicyId(esClient, id);
+ }
+
return {
id,
name: agentPolicy.name,
@@ -720,6 +727,23 @@ class AgentPolicyService {
});
}
+ public async deleteFleetServerPoliciesForPolicyId(
+ esClient: ElasticsearchClient,
+ agentPolicyId: string
+ ) {
+ await esClient.deleteByQuery({
+ index: AGENT_POLICY_INDEX,
+ ignore_unavailable: true,
+ body: {
+ query: {
+ term: {
+ policy_id: agentPolicyId,
+ },
+ },
+ },
+ });
+ }
+
public async getLatestFleetPolicy(esClient: ElasticsearchClient, agentPolicyId: string) {
const res = await esClient.search({
index: AGENT_POLICY_INDEX,
diff --git a/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key.ts b/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key.ts
index 568a8e45e1dffc..76e2d02970de09 100644
--- a/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key.ts
+++ b/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key.ts
@@ -98,21 +98,33 @@ export async function getEnrollmentAPIKey(
* Invalidate an api key and mark it as inactive
* @param id
*/
-export async function deleteEnrollmentApiKey(esClient: ElasticsearchClient, id: string) {
+export async function deleteEnrollmentApiKey(
+ esClient: ElasticsearchClient,
+ id: string,
+ forceDelete = false
+) {
const enrollmentApiKey = await getEnrollmentAPIKey(esClient, id);
await invalidateAPIKeys([enrollmentApiKey.api_key_id]);
- await esClient.update({
- index: ENROLLMENT_API_KEYS_INDEX,
- id,
- body: {
- doc: {
- active: false,
+ if (forceDelete) {
+ await esClient.delete({
+ index: ENROLLMENT_API_KEYS_INDEX,
+ id,
+ refresh: 'wait_for',
+ });
+ } else {
+ await esClient.update({
+ index: ENROLLMENT_API_KEYS_INDEX,
+ id,
+ body: {
+ doc: {
+ active: false,
+ },
},
- },
- refresh: 'wait_for',
- });
+ refresh: 'wait_for',
+ });
+ }
}
export async function deleteEnrollmentApiKeyForAgentPolicyId(
diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/transform/install.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/transform/install.ts
index 197d463797cac8..144f994646fe9c 100644
--- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/transform/install.ts
+++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/transform/install.ts
@@ -112,7 +112,7 @@ export const installTransform = async (
return installedTransforms;
};
-const isTransform = (path: string) => {
+export const isTransform = (path: string) => {
const pathParts = getPathParts(path);
return !path.endsWith('/') && pathParts.type === ElasticsearchAssetType.transform;
};
diff --git a/x-pack/plugins/fleet/server/services/epm/index.ts b/x-pack/plugins/fleet/server/services/epm/index.ts
new file mode 100644
index 00000000000000..f9d026dd0e0928
--- /dev/null
+++ b/x-pack/plugins/fleet/server/services/epm/index.ts
@@ -0,0 +1,10 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+export { PackageServiceImpl } from './package_service';
+
+export type { PackageService, PackageClient } from './package_service';
diff --git a/x-pack/plugins/fleet/server/services/epm/package_service.mock.ts b/x-pack/plugins/fleet/server/services/epm/package_service.mock.ts
new file mode 100644
index 00000000000000..f703399ca6df79
--- /dev/null
+++ b/x-pack/plugins/fleet/server/services/epm/package_service.mock.ts
@@ -0,0 +1,26 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import type { PackageClient, PackageService } from './package_service';
+
+const createClientMock = (): jest.Mocked => ({
+ getInstallation: jest.fn(),
+ ensureInstalledPackage: jest.fn(),
+ fetchFindLatestPackage: jest.fn(),
+ getRegistryPackage: jest.fn(),
+ reinstallEsAssets: jest.fn(),
+});
+
+const createServiceMock = (): PackageService => ({
+ asScoped: jest.fn(createClientMock),
+ asInternalUser: createClientMock(),
+});
+
+export const packageServiceMock = {
+ createClient: createClientMock,
+ create: createServiceMock,
+};
diff --git a/x-pack/plugins/fleet/server/services/epm/package_service.test.ts b/x-pack/plugins/fleet/server/services/epm/package_service.test.ts
new file mode 100644
index 00000000000000..fb92b341928da4
--- /dev/null
+++ b/x-pack/plugins/fleet/server/services/epm/package_service.test.ts
@@ -0,0 +1,206 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+jest.mock('../../routes/security');
+
+import type { MockedLogger } from '@kbn/logging/target_types/mocks';
+
+import type {
+ ElasticsearchClient,
+ SavedObjectsClientContract,
+} from '../../../../../../src/core/server';
+import {
+ elasticsearchServiceMock,
+ httpServerMock,
+ loggingSystemMock,
+ savedObjectsClientMock,
+} from '../../../../../../src/core/server/mocks';
+
+import { FleetUnauthorizedError } from '../../errors';
+import type { InstallablePackage } from '../../types';
+
+import type { PackageClient, PackageService } from './package_service';
+import { PackageServiceImpl } from './package_service';
+import * as epmPackagesGet from './packages/get';
+import * as epmPackagesInstall from './packages/install';
+import * as epmRegistry from './registry';
+import * as epmTransformsInstall from './elasticsearch/transform/install';
+
+const testKeys = [
+ 'getInstallation',
+ 'ensureInstalledPackage',
+ 'fetchFindLatestPackage',
+ 'getRegistryPackage',
+ 'reinstallEsAssets',
+];
+
+function getTest(
+ mocks: {
+ packageClient: PackageClient;
+ esClient?: ElasticsearchClient;
+ soClient?: SavedObjectsClientContract;
+ logger?: MockedLogger;
+ },
+ testKey: string
+) {
+ let test: {
+ method: Function;
+ args: any[];
+ spy: jest.SpyInstance;
+ spyArgs: any[];
+ spyResponse: any;
+ };
+
+ switch (testKey) {
+ case testKeys[0]:
+ test = {
+ method: mocks.packageClient.getInstallation.bind(mocks.packageClient),
+ args: ['package name'],
+ spy: jest.spyOn(epmPackagesGet, 'getInstallation'),
+ spyArgs: [
+ {
+ pkgName: 'package name',
+ savedObjectsClient: mocks.soClient,
+ },
+ ],
+ spyResponse: { name: 'getInstallation test' },
+ };
+ break;
+ case testKeys[1]:
+ test = {
+ method: mocks.packageClient.ensureInstalledPackage.bind(mocks.packageClient),
+ args: [{ pkgName: 'package name', pkgVersion: '8.0.0', spaceId: 'spaceId' }],
+ spy: jest.spyOn(epmPackagesInstall, 'ensureInstalledPackage'),
+ spyArgs: [
+ {
+ pkgName: 'package name',
+ pkgVersion: '8.0.0',
+ spaceId: 'spaceId',
+ esClient: mocks.esClient,
+ savedObjectsClient: mocks.soClient,
+ },
+ ],
+ spyResponse: { name: 'ensureInstalledPackage test' },
+ };
+ break;
+ case testKeys[2]:
+ test = {
+ method: mocks.packageClient.fetchFindLatestPackage.bind(mocks.packageClient),
+ args: ['package name'],
+ spy: jest.spyOn(epmRegistry, 'fetchFindLatestPackage'),
+ spyArgs: ['package name'],
+ spyResponse: { name: 'fetchFindLatestPackage test' },
+ };
+ break;
+ case testKeys[3]:
+ test = {
+ method: mocks.packageClient.getRegistryPackage.bind(mocks.packageClient),
+ args: ['package name', '8.0.0'],
+ spy: jest.spyOn(epmRegistry, 'getRegistryPackage'),
+ spyArgs: ['package name', '8.0.0'],
+ spyResponse: {
+ packageInfo: { name: 'getRegistryPackage test' },
+ paths: ['/some/test/path'],
+ },
+ };
+ break;
+ case testKeys[4]:
+ const pkg: InstallablePackage = {
+ format_version: '1.0.0',
+ name: 'package name',
+ title: 'package title',
+ description: 'package description',
+ version: '8.0.0',
+ release: 'ga',
+ owner: { github: 'elastic' },
+ };
+ const paths = ['some/test/transform/path'];
+
+ test = {
+ method: mocks.packageClient.reinstallEsAssets.bind(mocks.packageClient),
+ args: [pkg, paths],
+ spy: jest.spyOn(epmTransformsInstall, 'installTransform'),
+ spyArgs: [pkg, paths, mocks.esClient, mocks.soClient, mocks.logger],
+ spyResponse: [
+ {
+ name: 'package name',
+ },
+ ],
+ };
+ break;
+ default:
+ throw new Error('invalid test key');
+ }
+
+ return test;
+}
+
+describe('PackageService', () => {
+ let mockPackageService: PackageService;
+ let mockEsClient: ElasticsearchClient;
+ let mockSoClient: SavedObjectsClientContract;
+ let mockLogger: MockedLogger;
+
+ beforeEach(() => {
+ mockEsClient = elasticsearchServiceMock.createElasticsearchClient();
+ mockSoClient = savedObjectsClientMock.create();
+ mockLogger = loggingSystemMock.createLogger();
+ mockPackageService = new PackageServiceImpl(mockEsClient, mockSoClient, mockLogger);
+ });
+
+ afterEach(() => {
+ jest.resetAllMocks();
+ });
+
+ describe('asScoped', () => {
+ describe.each(testKeys)('without required privileges', (testKey: string) => {
+ const unauthError = new FleetUnauthorizedError(
+ `User does not have adequate permissions to access Fleet packages.`
+ );
+
+ it(`rejects on ${testKey}`, async () => {
+ const { method, args } = getTest(
+ { packageClient: mockPackageService.asScoped(httpServerMock.createKibanaRequest()) },
+ testKey
+ );
+ await expect(method(...args)).rejects.toThrowError(unauthError);
+ });
+ });
+
+ describe.each(testKeys)('with required privileges', (testKey: string) => {
+ it(`calls ${testKey} and returns results`, async () => {
+ const mockClients = {
+ packageClient: mockPackageService.asInternalUser,
+ esClient: mockEsClient,
+ soClient: mockSoClient,
+ logger: mockLogger,
+ };
+ const { method, args, spy, spyArgs, spyResponse } = getTest(mockClients, testKey);
+ spy.mockResolvedValue(spyResponse);
+
+ await expect(method(...args)).resolves.toEqual(spyResponse);
+ expect(spy).toHaveBeenCalledWith(...spyArgs);
+ });
+ });
+ });
+
+ describe.each(testKeys)('asInternalUser', (testKey: string) => {
+ it(`calls ${testKey} and returns results`, async () => {
+ const mockClients = {
+ packageClient: mockPackageService.asInternalUser,
+ esClient: mockEsClient,
+ soClient: mockSoClient,
+ logger: mockLogger,
+ };
+ const { method, args, spy, spyArgs, spyResponse } = getTest(mockClients, testKey);
+ spy.mockResolvedValue(spyResponse);
+
+ await expect(method(...args)).resolves.toEqual(spyResponse);
+ expect(spy).toHaveBeenCalledWith(...spyArgs);
+ });
+ });
+});
diff --git a/x-pack/plugins/fleet/server/services/epm/package_service.ts b/x-pack/plugins/fleet/server/services/epm/package_service.ts
new file mode 100644
index 00000000000000..0d9b8cb74b5036
--- /dev/null
+++ b/x-pack/plugins/fleet/server/services/epm/package_service.ts
@@ -0,0 +1,164 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+/* eslint-disable max-classes-per-file */
+
+import type {
+ KibanaRequest,
+ ElasticsearchClient,
+ SavedObjectsClientContract,
+ Logger,
+} from 'kibana/server';
+
+import type {
+ EsAssetReference,
+ InstallablePackage,
+ Installation,
+ RegistryPackage,
+ RegistrySearchResult,
+} from '../../types';
+import { checkSuperuser } from '../../routes/security';
+import { FleetUnauthorizedError } from '../../errors';
+
+import { installTransform, isTransform } from './elasticsearch/transform/install';
+import { fetchFindLatestPackage, getRegistryPackage } from './registry';
+import { ensureInstalledPackage, getInstallation } from './packages';
+
+export type InstalledAssetType = EsAssetReference;
+
+export interface PackageService {
+ asScoped(request: KibanaRequest): PackageClient;
+ asInternalUser: PackageClient;
+}
+
+export interface PackageClient {
+ getInstallation(pkgName: string): Promise;
+
+ ensureInstalledPackage(options: {
+ pkgName: string;
+ pkgVersion?: string;
+ spaceId?: string;
+ }): Promise;
+
+ fetchFindLatestPackage(packageName: string): Promise;
+
+ getRegistryPackage(
+ packageName: string,
+ packageVersion: string
+ ): Promise<{ packageInfo: RegistryPackage; paths: string[] }>;
+
+ reinstallEsAssets(
+ packageInfo: InstallablePackage,
+ assetPaths: string[]
+ ): Promise;
+}
+
+export class PackageServiceImpl implements PackageService {
+ constructor(
+ private readonly internalEsClient: ElasticsearchClient,
+ private readonly internalSoClient: SavedObjectsClientContract,
+ private readonly logger: Logger
+ ) {}
+
+ public asScoped(request: KibanaRequest) {
+ const preflightCheck = () => {
+ if (!checkSuperuser(request)) {
+ throw new FleetUnauthorizedError(
+ `User does not have adequate permissions to access Fleet packages.`
+ );
+ }
+ };
+
+ return new PackageClientImpl(
+ this.internalEsClient,
+ this.internalSoClient,
+ this.logger,
+ preflightCheck
+ );
+ }
+
+ public get asInternalUser() {
+ return new PackageClientImpl(this.internalEsClient, this.internalSoClient, this.logger);
+ }
+}
+
+class PackageClientImpl implements PackageClient {
+ constructor(
+ private readonly internalEsClient: ElasticsearchClient,
+ private readonly internalSoClient: SavedObjectsClientContract,
+ private readonly logger: Logger,
+ private readonly preflightCheck?: () => void | Promise
+ ) {}
+
+ public async getInstallation(pkgName: string) {
+ await this.#runPreflight();
+ return getInstallation({
+ pkgName,
+ savedObjectsClient: this.internalSoClient,
+ });
+ }
+
+ public async ensureInstalledPackage(options: {
+ pkgName: string;
+ pkgVersion?: string;
+ spaceId?: string;
+ }): Promise {
+ await this.#runPreflight();
+ return ensureInstalledPackage({
+ ...options,
+ esClient: this.internalEsClient,
+ savedObjectsClient: this.internalSoClient,
+ });
+ }
+
+ public async fetchFindLatestPackage(packageName: string) {
+ await this.#runPreflight();
+ return fetchFindLatestPackage(packageName);
+ }
+
+ public async getRegistryPackage(packageName: string, packageVersion: string) {
+ await this.#runPreflight();
+ return getRegistryPackage(packageName, packageVersion);
+ }
+
+ public async reinstallEsAssets(
+ packageInfo: InstallablePackage,
+ assetPaths: string[]
+ ): Promise {
+ await this.#runPreflight();
+ let installedAssets: InstalledAssetType[] = [];
+
+ const transformPaths = assetPaths.filter(isTransform);
+
+ if (transformPaths.length !== assetPaths.length) {
+ throw new Error('reinstallEsAssets is currently only implemented for transform assets');
+ }
+
+ if (transformPaths.length) {
+ const installedTransformAssets = await this.#reinstallTransforms(packageInfo, transformPaths);
+ installedAssets = [...installedAssets, ...installedTransformAssets];
+ }
+
+ return installedAssets;
+ }
+
+ #reinstallTransforms(packageInfo: InstallablePackage, paths: string[]) {
+ return installTransform(
+ packageInfo,
+ paths,
+ this.internalEsClient,
+ this.internalSoClient,
+ this.logger
+ );
+ }
+
+ #runPreflight() {
+ if (this.preflightCheck) {
+ return this.preflightCheck();
+ }
+ }
+}
diff --git a/x-pack/plugins/fleet/server/services/index.ts b/x-pack/plugins/fleet/server/services/index.ts
index 7e615f923b2213..41b7d6c4e6ac81 100644
--- a/x-pack/plugins/fleet/server/services/index.ts
+++ b/x-pack/plugins/fleet/server/services/index.ts
@@ -9,7 +9,6 @@ import type { SavedObjectsClientContract } from 'kibana/server';
import type { agentPolicyService } from './agent_policy';
import * as settingsService from './settings';
-import type { getInstallation, ensureInstalledPackage } from './epm/packages';
export { ESIndexPatternSavedObjectService } from './es_index_pattern';
export { getRegistryUrl } from './epm/registry/registry_url';
@@ -29,11 +28,6 @@ export interface ESIndexPatternService {
* Service that provides exported function that return information about EPM packages
*/
-export interface PackageService {
- getInstallation: typeof getInstallation;
- ensureInstalledPackage: typeof ensureInstalledPackage;
-}
-
export interface AgentPolicyServiceInterface {
get: typeof agentPolicyService['get'];
list: typeof agentPolicyService['list'];
@@ -61,3 +55,7 @@ export * from './artifacts';
// Policy preconfiguration functions
export { ensurePreconfiguredPackagesAndPolicies } from './preconfiguration';
+
+// Package Services
+export { PackageServiceImpl } from './epm';
+export type { PackageService, PackageClient } from './epm';
diff --git a/x-pack/plugins/fleet/server/services/preconfiguration/index.ts b/x-pack/plugins/fleet/server/services/preconfiguration/index.ts
new file mode 100644
index 00000000000000..ccd550759337b2
--- /dev/null
+++ b/x-pack/plugins/fleet/server/services/preconfiguration/index.ts
@@ -0,0 +1,8 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+export { resetPreconfiguredAgentPolicies } from './reset_agent_policies';
diff --git a/x-pack/plugins/fleet/server/services/preconfiguration/reset_agent_policies.ts b/x-pack/plugins/fleet/server/services/preconfiguration/reset_agent_policies.ts
new file mode 100644
index 00000000000000..4285b62899d349
--- /dev/null
+++ b/x-pack/plugins/fleet/server/services/preconfiguration/reset_agent_policies.ts
@@ -0,0 +1,149 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import pMap from 'p-map';
+import type { ElasticsearchClient, SavedObjectsClientContract, Logger } from 'src/core/server';
+
+import { appContextService } from '../app_context';
+import { setupFleet } from '../setup';
+import {
+ AGENT_POLICY_SAVED_OBJECT_TYPE,
+ SO_SEARCH_LIMIT,
+ PACKAGE_POLICY_SAVED_OBJECT_TYPE,
+} from '../../constants';
+import { agentPolicyService } from '../agent_policy';
+import { packagePolicyService } from '../package_policy';
+import { getAgentsByKuery, forceUnenrollAgent } from '../agents';
+import { listEnrollmentApiKeys, deleteEnrollmentApiKey } from '../api_keys';
+import type { AgentPolicy } from '../../types';
+
+export async function resetPreconfiguredAgentPolicies(
+ soClient: SavedObjectsClientContract,
+ esClient: ElasticsearchClient,
+ agentPolicyId?: string
+) {
+ const logger = appContextService.getLogger();
+ logger.warn('Reseting Fleet preconfigured agent policies');
+ await _deleteExistingData(soClient, esClient, logger, agentPolicyId);
+ await _deleteGhostPackagePolicies(soClient, esClient, logger);
+
+ await setupFleet(soClient, esClient);
+}
+
+/**
+ * Delete all package policies that are not used in any agent policies
+ */
+async function _deleteGhostPackagePolicies(
+ soClient: SavedObjectsClientContract,
+ esClient: ElasticsearchClient,
+ logger: Logger
+) {
+ const { items: packagePolicies } = await packagePolicyService.list(soClient, {
+ perPage: SO_SEARCH_LIMIT,
+ });
+
+ const policyIds = Array.from(
+ packagePolicies.reduce((acc, packagePolicy) => {
+ acc.add(packagePolicy.policy_id);
+
+ return acc;
+ }, new Set())
+ );
+
+ const objects = policyIds.map((id) => ({ id, type: AGENT_POLICY_SAVED_OBJECT_TYPE }));
+ const agentPolicyExistsMap = (await soClient.bulkGet(objects)).saved_objects.reduce((acc, so) => {
+ if (so.error && so.error.statusCode === 404) {
+ acc.set(so.id, false);
+ } else {
+ acc.set(so.id, true);
+ }
+ return acc;
+ }, new Map());
+
+ await pMap(
+ packagePolicies,
+ (packagePolicy) => {
+ if (agentPolicyExistsMap.get(packagePolicy.policy_id) === false) {
+ logger.info(`Deleting ghost package policy ${packagePolicy.name} (${packagePolicy.id})`);
+ return soClient.delete(PACKAGE_POLICY_SAVED_OBJECT_TYPE, packagePolicy.id);
+ }
+ },
+ {
+ concurrency: 20,
+ }
+ );
+}
+
+async function _deleteExistingData(
+ soClient: SavedObjectsClientContract,
+ esClient: ElasticsearchClient,
+ logger: Logger,
+ agentPolicyId?: string
+) {
+ let existingPolicies: AgentPolicy[];
+
+ if (agentPolicyId) {
+ const policy = await agentPolicyService.get(soClient, agentPolicyId);
+ if (!policy || !policy.is_preconfigured) {
+ throw new Error('Invalid policy');
+ }
+ existingPolicies = [policy];
+ }
+ {
+ existingPolicies = (
+ await agentPolicyService.list(soClient, {
+ perPage: SO_SEARCH_LIMIT,
+ kuery: `${AGENT_POLICY_SAVED_OBJECT_TYPE}.is_preconfigured:true`,
+ })
+ ).items;
+ }
+
+ // unenroll all the agents enroled in this policies
+ const { agents } = await getAgentsByKuery(esClient, {
+ showInactive: false,
+ perPage: SO_SEARCH_LIMIT,
+ kuery: existingPolicies.map((policy) => `policy_id:"${policy.id}"`).join(' or '),
+ });
+
+ // Delete
+ if (agents.length > 0) {
+ logger.info(`Force unenrolling ${agents.length} agents`);
+ await pMap(agents, (agent) => forceUnenrollAgent(soClient, esClient, agent.id), {
+ concurrency: 20,
+ });
+ }
+
+ const { items: enrollmentApiKeys } = await listEnrollmentApiKeys(esClient, {
+ perPage: SO_SEARCH_LIMIT,
+ showInactive: true,
+ });
+
+ if (enrollmentApiKeys.length > 0) {
+ logger.info(`Deleting ${enrollmentApiKeys.length} enrollment api keys`);
+ await pMap(
+ enrollmentApiKeys,
+ (enrollmentKey) => deleteEnrollmentApiKey(esClient, enrollmentKey.id, true),
+ {
+ concurrency: 20,
+ }
+ );
+ }
+ if (existingPolicies.length > 0) {
+ logger.info(`Deleting ${existingPolicies.length} agent policies`);
+ await pMap(
+ existingPolicies,
+ (policy) =>
+ agentPolicyService.delete(soClient, esClient, policy.id, {
+ force: true,
+ removeFleetServerDocuments: true,
+ }),
+ {
+ concurrency: 20,
+ }
+ );
+ }
+}
diff --git a/x-pack/plugins/fleet/server/types/rest_spec/preconfiguration.ts b/x-pack/plugins/fleet/server/types/rest_spec/preconfiguration.ts
index dc802b89f1894c..936469a16100fa 100644
--- a/x-pack/plugins/fleet/server/types/rest_spec/preconfiguration.ts
+++ b/x-pack/plugins/fleet/server/types/rest_spec/preconfiguration.ts
@@ -15,3 +15,9 @@ export const PutPreconfigurationSchema = {
packages: schema.maybe(PreconfiguredPackagesSchema),
}),
};
+
+export const PostResetOnePreconfiguredAgentPolicies = {
+ params: schema.object({
+ agentPolicyid: schema.string(),
+ }),
+};
diff --git a/x-pack/plugins/graph/public/apps/listing_route.tsx b/x-pack/plugins/graph/public/apps/listing_route.tsx
index 4ed0789f33fdf1..dc70d84155bf96 100644
--- a/x-pack/plugins/graph/public/apps/listing_route.tsx
+++ b/x-pack/plugins/graph/public/apps/listing_route.tsx
@@ -102,6 +102,7 @@ export function ListingRoute({
tableListTitle={i18n.translate('xpack.graph.listing.graphsTitle', {
defaultMessage: 'Graphs',
})}
+ theme={coreStart.theme}
/>
);
diff --git a/x-pack/plugins/lens/public/embeddable/embeddable_component.tsx b/x-pack/plugins/lens/public/embeddable/embeddable_component.tsx
index 7f65e50bf44292..e501138648b14e 100644
--- a/x-pack/plugins/lens/public/embeddable/embeddable_component.tsx
+++ b/x-pack/plugins/lens/public/embeddable/embeddable_component.tsx
@@ -6,7 +6,7 @@
*/
import React, { FC, useEffect } from 'react';
-import type { CoreStart } from 'kibana/public';
+import type { CoreStart, ThemeServiceStart } from 'kibana/public';
import type { UiActionsStart } from 'src/plugins/ui_actions/public';
import type { Start as InspectorStartContract } from 'src/plugins/inspector/public';
import { EuiLoadingChart } from '@elastic/eui';
@@ -68,6 +68,7 @@ export function getEmbeddableComponent(core: CoreStart, plugins: PluginsStartDep
const input = { ...props };
const [embeddable, loading, error] = useEmbeddableFactory({ factory, input });
const hasActions = props.withActions === true;
+ const theme = core.theme;
if (loading) {
return ;
@@ -81,6 +82,7 @@ export function getEmbeddableComponent(core: CoreStart, plugins: PluginsStartDep
inspector={inspector}
actionPredicate={() => hasActions}
input={input}
+ theme={theme}
/>
);
}
@@ -95,6 +97,7 @@ interface EmbeddablePanelWrapperProps {
inspector: PluginsStartDependencies['inspector'];
actionPredicate: (id: string) => boolean;
input: EmbeddableComponentProps;
+ theme: ThemeServiceStart;
}
const EmbeddablePanelWrapper: FC = ({
@@ -103,6 +106,7 @@ const EmbeddablePanelWrapper: FC = ({
actionPredicate,
inspector,
input,
+ theme,
}) => {
useEffect(() => {
embeddable.updateInput(input);
@@ -118,6 +122,7 @@ const EmbeddablePanelWrapper: FC = ({
showShadow={false}
showBadges={false}
showNotifications={false}
+ theme={theme}
/>
);
};
diff --git a/x-pack/plugins/maps/public/kibana_services.ts b/x-pack/plugins/maps/public/kibana_services.ts
index ddc2851f595b06..027981de32295e 100644
--- a/x-pack/plugins/maps/public/kibana_services.ts
+++ b/x-pack/plugins/maps/public/kibana_services.ts
@@ -54,6 +54,7 @@ export const getSavedObjectsTagging = () => pluginsStart.savedObjectsTagging;
export const getPresentationUtilContext = () => pluginsStart.presentationUtil.ContextProvider;
export const getSecurityService = () => pluginsStart.security;
export const getSpacesApi = () => pluginsStart.spaces;
+export const getTheme = () => coreStart.theme;
// xpack.maps.* kibana.yml settings from this plugin
let mapAppConfig: MapsConfigType;
diff --git a/x-pack/plugins/maps/public/routes/list_page/maps_list_view.tsx b/x-pack/plugins/maps/public/routes/list_page/maps_list_view.tsx
index 7dc8c9c88d4ca9..571cba64a06c45 100644
--- a/x-pack/plugins/maps/public/routes/list_page/maps_list_view.tsx
+++ b/x-pack/plugins/maps/public/routes/list_page/maps_list_view.tsx
@@ -21,6 +21,7 @@ import {
getSavedObjectsClient,
getSavedObjectsTagging,
getSavedObjects,
+ getTheme,
} from '../../kibana_services';
import { getAppTitle } from '../../../common/i18n_getters';
import { MapSavedObjectAttributes } from '../../../common/map_saved_object_type';
@@ -148,6 +149,7 @@ export function MapsListView() {
tableListTitle={getAppTitle()}
toastNotifications={getToasts()}
searchFilters={searchFilters}
+ theme={getTheme()}
/>
);
}
diff --git a/x-pack/plugins/ml/public/application/trained_models/models_management/pipelines/expanded_row.tsx b/x-pack/plugins/ml/public/application/trained_models/models_management/pipelines/expanded_row.tsx
index 272f3e4346a5ba..e4d18a7f1b0c4b 100644
--- a/x-pack/plugins/ml/public/application/trained_models/models_management/pipelines/expanded_row.tsx
+++ b/x-pack/plugins/ml/public/application/trained_models/models_management/pipelines/expanded_row.tsx
@@ -76,35 +76,42 @@ export const ProcessorsStats: FC = ({ stats }) => {
truncateText: true,
'data-test-subj': 'mlProcessorStatsCount',
},
- {
- field: 'stats.time_in_millis',
- name: (
-
-
-
-
-
-
- }
- />
-
-
- ),
- width: '100px',
- truncateText: false,
- 'data-test-subj': 'mlProcessorStatsTimePerDoc',
- render: (v: number) => {
- return durationFormatter(v);
- },
- },
+ /**
+ * TODO Display when https://github.com/elastic/elasticsearch/issues/81037 is resolved
+ */
+ ...(true
+ ? []
+ : [
+ {
+ field: 'stats.time_in_millis',
+ name: (
+
+
+
+
+
+
+ }
+ />
+
+
+ ),
+ width: '100px',
+ truncateText: false,
+ 'data-test-subj': 'mlProcessorStatsTimePerDoc',
+ render: (v: number) => {
+ return durationFormatter(v);
+ },
+ },
+ ]),
{
field: 'stats.current',
name: (
diff --git a/x-pack/plugins/osquery/server/routes/status/create_status_route.ts b/x-pack/plugins/osquery/server/routes/status/create_status_route.ts
index ae79ef851bed97..871e112abc6c2b 100644
--- a/x-pack/plugins/osquery/server/routes/status/create_status_route.ts
+++ b/x-pack/plugins/osquery/server/routes/status/create_status_route.ts
@@ -31,14 +31,11 @@ export const createStatusRoute = (router: IRouter, osqueryContext: OsqueryAppCon
const internalSavedObjectsClient = await getInternalSavedObjectsClient(
osqueryContext.getStartServices
);
- const packageService = osqueryContext.service.getPackageService();
+ const packageService = osqueryContext.service.getPackageService()?.asInternalUser;
const packagePolicyService = osqueryContext.service.getPackagePolicyService();
const agentPolicyService = osqueryContext.service.getAgentPolicyService();
- const packageInfo = await osqueryContext.service.getPackageService()?.getInstallation({
- savedObjectsClient: internalSavedObjectsClient,
- pkgName: OSQUERY_INTEGRATION_NAME,
- });
+ const packageInfo = await packageService?.getInstallation(OSQUERY_INTEGRATION_NAME);
if (packageInfo?.install_version && satisfies(packageInfo?.install_version, '<0.6.0')) {
try {
@@ -102,8 +99,6 @@ export const createStatusRoute = (router: IRouter, osqueryContext: OsqueryAppCon
);
await packageService?.ensureInstalledPackage({
- esClient,
- savedObjectsClient: internalSavedObjectsClient,
pkgName: OSQUERY_INTEGRATION_NAME,
});
diff --git a/x-pack/plugins/reporting/kibana.json b/x-pack/plugins/reporting/kibana.json
index 123c23e5e1c29c..57cfc25c4b5288 100644
--- a/x-pack/plugins/reporting/kibana.json
+++ b/x-pack/plugins/reporting/kibana.json
@@ -17,6 +17,7 @@
"licensing",
"uiActions",
"taskManager",
+ "eventLog",
"embeddable",
"screenshotting",
"screenshotMode",
diff --git a/x-pack/plugins/reporting/public/lib/stream_handler.test.ts b/x-pack/plugins/reporting/public/lib/stream_handler.test.ts
index 1bb8c3229407da..78742af7fe8790 100644
--- a/x-pack/plugins/reporting/public/lib/stream_handler.test.ts
+++ b/x-pack/plugins/reporting/public/lib/stream_handler.test.ts
@@ -7,7 +7,7 @@
import sinon, { stub } from 'sinon';
import { NotificationsStart } from 'src/core/public';
-import { coreMock } from '../../../../../src/core/public/mocks';
+import { coreMock, themeServiceMock } from '../../../../../src/core/public/mocks';
import { JobSummary, ReportApiJSON } from '../../common/types';
import { Job } from './job';
import { ReportingAPIClient } from './reporting_api_client';
@@ -46,19 +46,21 @@ const notificationsMock = {
},
} as unknown as NotificationsStart;
+const theme = themeServiceMock.createStartContract();
+
describe('stream handler', () => {
afterEach(() => {
sinon.reset();
});
it('constructs', () => {
- const sh = new ReportingNotifierStreamHandler(notificationsMock, jobQueueClientMock);
+ const sh = new ReportingNotifierStreamHandler(notificationsMock, jobQueueClientMock, theme);
expect(sh).not.toBe(null);
});
describe('findChangedStatusJobs', () => {
it('finds no changed status jobs from empty', (done) => {
- const sh = new ReportingNotifierStreamHandler(notificationsMock, jobQueueClientMock);
+ const sh = new ReportingNotifierStreamHandler(notificationsMock, jobQueueClientMock, theme);
const findJobs = sh.findChangedStatusJobs([]);
findJobs.subscribe((data) => {
expect(data).toEqual({ completed: [], failed: [] });
@@ -67,7 +69,7 @@ describe('stream handler', () => {
});
it('finds changed status jobs', (done) => {
- const sh = new ReportingNotifierStreamHandler(notificationsMock, jobQueueClientMock);
+ const sh = new ReportingNotifierStreamHandler(notificationsMock, jobQueueClientMock, theme);
const findJobs = sh.findChangedStatusJobs([
'job-source-mock1',
'job-source-mock2',
@@ -83,7 +85,7 @@ describe('stream handler', () => {
describe('showNotifications', () => {
it('show success', (done) => {
- const sh = new ReportingNotifierStreamHandler(notificationsMock, jobQueueClientMock);
+ const sh = new ReportingNotifierStreamHandler(notificationsMock, jobQueueClientMock, theme);
sh.showNotifications({
completed: [
{
@@ -104,7 +106,7 @@ describe('stream handler', () => {
});
it('show max length warning', (done) => {
- const sh = new ReportingNotifierStreamHandler(notificationsMock, jobQueueClientMock);
+ const sh = new ReportingNotifierStreamHandler(notificationsMock, jobQueueClientMock, theme);
sh.showNotifications({
completed: [
{
@@ -126,7 +128,7 @@ describe('stream handler', () => {
});
it('show csv formulas warning', (done) => {
- const sh = new ReportingNotifierStreamHandler(notificationsMock, jobQueueClientMock);
+ const sh = new ReportingNotifierStreamHandler(notificationsMock, jobQueueClientMock, theme);
sh.showNotifications({
completed: [
{
@@ -148,7 +150,7 @@ describe('stream handler', () => {
});
it('show failed job toast', (done) => {
- const sh = new ReportingNotifierStreamHandler(notificationsMock, jobQueueClientMock);
+ const sh = new ReportingNotifierStreamHandler(notificationsMock, jobQueueClientMock, theme);
sh.showNotifications({
completed: [],
failed: [
@@ -169,7 +171,7 @@ describe('stream handler', () => {
});
it('show multiple toast', (done) => {
- const sh = new ReportingNotifierStreamHandler(notificationsMock, jobQueueClientMock);
+ const sh = new ReportingNotifierStreamHandler(notificationsMock, jobQueueClientMock, theme);
sh.showNotifications({
completed: [
{
diff --git a/x-pack/plugins/reporting/public/lib/stream_handler.ts b/x-pack/plugins/reporting/public/lib/stream_handler.ts
index 304b4fb73374df..27e220221156e9 100644
--- a/x-pack/plugins/reporting/public/lib/stream_handler.ts
+++ b/x-pack/plugins/reporting/public/lib/stream_handler.ts
@@ -8,7 +8,7 @@
import { i18n } from '@kbn/i18n';
import * as Rx from 'rxjs';
import { catchError, map } from 'rxjs/operators';
-import { NotificationsSetup } from 'src/core/public';
+import { NotificationsSetup, ThemeServiceStart } from 'src/core/public';
import { JOB_COMPLETION_NOTIFICATIONS_SESSION_KEY, JOB_STATUSES } from '../../common/constants';
import { JobId, JobSummary, JobSummarySet } from '../../common/types';
import {
@@ -37,7 +37,11 @@ function getReportStatus(src: Job): JobSummary {
}
export class ReportingNotifierStreamHandler {
- constructor(private notifications: NotificationsSetup, private apiClient: ReportingAPIClient) {}
+ constructor(
+ private notifications: NotificationsSetup,
+ private apiClient: ReportingAPIClient,
+ private theme: ThemeServiceStart
+ ) {}
/*
* Use Kibana Toast API to show our messages
@@ -54,7 +58,8 @@ export class ReportingNotifierStreamHandler {
getWarningFormulasToast(
job,
this.apiClient.getManagementLink,
- this.apiClient.getDownloadLink
+ this.apiClient.getDownloadLink,
+ this.theme
)
);
} else if (job.maxSizeReached) {
@@ -62,12 +67,18 @@ export class ReportingNotifierStreamHandler {
getWarningMaxSizeToast(
job,
this.apiClient.getManagementLink,
- this.apiClient.getDownloadLink
+ this.apiClient.getDownloadLink,
+ this.theme
)
);
} else {
this.notifications.toasts.addSuccess(
- getSuccessToast(job, this.apiClient.getManagementLink, this.apiClient.getDownloadLink)
+ getSuccessToast(
+ job,
+ this.apiClient.getManagementLink,
+ this.apiClient.getDownloadLink,
+ this.theme
+ )
);
}
}
@@ -76,7 +87,7 @@ export class ReportingNotifierStreamHandler {
for (const job of failedJobs) {
const errorText = await this.apiClient.getError(job.id);
this.notifications.toasts.addDanger(
- getFailureToast(errorText, job, this.apiClient.getManagementLink)
+ getFailureToast(errorText, job, this.apiClient.getManagementLink, this.theme)
);
}
return { completed: completedJobs, failed: failedJobs };
@@ -120,7 +131,8 @@ export class ReportingNotifierStreamHandler {
i18n.translate('xpack.reporting.publicNotifier.httpErrorMessage', {
defaultMessage: 'Could not check Reporting job status!',
}),
- err
+ err,
+ this.theme
)
); // prettier-ignore
window.console.error(err);
diff --git a/x-pack/plugins/reporting/public/notifier/general_error.tsx b/x-pack/plugins/reporting/public/notifier/general_error.tsx
index 141b7b49444b0f..66fff4d00ceeb2 100644
--- a/x-pack/plugins/reporting/public/notifier/general_error.tsx
+++ b/x-pack/plugins/reporting/public/notifier/general_error.tsx
@@ -8,10 +8,14 @@
import React, { Fragment } from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import { EuiCallOut, EuiSpacer } from '@elastic/eui';
-import { ToastInput } from 'src/core/public';
+import { ThemeServiceStart, ToastInput } from 'src/core/public';
import { toMountPoint } from '../../../../../src/plugins/kibana_react/public';
-export const getGeneralErrorToast = (errorText: string, err: Error): ToastInput => ({
+export const getGeneralErrorToast = (
+ errorText: string,
+ err: Error,
+ theme: ThemeServiceStart
+): ToastInput => ({
text: toMountPoint(
@@ -24,7 +28,8 @@ export const getGeneralErrorToast = (errorText: string, err: Error): ToastInput
id="xpack.reporting.publicNotifier.error.tryRefresh"
defaultMessage="Try refreshing the page."
/>
-
+ ,
+ { theme$: theme.theme$ }
),
iconType: undefined,
});
diff --git a/x-pack/plugins/reporting/public/notifier/job_failure.tsx b/x-pack/plugins/reporting/public/notifier/job_failure.tsx
index a9e7b78c7e12f6..87fbc72d29ab82 100644
--- a/x-pack/plugins/reporting/public/notifier/job_failure.tsx
+++ b/x-pack/plugins/reporting/public/notifier/job_failure.tsx
@@ -9,14 +9,15 @@ import { EuiCallOut, EuiSpacer } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import React, { Fragment } from 'react';
-import { ToastInput } from 'src/core/public';
+import { ThemeServiceStart, ToastInput } from 'src/core/public';
import { toMountPoint } from '../../../../../src/plugins/kibana_react/public';
import { JobSummary, ManagementLinkFn } from '../../common/types';
export const getFailureToast = (
errorText: string,
job: JobSummary,
- getManagmenetLink: ManagementLinkFn
+ getManagmenetLink: ManagementLinkFn,
+ theme: ThemeServiceStart
): ToastInput => {
return {
title: toMountPoint(
@@ -24,7 +25,8 @@ export const getFailureToast = (
id="xpack.reporting.publicNotifier.error.couldNotCreateReportTitle"
defaultMessage="Could not create report for {reportObjectType} '{reportObjectTitle}'."
values={{ reportObjectType: job.jobtype, reportObjectTitle: job.title }}
- />
+ />,
+ { theme$: theme.theme$ }
),
text: toMountPoint(
@@ -58,7 +60,8 @@ export const getFailureToast = (
}}
/>
-
+ ,
+ { theme$: theme.theme$ }
),
iconType: undefined,
'data-test-subj': 'completeReportFailure',
diff --git a/x-pack/plugins/reporting/public/notifier/job_success.tsx b/x-pack/plugins/reporting/public/notifier/job_success.tsx
index c1de9a7625858e..f949c27f6fedbf 100644
--- a/x-pack/plugins/reporting/public/notifier/job_success.tsx
+++ b/x-pack/plugins/reporting/public/notifier/job_success.tsx
@@ -7,7 +7,7 @@
import { FormattedMessage } from '@kbn/i18n-react';
import React, { Fragment } from 'react';
-import { ToastInput } from 'src/core/public';
+import { ThemeServiceStart, ToastInput } from 'src/core/public';
import { toMountPoint } from '../../../../../src/plugins/kibana_react/public';
import { JobId, JobSummary } from '../../common/types';
import { DownloadButton } from './job_download_button';
@@ -16,14 +16,16 @@ import { ReportLink } from './report_link';
export const getSuccessToast = (
job: JobSummary,
getReportLink: () => string,
- getDownloadLink: (jobId: JobId) => string
+ getDownloadLink: (jobId: JobId) => string,
+ theme: ThemeServiceStart
): ToastInput => ({
title: toMountPoint(
+ />,
+ { theme$: theme.theme$ }
),
color: 'success',
text: toMountPoint(
@@ -32,7 +34,8 @@ export const getSuccessToast = (
-
+ ,
+ { theme$: theme.theme$ }
),
'data-test-subj': 'completeReportSuccess',
});
diff --git a/x-pack/plugins/reporting/public/notifier/job_warning_formulas.tsx b/x-pack/plugins/reporting/public/notifier/job_warning_formulas.tsx
index c835203813b866..08c87a40a829ac 100644
--- a/x-pack/plugins/reporting/public/notifier/job_warning_formulas.tsx
+++ b/x-pack/plugins/reporting/public/notifier/job_warning_formulas.tsx
@@ -7,7 +7,7 @@
import { FormattedMessage } from '@kbn/i18n-react';
import React, { Fragment } from 'react';
-import { ToastInput } from 'src/core/public';
+import { ThemeServiceStart, ToastInput } from 'src/core/public';
import { toMountPoint } from '../../../../../src/plugins/kibana_react/public';
import { JobId, JobSummary } from '../../common/types';
import { DownloadButton } from './job_download_button';
@@ -16,14 +16,16 @@ import { ReportLink } from './report_link';
export const getWarningFormulasToast = (
job: JobSummary,
getReportLink: () => string,
- getDownloadLink: (jobId: JobId) => string
+ getDownloadLink: (jobId: JobId) => string,
+ theme: ThemeServiceStart
): ToastInput => ({
title: toMountPoint(
+ />,
+ { theme$: theme.theme$ }
),
text: toMountPoint(
@@ -37,7 +39,8 @@ export const getWarningFormulasToast = (
-
+ ,
+ { theme$: theme.theme$ }
),
'data-test-subj': 'completeReportCsvFormulasWarning',
});
diff --git a/x-pack/plugins/reporting/public/notifier/job_warning_max_size.tsx b/x-pack/plugins/reporting/public/notifier/job_warning_max_size.tsx
index f7cc8e2219df90..629ac44adeae86 100644
--- a/x-pack/plugins/reporting/public/notifier/job_warning_max_size.tsx
+++ b/x-pack/plugins/reporting/public/notifier/job_warning_max_size.tsx
@@ -7,7 +7,7 @@
import { FormattedMessage } from '@kbn/i18n-react';
import React, { Fragment } from 'react';
-import { ToastInput } from 'src/core/public';
+import { ThemeServiceStart, ToastInput } from 'src/core/public';
import { toMountPoint } from '../../../../../src/plugins/kibana_react/public';
import { JobId, JobSummary } from '../../common/types';
import { DownloadButton } from './job_download_button';
@@ -16,14 +16,16 @@ import { ReportLink } from './report_link';
export const getWarningMaxSizeToast = (
job: JobSummary,
getReportLink: () => string,
- getDownloadLink: (jobId: JobId) => string
+ getDownloadLink: (jobId: JobId) => string,
+ theme: ThemeServiceStart
): ToastInput => ({
title: toMountPoint(
+ />,
+ { theme$: theme.theme$ }
),
text: toMountPoint(
@@ -37,7 +39,8 @@ export const getWarningMaxSizeToast = (
-
+ ,
+ { theme$: theme.theme$ }
),
'data-test-subj': 'completeReportMaxSizeWarning',
});
diff --git a/x-pack/plugins/reporting/public/plugin.ts b/x-pack/plugins/reporting/public/plugin.ts
index b1f9b63e66cbe2..77c8489bb89926 100644
--- a/x-pack/plugins/reporting/public/plugin.ts
+++ b/x-pack/plugins/reporting/public/plugin.ts
@@ -17,6 +17,7 @@ import {
NotificationsSetup,
Plugin,
PluginInitializerContext,
+ ThemeServiceStart,
} from 'src/core/public';
import type { ScreenshottingSetup } from '../../screenshotting/public';
import { CONTEXT_MENU_TRIGGER } from '../../../../src/plugins/embeddable/public';
@@ -56,13 +57,18 @@ function getStored(): JobId[] {
return sessionValue ? JSON.parse(sessionValue) : [];
}
-function handleError(notifications: NotificationsSetup, err: Error): Rx.Observable {
+function handleError(
+ notifications: NotificationsSetup,
+ err: Error,
+ theme: ThemeServiceStart
+): Rx.Observable {
notifications.toasts.addDanger(
getGeneralErrorToast(
i18n.translate('xpack.reporting.publicNotifier.pollingErrorMessage', {
defaultMessage: 'Reporting notifier error!',
}),
- err
+ err,
+ theme
)
);
window.console.error(err);
@@ -235,6 +241,7 @@ export class ReportingPublicPlugin
startServices$,
uiSettings,
usesUiCapabilities,
+ theme: core.theme,
})
);
@@ -246,6 +253,7 @@ export class ReportingPublicPlugin
startServices$,
uiSettings,
usesUiCapabilities,
+ theme: core.theme,
})
);
@@ -255,7 +263,7 @@ export class ReportingPublicPlugin
public start(core: CoreStart) {
const { notifications } = core;
const apiClient = this.getApiClient(core.http, core.uiSettings);
- const streamHandler = new StreamHandler(notifications, apiClient);
+ const streamHandler = new StreamHandler(notifications, apiClient, core.theme);
const interval = durationToNumber(this.config.poll.jobsRefresh.interval);
Rx.timer(0, interval)
.pipe(
@@ -264,7 +272,7 @@ export class ReportingPublicPlugin
filter((storedJobs) => storedJobs.length > 0), // stop the pipeline here if there are none pending
mergeMap((storedJobs) => streamHandler.findChangedStatusJobs(storedJobs)), // look up the latest status of all pending jobs on the server
mergeMap(({ completed, failed }) => streamHandler.showNotifications({ completed, failed })),
- catchError((err) => handleError(notifications, err))
+ catchError((err) => handleError(notifications, err, core.theme))
)
.subscribe();
diff --git a/x-pack/plugins/reporting/public/share_context_menu/index.ts b/x-pack/plugins/reporting/public/share_context_menu/index.ts
index 321a5a29281af9..6a5dbf970e0b4d 100644
--- a/x-pack/plugins/reporting/public/share_context_menu/index.ts
+++ b/x-pack/plugins/reporting/public/share_context_menu/index.ts
@@ -6,7 +6,7 @@
*/
import * as Rx from 'rxjs';
-import type { IUiSettingsClient, ToastsSetup } from 'src/core/public';
+import type { IUiSettingsClient, ThemeServiceSetup, ToastsSetup } from 'src/core/public';
import { CoreStart } from 'src/core/public';
import type { LayoutParams } from '../../../screenshotting/common';
import type { LicensingPluginSetup } from '../../../licensing/public';
@@ -19,6 +19,7 @@ export interface ExportPanelShareOpts {
license$: LicensingPluginSetup['license$']; // FIXME: 'license$' is deprecated
startServices$: Rx.Observable<[CoreStart, object, unknown]>;
usesUiCapabilities: boolean;
+ theme: ThemeServiceSetup;
}
export interface ReportingSharingData {
diff --git a/x-pack/plugins/reporting/public/share_context_menu/register_csv_reporting.tsx b/x-pack/plugins/reporting/public/share_context_menu/register_csv_reporting.tsx
index 8859d01e4fe9a5..b264c963611222 100644
--- a/x-pack/plugins/reporting/public/share_context_menu/register_csv_reporting.tsx
+++ b/x-pack/plugins/reporting/public/share_context_menu/register_csv_reporting.tsx
@@ -21,6 +21,7 @@ export const ReportingCsvShareProvider = ({
license$,
startServices$,
usesUiCapabilities,
+ theme,
}: ExportPanelShareOpts) => {
let licenseToolTipContent = '';
let licenseHasCsvReporting = false;
@@ -96,6 +97,7 @@ export const ReportingCsvShareProvider = ({
objectId={objectId}
getJobParams={getJobParams}
onClose={onClose}
+ theme={theme}
/>
),
},
diff --git a/x-pack/plugins/reporting/public/share_context_menu/register_pdf_png_reporting.tsx b/x-pack/plugins/reporting/public/share_context_menu/register_pdf_png_reporting.tsx
index 610781f3b6ea07..3cc8cbacc7921b 100644
--- a/x-pack/plugins/reporting/public/share_context_menu/register_pdf_png_reporting.tsx
+++ b/x-pack/plugins/reporting/public/share_context_menu/register_pdf_png_reporting.tsx
@@ -63,6 +63,7 @@ export const reportingScreenshotShareProvider = ({
license$,
startServices$,
usesUiCapabilities,
+ theme,
}: ExportPanelShareOpts) => {
let licenseToolTipContent = '';
let licenseDisabled = true;
@@ -156,6 +157,7 @@ export const reportingScreenshotShareProvider = ({
getJobParams={getJobParams(apiClient, jobProviderOptions, pngReportType)}
isDirty={isDirty}
onClose={onClose}
+ theme={theme}
/>
),
},
@@ -191,6 +193,7 @@ export const reportingScreenshotShareProvider = ({
getJobParams={getJobParams(apiClient, jobProviderOptions, pdfReportType)}
isDirty={isDirty}
onClose={onClose}
+ theme={theme}
/>
),
},
diff --git a/x-pack/plugins/reporting/public/share_context_menu/reporting_panel_content/reporting_panel_content.test.tsx b/x-pack/plugins/reporting/public/share_context_menu/reporting_panel_content/reporting_panel_content.test.tsx
index e9dd584e51f82c..ef3e9940238c11 100644
--- a/x-pack/plugins/reporting/public/share_context_menu/reporting_panel_content/reporting_panel_content.test.tsx
+++ b/x-pack/plugins/reporting/public/share_context_menu/reporting_panel_content/reporting_panel_content.test.tsx
@@ -10,6 +10,7 @@ import { mountWithIntl } from '@kbn/test/jest';
import {
httpServiceMock,
notificationServiceMock,
+ themeServiceMock,
uiSettingsServiceMock,
} from 'src/core/public/mocks';
import { ReportingAPIClient } from '../../lib/reporting_api_client';
@@ -21,6 +22,8 @@ jest.mock('./constants', () => ({
}));
import * as constants from './constants';
+const theme = themeServiceMock.createSetupContract();
+
describe('ReportingPanelContent', () => {
const props: Partial = {
layoutId: 'super_cool_layout_id_X',
@@ -58,6 +61,7 @@ describe('ReportingPanelContent', () => {
apiClient={apiClient}
toasts={toasts}
uiSettings={uiSettings}
+ theme={theme}
{...props}
{...newProps}
/>
diff --git a/x-pack/plugins/reporting/public/share_context_menu/reporting_panel_content/reporting_panel_content.tsx b/x-pack/plugins/reporting/public/share_context_menu/reporting_panel_content/reporting_panel_content.tsx
index 73ccbc2b13d753..e1fa1198cf1f80 100644
--- a/x-pack/plugins/reporting/public/share_context_menu/reporting_panel_content/reporting_panel_content.tsx
+++ b/x-pack/plugins/reporting/public/share_context_menu/reporting_panel_content/reporting_panel_content.tsx
@@ -18,7 +18,7 @@ import {
import { i18n } from '@kbn/i18n';
import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n-react';
import React, { Component, ReactElement } from 'react';
-import { IUiSettingsClient, ToastsSetup } from 'src/core/public';
+import { IUiSettingsClient, ThemeServiceSetup, ToastsSetup } from 'src/core/public';
import url from 'url';
import { toMountPoint } from '../../../../../../src/plugins/kibana_react/public';
import {
@@ -46,6 +46,7 @@ export interface ReportingPanelProps {
options?: ReactElement | null;
isDirty?: boolean;
onClose?: () => void;
+ theme: ThemeServiceSetup;
}
export type Props = ReportingPanelProps & { intl: InjectedIntl };
@@ -291,7 +292,8 @@ class ReportingPanelContentUi extends Component {
),
}}
- />
+ />,
+ { theme$: this.props.theme.theme$ }
),
'data-test-subj': 'queueReportSuccess',
});
diff --git a/x-pack/plugins/reporting/public/share_context_menu/screen_capture_panel_content.test.tsx b/x-pack/plugins/reporting/public/share_context_menu/screen_capture_panel_content.test.tsx
index 7a2fa52d010e31..ebf741c79bd86a 100644
--- a/x-pack/plugins/reporting/public/share_context_menu/screen_capture_panel_content.test.tsx
+++ b/x-pack/plugins/reporting/public/share_context_menu/screen_capture_panel_content.test.tsx
@@ -8,7 +8,7 @@
import { mount } from 'enzyme';
import React from 'react';
import { __IntlProvider as IntlProvider } from '@kbn/i18n-react';
-import { coreMock } from 'src/core/public/mocks';
+import { coreMock, themeServiceMock } from 'src/core/public/mocks';
import { ReportingAPIClient } from '../lib/reporting_api_client';
import { ScreenCapturePanelContent } from './screen_capture_panel_content';
@@ -27,6 +27,8 @@ const getJobParamsDefault = () => ({
browserTimezone: 'America/New_York',
});
+const theme = themeServiceMock.createSetupContract();
+
test('ScreenCapturePanelContent renders the default view properly', () => {
const component = mount(
@@ -37,6 +39,7 @@ test('ScreenCapturePanelContent renders the default view properly', () => {
uiSettings={uiSettings}
toasts={coreSetup.notifications.toasts}
getJobParams={getJobParamsDefault}
+ theme={theme}
/>
);
@@ -56,6 +59,7 @@ test('ScreenCapturePanelContent properly renders a view with "canvas" layout opt
uiSettings={uiSettings}
toasts={coreSetup.notifications.toasts}
getJobParams={getJobParamsDefault}
+ theme={theme}
/>
);
@@ -75,6 +79,7 @@ test('ScreenCapturePanelContent allows POST URL to be copied when objectId is pr
toasts={coreSetup.notifications.toasts}
getJobParams={getJobParamsDefault}
objectId={'1234-5'}
+ theme={theme}
/>
);
@@ -93,6 +98,7 @@ test('ScreenCapturePanelContent does not allow POST URL to be copied when object
uiSettings={uiSettings}
toasts={coreSetup.notifications.toasts}
getJobParams={getJobParamsDefault}
+ theme={theme}
/>
);
@@ -111,6 +117,7 @@ test('ScreenCapturePanelContent properly renders a view with "print" layout opti
uiSettings={uiSettings}
toasts={coreSetup.notifications.toasts}
getJobParams={getJobParamsDefault}
+ theme={theme}
/>
);
@@ -130,6 +137,7 @@ test('ScreenCapturePanelContent decorated job params are visible in the POST URL
uiSettings={uiSettings}
toasts={coreSetup.notifications.toasts}
getJobParams={getJobParamsDefault}
+ theme={theme}
/>
);
diff --git a/x-pack/plugins/reporting/public/shared/get_shared_components.tsx b/x-pack/plugins/reporting/public/shared/get_shared_components.tsx
index b08036e8b1c806..0906bf85c9538a 100644
--- a/x-pack/plugins/reporting/public/shared/get_shared_components.tsx
+++ b/x-pack/plugins/reporting/public/shared/get_shared_components.tsx
@@ -35,6 +35,7 @@ export function getSharedComponents(core: CoreSetup, apiClient: ReportingAPIClie
apiClient={apiClient}
toasts={core.notifications.toasts}
uiSettings={core.uiSettings}
+ theme={core.theme}
{...props}
/>
);
@@ -48,6 +49,7 @@ export function getSharedComponents(core: CoreSetup, apiClient: ReportingAPIClie
apiClient={apiClient}
toasts={core.notifications.toasts}
uiSettings={core.uiSettings}
+ theme={core.theme}
{...props}
/>
);
@@ -61,6 +63,7 @@ export function getSharedComponents(core: CoreSetup, apiClient: ReportingAPIClie
apiClient={apiClient}
toasts={core.notifications.toasts}
uiSettings={core.uiSettings}
+ theme={core.theme}
{...props}
/>
);
diff --git a/x-pack/plugins/reporting/server/core.ts b/x-pack/plugins/reporting/server/core.ts
index 232d32aeff10b6..e48983634efd88 100644
--- a/x-pack/plugins/reporting/server/core.ts
+++ b/x-pack/plugins/reporting/server/core.ts
@@ -8,7 +8,6 @@
import Hapi from '@hapi/hapi';
import * as Rx from 'rxjs';
import { filter, first, map, switchMap, take } from 'rxjs/operators';
-import type { ScreenshottingStart, ScreenshotResult } from '../../screenshotting/server';
import {
BasePath,
IClusterClient,
@@ -22,8 +21,10 @@ import {
UiSettingsServiceStart,
} from '../../../../src/core/server';
import { PluginStart as DataPluginStart } from '../../../../src/plugins/data/server';
+import { IEventLogService } from '../../event_log/server';
import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server';
import { LicensingPluginSetup } from '../../licensing/server';
+import type { ScreenshotResult, ScreenshottingStart } from '../../screenshotting/server';
import { SecurityPluginSetup } from '../../security/server';
import { DEFAULT_SPACE_ID } from '../../spaces/common/constants';
import { SpacesPluginSetup } from '../../spaces/server';
@@ -33,11 +34,13 @@ import { durationToNumber } from '../common/schema_utils';
import { ReportingConfig, ReportingSetup } from './';
import { ReportingConfigType } from './config';
import { checkLicense, getExportTypesRegistry, LevelLogger } from './lib';
-import { ReportingStore } from './lib/store';
+import { reportingEventLoggerFactory } from './lib/event_logger/logger';
+import { IReport, ReportingStore } from './lib/store';
import { ExecuteReportTask, MonitorReportsTask, ReportTaskParams } from './lib/tasks';
import { ReportingPluginRouter, ScreenshotOptions } from './types';
export interface ReportingInternalSetup {
+ eventLog: IEventLogService;
basePath: Pick;
router: ReportingPluginRouter;
features: FeaturesPluginSetup;
@@ -381,4 +384,9 @@ export class ReportingCore {
public countConcurrentReports(): number {
return this.executing.size;
}
+
+ public getEventLogger(report: IReport, task?: { id: string }) {
+ const ReportingEventLogger = reportingEventLoggerFactory(this.pluginSetupDeps!.eventLog);
+ return new ReportingEventLogger(report, task);
+ }
}
diff --git a/x-pack/plugins/reporting/server/lib/event_logger/index.ts b/x-pack/plugins/reporting/server/lib/event_logger/index.ts
new file mode 100644
index 00000000000000..566f0a21e2b05d
--- /dev/null
+++ b/x-pack/plugins/reporting/server/lib/event_logger/index.ts
@@ -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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { IEventLogService } from '../../../../event_log/server';
+import { PLUGIN_ID } from '../../../common/constants';
+
+export enum ActionType {
+ SCHEDULE_TASK = 'schedule-task',
+ CLAIM_TASK = 'claim-task',
+ EXECUTE_START = 'execute-start',
+ EXECUTE_COMPLETE = 'execute-complete',
+ SAVE_REPORT = 'save-report',
+ RETRY = 'retry',
+ FAIL_REPORT = 'fail-report',
+}
+export function registerEventLogProviderActions(eventLog: IEventLogService) {
+ eventLog.registerProviderActions(PLUGIN_ID, Object.values(ActionType));
+}
diff --git a/x-pack/plugins/reporting/server/lib/event_logger/logger.test.ts b/x-pack/plugins/reporting/server/lib/event_logger/logger.test.ts
new file mode 100644
index 00000000000000..21c4ee2d5e4cf9
--- /dev/null
+++ b/x-pack/plugins/reporting/server/lib/event_logger/logger.test.ts
@@ -0,0 +1,250 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { ConcreteTaskInstance } from '../../../../task_manager/server';
+import { eventLogServiceMock } from '../../../../event_log/server/mocks';
+import { BasePayload } from '../../types';
+import { Report } from '../store';
+import { ReportingEventLogger, reportingEventLoggerFactory } from './logger';
+
+describe('Event Logger', () => {
+ const mockReport = new Report({
+ _id: '12348',
+ payload: { browserTimezone: 'UTC' } as BasePayload,
+ jobtype: 'csv',
+ });
+
+ let factory: ReportingEventLogger;
+
+ beforeEach(() => {
+ factory = reportingEventLoggerFactory(eventLogServiceMock.create());
+ });
+
+ it(`should construct with an internal seed object`, () => {
+ const logger = new factory(mockReport);
+ expect(logger.eventObj).toMatchInlineSnapshot(`
+ Object {
+ "event": Object {
+ "provider": "reporting",
+ "timezone": "UTC",
+ },
+ "kibana": Object {
+ "reporting": Object {
+ "id": "12348",
+ "jobType": "csv",
+ },
+ },
+ "log": Object {
+ "logger": "reporting",
+ },
+ "user": undefined,
+ }
+ `);
+ });
+
+ it(`allows optional user name`, () => {
+ const logger = new factory(new Report({ ...mockReport, created_by: 'thundercat' }));
+ expect(logger.eventObj).toMatchInlineSnapshot(`
+ Object {
+ "event": Object {
+ "provider": "reporting",
+ "timezone": "UTC",
+ },
+ "kibana": Object {
+ "reporting": Object {
+ "id": "12348",
+ "jobType": "csv",
+ },
+ },
+ "log": Object {
+ "logger": "reporting",
+ },
+ "user": Object {
+ "name": "thundercat",
+ },
+ }
+ `);
+ });
+
+ it(`allows optional task.id`, () => {
+ const logger = new factory(new Report({ ...mockReport, created_by: 'thundercat' }), {
+ id: 'some-task-id-123',
+ } as ConcreteTaskInstance);
+ expect(logger.eventObj).toMatchInlineSnapshot(`
+ Object {
+ "event": Object {
+ "provider": "reporting",
+ "timezone": "UTC",
+ },
+ "kibana": Object {
+ "reporting": Object {
+ "id": "12348",
+ "jobType": "csv",
+ },
+ "task": Object {
+ "id": "some-task-id-123",
+ },
+ },
+ "log": Object {
+ "logger": "reporting",
+ },
+ "user": Object {
+ "name": "thundercat",
+ },
+ }
+ `);
+ });
+
+ it(`logExecutionStart`, () => {
+ const logger = new factory(mockReport);
+ const result = logger.logExecutionStart();
+ expect([result.event, result.kibana.reporting, result.message]).toMatchInlineSnapshot(`
+ Array [
+ Object {
+ "action": "execute-start",
+ "kind": "event",
+ "provider": "reporting",
+ "timezone": "UTC",
+ },
+ Object {
+ "id": "12348",
+ "jobType": "csv",
+ },
+ "starting csv execution",
+ ]
+ `);
+ expect(result.message).toMatchInlineSnapshot(`"starting csv execution"`);
+ expect(logger.completionLogger.startTiming).toBeCalled();
+ });
+
+ it(`logExecutionComplete`, () => {
+ const logger = new factory(mockReport);
+ logger.logExecutionStart();
+
+ const result = logger.logExecutionComplete({ byteSize: 444 });
+ expect([result.event, result.kibana.reporting, result.message]).toMatchInlineSnapshot(`
+ Array [
+ Object {
+ "action": "execute-complete",
+ "kind": "metrics",
+ "outcome": "success",
+ "provider": "reporting",
+ "timezone": "UTC",
+ },
+ Object {
+ "byteSize": 444,
+ "id": "12348",
+ "jobType": "csv",
+ },
+ "completed csv execution",
+ ]
+ `);
+ expect(result.message).toMatchInlineSnapshot(`"completed csv execution"`);
+ expect(logger.completionLogger.startTiming).toBeCalled();
+ expect(logger.completionLogger.stopTiming).toBeCalled();
+ });
+
+ it(`logError`, () => {
+ const logger = new factory(mockReport);
+ const result = logger.logError(new Error('an error occurred'));
+ expect([result.event, result.kibana.reporting, result.message]).toMatchInlineSnapshot(`
+ Array [
+ Object {
+ "action": "execute-complete",
+ "kind": "error",
+ "outcome": "failure",
+ "provider": "reporting",
+ "timezone": "UTC",
+ },
+ Object {
+ "id": "12348",
+ "jobType": "csv",
+ },
+ "an error occurred",
+ ]
+ `);
+ expect(result.message).toBe(`an error occurred`);
+ });
+
+ it(`logClaimTask`, () => {
+ const logger = new factory(mockReport);
+ const result = logger.logClaimTask();
+ expect([result.event, result.kibana.reporting, result.message]).toMatchInlineSnapshot(`
+ Array [
+ Object {
+ "action": "claim-task",
+ "kind": "event",
+ "provider": "reporting",
+ "timezone": "UTC",
+ },
+ Object {
+ "id": "12348",
+ "jobType": "csv",
+ },
+ "claimed report 12348",
+ ]
+ `);
+ });
+
+ it(`logReportFailure`, () => {
+ const logger = new factory(mockReport);
+ const result = logger.logReportFailure();
+ expect([result.event, result.kibana.reporting, result.message]).toMatchInlineSnapshot(`
+ Array [
+ Object {
+ "action": "fail-report",
+ "kind": "event",
+ "provider": "reporting",
+ "timezone": "UTC",
+ },
+ Object {
+ "id": "12348",
+ "jobType": "csv",
+ },
+ "report 12348 has failed",
+ ]
+ `);
+ });
+ it(`logReportSaved`, () => {
+ const logger = new factory(mockReport);
+ const result = logger.logReportSaved();
+ expect([result.event, result.kibana.reporting, result.message]).toMatchInlineSnapshot(`
+ Array [
+ Object {
+ "action": "save-report",
+ "kind": "event",
+ "provider": "reporting",
+ "timezone": "UTC",
+ },
+ Object {
+ "id": "12348",
+ "jobType": "csv",
+ },
+ "saved report 12348",
+ ]
+ `);
+ });
+ it(`logRetry`, () => {
+ const logger = new factory(mockReport);
+ const result = logger.logRetry();
+ expect([result.event, result.kibana.reporting, result.message]).toMatchInlineSnapshot(`
+ Array [
+ Object {
+ "action": "retry",
+ "kind": "event",
+ "provider": "reporting",
+ "timezone": "UTC",
+ },
+ Object {
+ "id": "12348",
+ "jobType": "csv",
+ },
+ "scheduled retry for report 12348",
+ ]
+ `);
+ });
+});
diff --git a/x-pack/plugins/reporting/server/lib/event_logger/logger.ts b/x-pack/plugins/reporting/server/lib/event_logger/logger.ts
new file mode 100644
index 00000000000000..0ec864e36620b4
--- /dev/null
+++ b/x-pack/plugins/reporting/server/lib/event_logger/logger.ts
@@ -0,0 +1,200 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import deepMerge from 'deepmerge';
+import { IEventLogger, IEventLogService } from '../../../../event_log/server';
+import { PLUGIN_ID } from '../../../common/constants';
+import { IReport } from '../store';
+import { ActionType } from './';
+import {
+ ClaimedTask,
+ CompletedExecution,
+ ErrorAction,
+ ExecuteError,
+ FailedReport,
+ SavedReport,
+ ScheduledRetry,
+ ScheduledTask,
+ StartedExecution,
+} from './types';
+
+/** @internal */
+export interface ExecutionCompleteMetrics {
+ byteSize: number;
+}
+
+/** @internal */
+export function reportingEventLoggerFactory(eventLog: IEventLogService) {
+ const genericLogger = eventLog.getLogger({ event: { provider: PLUGIN_ID } });
+
+ return class ReportingEventLogger {
+ readonly eventObj: {
+ event: {
+ timezone: string;
+ provider: 'reporting';
+ };
+ kibana: { reporting: StartedExecution['kibana']['reporting']; task?: { id: string } };
+ log: { logger: 'reporting' };
+ user?: { name: string };
+ };
+
+ readonly report: IReport;
+ readonly task?: { id: string };
+
+ completionLogger: IEventLogger;
+
+ constructor(report: IReport, task?: { id: string }) {
+ this.report = report;
+ this.task = task;
+ this.eventObj = {
+ event: { timezone: report.payload.browserTimezone, provider: 'reporting' },
+ kibana: {
+ reporting: { id: report._id, jobType: report.jobtype },
+ ...(task?.id ? { task: { id: task.id } } : undefined),
+ },
+ log: { logger: 'reporting' },
+ user: report.created_by ? { name: report.created_by } : undefined,
+ };
+
+ // create a "complete" logger that will use EventLog helpers to calculate timings
+ this.completionLogger = eventLog.getLogger({ event: { provider: PLUGIN_ID } });
+ }
+
+ logScheduleTask(): ScheduledTask {
+ const event = deepMerge(
+ {
+ message: `queued report ${this.report._id}`,
+ event: { kind: 'event', action: ActionType.SCHEDULE_TASK },
+ log: { level: 'info' },
+ } as Partial,
+ this.eventObj
+ );
+
+ genericLogger.logEvent(event);
+ return event;
+ }
+
+ logExecutionStart(): StartedExecution {
+ this.completionLogger.startTiming(this.eventObj);
+ const event = deepMerge(
+ {
+ message: `starting ${this.report.jobtype} execution`,
+ event: { kind: 'event', action: ActionType.EXECUTE_START },
+ log: { level: 'info' },
+ } as Partial,
+ this.eventObj
+ );
+
+ genericLogger.logEvent(event);
+ return event;
+ }
+
+ logExecutionComplete({ byteSize }: ExecutionCompleteMetrics): CompletedExecution {
+ this.completionLogger.stopTiming(this.eventObj);
+ const event = deepMerge(
+ {
+ message: `completed ${this.report.jobtype} execution`,
+ event: {
+ kind: 'metrics',
+ outcome: 'success',
+ action: ActionType.EXECUTE_COMPLETE,
+ },
+ kibana: { reporting: { byteSize } },
+ log: { level: 'info' },
+ } as Partial,
+ this.eventObj
+ );
+ this.completionLogger.logEvent(event);
+ return event;
+ }
+
+ logError(error: ErrorAction): ExecuteError {
+ interface LoggedErrorMessage {
+ message: string;
+ error: ExecuteError['error'];
+ event: Omit;
+ log: Omit;
+ }
+ const logErrorMessage: LoggedErrorMessage = {
+ message: error.message,
+ error: {
+ message: error.message,
+ code: error.code,
+ stack_trace: error.stack_trace,
+ type: error.type,
+ },
+ event: {
+ kind: 'error',
+ outcome: 'failure',
+ action: ActionType.EXECUTE_COMPLETE,
+ },
+ log: { level: 'error' },
+ };
+ const event = deepMerge(logErrorMessage, this.eventObj);
+ genericLogger.logEvent(event);
+ return event;
+ }
+
+ logClaimTask(): ClaimedTask {
+ const event = deepMerge(
+ {
+ message: `claimed report ${this.report._id}`,
+ event: { kind: 'event', action: ActionType.CLAIM_TASK },
+ log: { level: 'info' },
+ } as Partial,
+ this.eventObj
+ );
+
+ genericLogger.logEvent(event);
+ return event;
+ }
+
+ logReportFailure(): FailedReport {
+ const event = deepMerge(
+ {
+ message: `report ${this.report._id} has failed`,
+ event: { kind: 'event', action: ActionType.FAIL_REPORT },
+ log: { level: 'info' },
+ } as Partial,
+ this.eventObj
+ );
+
+ genericLogger.logEvent(event);
+ return event;
+ }
+
+ logReportSaved(): SavedReport {
+ const event = deepMerge(
+ {
+ message: `saved report ${this.report._id}`,
+ event: { kind: 'event', action: ActionType.SAVE_REPORT },
+ log: { level: 'info' },
+ } as Partial,
+ this.eventObj
+ );
+
+ genericLogger.logEvent(event);
+ return event;
+ }
+
+ logRetry(): ScheduledRetry {
+ const event = deepMerge(
+ {
+ message: `scheduled retry for report ${this.report._id}`,
+ event: { kind: 'event', action: ActionType.RETRY },
+ log: { level: 'info' },
+ } as Partial,
+ this.eventObj
+ );
+
+ genericLogger.logEvent(event);
+ return event;
+ }
+ };
+}
+
+export type ReportingEventLogger = ReturnType;
diff --git a/x-pack/plugins/reporting/server/lib/event_logger/types.ts b/x-pack/plugins/reporting/server/lib/event_logger/types.ts
new file mode 100644
index 00000000000000..1c31292d03e446
--- /dev/null
+++ b/x-pack/plugins/reporting/server/lib/event_logger/types.ts
@@ -0,0 +1,68 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { ActionType } from './';
+
+type ActionKind = 'event' | 'error' | 'metrics';
+type ActionOutcome = 'success' | 'failure';
+
+interface ActionBase<
+ A extends ActionType,
+ K extends ActionKind,
+ O extends ActionOutcome,
+ EventProvider
+> {
+ event: {
+ action: A;
+ kind: K;
+ outcome?: O;
+ provider: 'reporting';
+ timezone: string;
+ };
+ kibana: EventProvider & { task?: { id?: string } };
+ user?: { name: string };
+ log: {
+ logger: 'reporting';
+ level: K extends 'error' ? 'error' : 'info';
+ };
+ message: string;
+}
+
+export interface ErrorAction {
+ message: string;
+ code?: string;
+ stack_trace?: string;
+ type?: string;
+}
+
+type ReportingAction<
+ A extends ActionType,
+ K extends ActionKind,
+ O extends ActionOutcome = 'success'
+> = ActionBase<
+ A,
+ K,
+ O,
+ {
+ reporting: {
+ id?: string; // "immediate download" exports have no ID
+ jobType: string;
+ byteSize?: number;
+ };
+ }
+>;
+
+export type ScheduledTask = ReportingAction;
+export type StartedExecution = ReportingAction;
+export type CompletedExecution = ReportingAction;
+export type SavedReport = ReportingAction;
+export type ClaimedTask = ReportingAction;
+export type ScheduledRetry = ReportingAction;
+export type FailedReport = ReportingAction;
+export type ExecuteError = ReportingAction & {
+ error: ErrorAction;
+};
diff --git a/x-pack/plugins/reporting/server/lib/store/index.ts b/x-pack/plugins/reporting/server/lib/store/index.ts
index e5f1c65e47948e..125c592c8626b3 100644
--- a/x-pack/plugins/reporting/server/lib/store/index.ts
+++ b/x-pack/plugins/reporting/server/lib/store/index.ts
@@ -10,3 +10,10 @@ export { Report } from './report';
export { SavedReport } from './saved_report';
export { ReportingStore } from './store';
export { IlmPolicyManager } from './ilm_policy_manager';
+
+export interface IReport {
+ _id?: string;
+ jobtype: string;
+ created_by: string | false;
+ payload: { browserTimezone: string };
+}
diff --git a/x-pack/plugins/reporting/server/lib/store/store.ts b/x-pack/plugins/reporting/server/lib/store/store.ts
index 7ddef6d66e2756..94375d66c00ad3 100644
--- a/x-pack/plugins/reporting/server/lib/store/store.ts
+++ b/x-pack/plugins/reporting/server/lib/store/store.ts
@@ -12,7 +12,7 @@ import { ReportingCore } from '../../';
import { ILM_POLICY_NAME, REPORTING_SYSTEM_INDEX } from '../../../common/constants';
import { JobStatus, ReportOutput, ReportSource } from '../../../common/types';
import { ReportTaskParams } from '../tasks';
-import { Report, ReportDocument, SavedReport } from './';
+import { IReport, Report, ReportDocument, SavedReport } from './';
import { IlmPolicyManager } from './ilm_policy_manager';
import { indexTimestamp } from './index_timestamp';
import { mapping } from './mapping';
@@ -216,8 +216,8 @@ export class ReportingStore {
return report as SavedReport;
} catch (err) {
- this.logger.error(`Error in adding a report!`);
- this.logger.error(err);
+ this.reportingCore.getEventLogger(report).logError(err);
+ this.logError(`Error in adding a report!`, err, report);
throw err;
}
}
@@ -266,6 +266,7 @@ export class ReportingStore {
`[id: ${taskJson.id}] [index: ${taskJson.index}]`
);
this.logger.error(err);
+ this.reportingCore.getEventLogger({ _id: taskJson.id } as IReport).logError(err);
throw err;
}
}
@@ -279,25 +280,33 @@ export class ReportingStore {
status: statuses.JOB_STATUS_PROCESSING,
});
+ let body: UpdateResponse;
try {
const client = await this.getClient();
- const { body } = await client.update({
- id: report._id,
- index: report._index,
- if_seq_no: report._seq_no,
- if_primary_term: report._primary_term,
- refresh: true,
- body: { doc },
- });
-
- return body;
+ body = (
+ await client.update({
+ id: report._id,
+ index: report._index,
+ if_seq_no: report._seq_no,
+ if_primary_term: report._primary_term,
+ refresh: true,
+ body: { doc },
+ })
+ ).body;
} catch (err) {
- this.logger.error(
- `Error in updating status to processing! Report: ` + jobDebugMessage(report)
- );
- this.logger.error(err);
+ this.logError(`Error in updating status to processing! Report: ${jobDebugMessage(report)}`, err, report); // prettier-ignore
throw err;
}
+
+ this.reportingCore.getEventLogger(report).logClaimTask();
+
+ return body;
+ }
+
+ private logError(message: string, err: Error, report: Report) {
+ this.logger.error(message);
+ this.logger.error(err);
+ this.reportingCore.getEventLogger(report).logError(err);
}
public async setReportFailed(
@@ -309,22 +318,27 @@ export class ReportingStore {
status: statuses.JOB_STATUS_FAILED,
});
+ let body: UpdateResponse;
try {
const client = await this.getClient();
- const { body } = await client.update({
- id: report._id,
- index: report._index,
- if_seq_no: report._seq_no,
- if_primary_term: report._primary_term,
- refresh: true,
- body: { doc },
- });
- return body;
+ body = (
+ await client.update({
+ id: report._id,
+ index: report._index,
+ if_seq_no: report._seq_no,
+ if_primary_term: report._primary_term,
+ refresh: true,
+ body: { doc },
+ })
+ ).body;
} catch (err) {
- this.logger.error(`Error in updating status to failed! Report: ` + jobDebugMessage(report));
- this.logger.error(err);
+ this.logError(`Error in updating status to failed! Report: ${jobDebugMessage(report)}`, err, report); // prettier-ignore
throw err;
}
+
+ this.reportingCore.getEventLogger(report).logReportFailure();
+
+ return body;
}
public async setReportCompleted(
@@ -341,22 +355,27 @@ export class ReportingStore {
status,
} as ReportSource);
+ let body: UpdateResponse;
try {
const client = await this.getClient();
- const { body } = await client.update({
- id: report._id,
- index: report._index,
- if_seq_no: report._seq_no,
- if_primary_term: report._primary_term,
- refresh: true,
- body: { doc },
- });
- return body;
+ body = (
+ await client.update({
+ id: report._id,
+ index: report._index,
+ if_seq_no: report._seq_no,
+ if_primary_term: report._primary_term,
+ refresh: true,
+ body: { doc },
+ })
+ ).body;
} catch (err) {
- this.logger.error(`Error in updating status to complete! Report: ` + jobDebugMessage(report));
- this.logger.error(err);
+ this.logError(`Error in updating status to complete! Report: ${jobDebugMessage(report)}`, err, report); // prettier-ignore
throw err;
}
+
+ this.reportingCore.getEventLogger(report).logReportSaved();
+
+ return body;
}
public async prepareReportForRetry(report: SavedReport): Promise> {
@@ -365,24 +384,25 @@ export class ReportingStore {
process_expiration: null,
});
+ let body: UpdateResponse;
try {
const client = await this.getClient();
- const { body } = await client.update({
- id: report._id,
- index: report._index,
- if_seq_no: report._seq_no,
- if_primary_term: report._primary_term,
- refresh: true,
- body: { doc },
- });
- return body;
+ body = (
+ await client.update({
+ id: report._id,
+ index: report._index,
+ if_seq_no: report._seq_no,
+ if_primary_term: report._primary_term,
+ refresh: true,
+ body: { doc },
+ })
+ ).body;
} catch (err) {
- this.logger.error(
- `Error in clearing expiration and status for retry! Report: ` + jobDebugMessage(report)
- );
- this.logger.error(err);
+ this.logError(`Error in clearing expiration and status for retry! Report: ${jobDebugMessage(report)}`, err, report); // prettier-ignore
throw err;
}
+
+ return body;
}
/*
diff --git a/x-pack/plugins/reporting/server/lib/tasks/execute_report.ts b/x-pack/plugins/reporting/server/lib/tasks/execute_report.ts
index a17e3997828a46..dd3f93ad2c0c6c 100644
--- a/x-pack/plugins/reporting/server/lib/tasks/execute_report.ts
+++ b/x-pack/plugins/reporting/server/lib/tasks/execute_report.ts
@@ -333,6 +333,10 @@ export class ExecuteReportTask implements ReportingTask {
);
this.logger.debug(`Reports running: ${this.reporting.countConcurrentReports()}.`);
+ const eventLog = this.reporting.getEventLogger(
+ new Report({ ...task, _id: task.id, _index: task.index })
+ );
+
try {
const jobContentEncoding = this.getJobContentEncoding(jobType);
const stream = await getContentStream(
@@ -347,9 +351,15 @@ export class ExecuteReportTask implements ReportingTask {
encoding: jobContentEncoding === 'base64' ? 'base64' : 'raw',
}
);
+
+ eventLog.logExecutionStart();
+
const output = await this._performJob(task, cancellationToken, stream);
stream.end();
+
+ eventLog.logExecutionComplete({ byteSize: stream.bytesWritten });
+
await promisify(finished)(stream, { readable: false });
report._seq_no = stream.getSeqNo()!;
@@ -365,6 +375,8 @@ export class ExecuteReportTask implements ReportingTask {
// untrack the report for concurrency awareness
this.logger.debug(`Stopping ${jobId}.`);
} catch (failedToExecuteErr) {
+ eventLog.logError(failedToExecuteErr);
+
cancellationToken.cancel();
if (attempts < maxAttempts) {
diff --git a/x-pack/plugins/reporting/server/lib/tasks/monitor_reports.ts b/x-pack/plugins/reporting/server/lib/tasks/monitor_reports.ts
index ce8bb74d666c54..4af28e3d1a6981 100644
--- a/x-pack/plugins/reporting/server/lib/tasks/monitor_reports.ts
+++ b/x-pack/plugins/reporting/server/lib/tasks/monitor_reports.ts
@@ -152,6 +152,9 @@ export class MonitorReportsTask implements ReportingTask {
logger.info(`Rescheduling task:${task.id} to retry.`);
const newTask = await this.reporting.scheduleTask(task);
+
+ this.reporting.getEventLogger({ _id: task.id, ...task }, newTask).logRetry();
+
return newTask;
}
diff --git a/x-pack/plugins/reporting/server/plugin.test.ts b/x-pack/plugins/reporting/server/plugin.test.ts
index 4c04eb0c004e53..7afeedd3d2832a 100644
--- a/x-pack/plugins/reporting/server/plugin.test.ts
+++ b/x-pack/plugins/reporting/server/plugin.test.ts
@@ -5,52 +5,35 @@
* 2.0.
*/
+import { CoreSetup, CoreStart } from 'kibana/server';
import { coreMock } from 'src/core/server/mocks';
-import { featuresPluginMock } from '../../features/server/mocks';
-import { TaskManagerSetupContract } from '../../task_manager/server';
+import { ReportingInternalStart } from './core';
import { ReportingPlugin } from './plugin';
-import { createMockConfigSchema } from './test_helpers';
+import { createMockConfigSchema, createMockPluginSetup } from './test_helpers';
+import {
+ createMockPluginStart,
+ createMockReportingCore,
+} from './test_helpers/create_mock_reportingplugin';
+import { ReportingSetupDeps } from './types';
const sleep = (time: number) => new Promise((r) => setTimeout(r, time));
describe('Reporting Plugin', () => {
let configSchema: any;
let initContext: any;
- let coreSetup: any;
- let coreStart: any;
- let pluginSetup: any;
- let pluginStart: any;
+ let coreSetup: CoreSetup;
+ let coreStart: CoreStart;
+ let pluginSetup: ReportingSetupDeps;
+ let pluginStart: ReportingInternalStart;
beforeEach(async () => {
+ const reportingCore = await createMockReportingCore(createMockConfigSchema());
configSchema = createMockConfigSchema();
initContext = coreMock.createPluginInitializerContext(configSchema);
coreSetup = coreMock.createSetup(configSchema);
coreStart = coreMock.createStart();
- pluginSetup = {
- licensing: {},
- features: featuresPluginMock.createSetup(),
- usageCollection: {
- makeUsageCollector: jest.fn(),
- registerCollector: jest.fn(),
- },
- taskManager: {
- registerTaskDefinitions: jest.fn(),
- } as unknown as TaskManagerSetupContract,
- security: {
- authc: {
- getCurrentUser: () => ({
- id: '123',
- roles: ['superuser'],
- username: 'Tom Riddle',
- }),
- },
- },
- } as unknown as any;
- pluginStart = {
- data: {
- fieldFormats: {},
- },
- } as unknown as any;
+ pluginSetup = createMockPluginSetup({}) as unknown as ReportingSetupDeps;
+ pluginStart = createMockPluginStart(reportingCore, {});
});
it('has a sync setup process', () => {
@@ -70,15 +53,14 @@ describe('Reporting Plugin', () => {
const plugin = new ReportingPlugin(initContext);
plugin.setup(coreSetup, pluginSetup);
expect(coreSetup.uiSettings.register).toHaveBeenCalled();
- expect(coreSetup.uiSettings.register.mock.calls[0][0]).toHaveProperty(
+ expect((coreSetup.uiSettings.register as jest.Mock).mock.calls[0][0]).toHaveProperty(
'xpackReporting:customPdfLogo'
);
});
it('logs start issues', async () => {
const plugin = new ReportingPlugin(initContext);
- // @ts-ignore overloading error logger
- plugin.logger.error = jest.fn();
+ (plugin as unknown as { logger: { error: jest.Mock } }).logger.error = jest.fn();
plugin.setup(coreSetup, pluginSetup);
await sleep(5);
plugin.start(null as any, pluginStart);
diff --git a/x-pack/plugins/reporting/server/plugin.ts b/x-pack/plugins/reporting/server/plugin.ts
index 414966170772c8..942ebbea47881d 100644
--- a/x-pack/plugins/reporting/server/plugin.ts
+++ b/x-pack/plugins/reporting/server/plugin.ts
@@ -11,6 +11,7 @@ import { ReportingCore } from './';
import { buildConfig, registerUiSettings, ReportingConfigType } from './config';
import { registerDeprecations } from './deprecations';
import { LevelLogger, ReportingStore } from './lib';
+import { registerEventLogProviderActions } from './lib/event_logger';
import { registerRoutes } from './routes';
import { setFieldFormats } from './services';
import type {
@@ -37,7 +38,7 @@ export class ReportingPlugin
public setup(core: CoreSetup, plugins: ReportingSetupDeps) {
const { http } = core;
- const { features, licensing, security, spaces, taskManager } = plugins;
+ const { features, licensing, eventLog, security, spaces, taskManager } = plugins;
const reportingCore = new ReportingCore(this.logger, this.initContext);
@@ -57,15 +58,17 @@ export class ReportingPlugin
reportingCore.pluginSetup({
features,
licensing,
- basePath,
- router,
+ eventLog,
security,
spaces,
taskManager,
logger: this.logger,
status: core.status,
+ basePath,
+ router,
});
+ registerEventLogProviderActions(eventLog);
registerUiSettings(core);
registerDeprecations({
core,
diff --git a/x-pack/plugins/reporting/server/routes/generate/csv_searchsource_immediate.ts b/x-pack/plugins/reporting/server/routes/generate/csv_searchsource_immediate.ts
index 52e6eb87e05cde..23f27230b18424 100644
--- a/x-pack/plugins/reporting/server/routes/generate/csv_searchsource_immediate.ts
+++ b/x-pack/plugins/reporting/server/routes/generate/csv_searchsource_immediate.ts
@@ -9,10 +9,12 @@ import { schema } from '@kbn/config-schema';
import { KibanaRequest } from 'src/core/server';
import { Writable } from 'stream';
import { ReportingCore } from '../../';
+import { CSV_SEARCHSOURCE_IMMEDIATE_TYPE } from '../../../common/constants';
import { runTaskFnFactory } from '../../export_types/csv_searchsource_immediate/execute_job';
import { JobParamsDownloadCSV } from '../../export_types/csv_searchsource_immediate/types';
import { LevelLogger as Logger } from '../../lib';
import { TaskRunResult } from '../../lib/tasks';
+import { BaseParams } from '../../types';
import { authorizedUserPreRouting } from '../lib/authorized_user_pre_routing';
import { RequestHandler } from '../lib/request_handler';
@@ -64,10 +66,18 @@ export function registerGenerateCsvFromSavedObjectImmediate(
authorizedUserPreRouting(
reporting,
async (user, context, req: CsvFromSavedObjectRequest, res) => {
- const logger = parentLogger.clone(['csv_searchsource_immediate']);
+ const logger = parentLogger.clone([CSV_SEARCHSOURCE_IMMEDIATE_TYPE]);
const runTaskFn = runTaskFnFactory(reporting, logger);
const requestHandler = new RequestHandler(reporting, user, context, req, res, logger);
+ const eventLog = reporting.getEventLogger({
+ jobtype: CSV_SEARCHSOURCE_IMMEDIATE_TYPE,
+ created_by: user && user.username,
+ payload: { browserTimezone: (req.params as BaseParams).browserTimezone },
+ });
+
+ eventLog.logExecutionStart();
+
try {
let buffer = Buffer.from('');
const stream = new Writable({
@@ -98,6 +108,8 @@ export function registerGenerateCsvFromSavedObjectImmediate(
logger.warn('CSV Job Execution created empty content result');
}
+ eventLog.logExecutionComplete({ byteSize: jobOutputSize });
+
return res.ok({
body: jobOutputContent || '',
headers: {
@@ -107,6 +119,7 @@ export function registerGenerateCsvFromSavedObjectImmediate(
});
} catch (err) {
logger.error(err);
+ eventLog.logError(err);
return requestHandler.handleError(err);
}
}
diff --git a/x-pack/plugins/reporting/server/routes/lib/request_handler.ts b/x-pack/plugins/reporting/server/routes/lib/request_handler.ts
index 998e8d12076b95..b0a2032c18f19e 100644
--- a/x-pack/plugins/reporting/server/routes/lib/request_handler.ts
+++ b/x-pack/plugins/reporting/server/routes/lib/request_handler.ts
@@ -99,6 +99,9 @@ export class RequestHandler {
`Scheduled ${exportType.name} reporting task. Task ID: task:${task.id}. Report ID: ${report._id}`
);
+ // 6. Log the action with event log
+ reporting.getEventLogger(report, task).logScheduleTask();
+
return report;
}
diff --git a/x-pack/plugins/reporting/server/test_helpers/create_mock_reportingplugin.ts b/x-pack/plugins/reporting/server/test_helpers/create_mock_reportingplugin.ts
index 9f1d7e614bd926..9570c82f23a8a2 100644
--- a/x-pack/plugins/reporting/server/test_helpers/create_mock_reportingplugin.ts
+++ b/x-pack/plugins/reporting/server/test_helpers/create_mock_reportingplugin.ts
@@ -39,6 +39,10 @@ export const createMockPluginSetup = (
taskManager: taskManagerMock.createSetup(),
logger: createMockLevelLogger(),
status: statusServiceMock.createSetupContract(),
+ eventLog: setupMock.eventLog || {
+ registerProviderActions: jest.fn(),
+ getLogger: jest.fn(() => ({ logEvent: jest.fn() })),
+ },
...setupMock,
};
};
diff --git a/x-pack/plugins/reporting/server/types.ts b/x-pack/plugins/reporting/server/types.ts
index 114cd77fbf4b53..e23a1d555fdbae 100644
--- a/x-pack/plugins/reporting/server/types.ts
+++ b/x-pack/plugins/reporting/server/types.ts
@@ -11,6 +11,7 @@ import type { DataPluginStart } from 'src/plugins/data/server/plugin';
import type { ScreenshotModePluginSetup } from 'src/plugins/screenshot_mode/server';
import type { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
import type { Writable } from 'stream';
+import { IEventLogService } from '../../event_log/server';
import type { PluginSetupContract as FeaturesPluginSetup } from '../../features/server';
import type { LicensingPluginSetup } from '../../licensing/server';
import type {
@@ -91,6 +92,7 @@ export interface ExportTypeDefinition<
*/
export interface ReportingSetupDeps {
licensing: LicensingPluginSetup;
+ eventLog: IEventLogService;
features: FeaturesPluginSetup;
screenshotMode: ScreenshotModePluginSetup;
security?: SecurityPluginSetup;
diff --git a/x-pack/plugins/reporting/tsconfig.json b/x-pack/plugins/reporting/tsconfig.json
index 4e09708915f952..24db8258566273 100644
--- a/x-pack/plugins/reporting/tsconfig.json
+++ b/x-pack/plugins/reporting/tsconfig.json
@@ -6,28 +6,25 @@
"declaration": true,
"declarationMap": true
},
- "include": [
- "common/**/*",
- "public/**/*",
- "server/**/*",
- "../../../typings/**/*"
- ],
+ "include": ["common/**/*", "public/**/*", "server/**/*", "../../../typings/**/*"],
"references": [
{ "path": "../../../src/core/tsconfig.json" },
- { "path": "../../../src/plugins/data/tsconfig.json"},
+ { "path": "../../../src/plugins/data/tsconfig.json" },
{ "path": "../../../src/plugins/discover/tsconfig.json" },
{ "path": "../../../src/plugins/embeddable/tsconfig.json" },
{ "path": "../../../src/plugins/kibana_react/tsconfig.json" },
+ { "path": "../../../src/plugins/kibana_utils/tsconfig.json" },
{ "path": "../../../src/plugins/management/tsconfig.json" },
{ "path": "../../../src/plugins/screenshot_mode/tsconfig.json" },
{ "path": "../../../src/plugins/share/tsconfig.json" },
{ "path": "../../../src/plugins/ui_actions/tsconfig.json" },
{ "path": "../../../src/plugins/usage_collection/tsconfig.json" },
{ "path": "../../../src/plugins/field_formats/tsconfig.json" },
+ { "path": "../event_log/tsconfig.json" },
{ "path": "../features/tsconfig.json" },
{ "path": "../licensing/tsconfig.json" },
{ "path": "../screenshotting/tsconfig.json" },
{ "path": "../security/tsconfig.json" },
- { "path": "../spaces/tsconfig.json" },
+ { "path": "../spaces/tsconfig.json" }
]
}
diff --git a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.ts b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.ts
index 0256212608820c..9ae3dff28b2ae2 100644
--- a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.ts
+++ b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.ts
@@ -10,6 +10,7 @@ import type { PublicContract } from '@kbn/utility-types';
import { getOrElse } from 'fp-ts/lib/Either';
import * as rt from 'io-ts';
import { v4 } from 'uuid';
+import { difference } from 'lodash';
import {
AlertExecutorOptions,
AlertInstance,
@@ -24,7 +25,6 @@ import {
ALERT_DURATION,
ALERT_END,
ALERT_INSTANCE_ID,
- ALERT_RULE_UUID,
ALERT_START,
ALERT_STATUS,
ALERT_STATUS_ACTIVE,
@@ -39,6 +39,7 @@ import {
} from '../../common/technical_rule_data_field_names';
import { IRuleDataClient } from '../rule_data_client';
import { AlertExecutorOptionsWithExtraServices } from '../types';
+import { fetchExistingAlerts } from './fetch_existing_alerts';
import {
CommonAlertFieldName,
CommonAlertIdFieldName,
@@ -179,13 +180,13 @@ export const createLifecycleExecutor =
const currentAlertIds = Object.keys(currentAlerts);
const trackedAlertIds = Object.keys(state.trackedAlerts);
- const newAlertIds = currentAlertIds.filter((alertId) => !trackedAlertIds.includes(alertId));
+ const newAlertIds = difference(currentAlertIds, trackedAlertIds);
const allAlertIds = [...new Set(currentAlertIds.concat(trackedAlertIds))];
const trackedAlertStates = Object.values(state.trackedAlerts);
logger.debug(
- `Tracking ${allAlertIds.length} alerts (${newAlertIds.length} new, ${trackedAlertStates.length} previous)`
+ `[Rule Registry] Tracking ${allAlertIds.length} alerts (${newAlertIds.length} new, ${trackedAlertStates.length} previous)`
);
const trackedAlertsDataMap: Record<
@@ -194,40 +195,14 @@ export const createLifecycleExecutor =
> = {};
if (trackedAlertStates.length) {
- const { hits } = await ruleDataClient.getReader().search({
- body: {
- query: {
- bool: {
- filter: [
- {
- term: {
- [ALERT_RULE_UUID]: commonRuleFields[ALERT_RULE_UUID],
- },
- },
- {
- terms: {
- [ALERT_UUID]: trackedAlertStates.map(
- (trackedAlertState) => trackedAlertState.alertUuid
- ),
- },
- },
- ],
- },
- },
- size: trackedAlertStates.length,
- collapse: {
- field: ALERT_UUID,
- },
- sort: {
- [TIMESTAMP]: 'desc' as const,
- },
- },
- allow_no_indices: true,
- });
-
- hits.hits.forEach((hit) => {
- const alertId = hit._source[ALERT_INSTANCE_ID];
- if (alertId) {
+ const result = await fetchExistingAlerts(
+ ruleDataClient,
+ trackedAlertStates,
+ commonRuleFields
+ );
+ result.forEach((hit) => {
+ const alertId = hit._source ? hit._source[ALERT_INSTANCE_ID] : void 0;
+ if (alertId && hit._source) {
trackedAlertsDataMap[alertId] = {
indexName: hit._index,
fields: hit._source,
@@ -242,7 +217,7 @@ export const createLifecycleExecutor =
const currentAlertData = currentAlerts[alertId];
if (!alertData) {
- logger.warn(`Could not find alert data for ${alertId}`);
+ logger.debug(`[Rule Registry] Could not find alert data for ${alertId}`);
}
const isNew = !state.trackedAlerts[alertId];
@@ -291,7 +266,7 @@ export const createLifecycleExecutor =
const writeAlerts = ruleDataClient.isWriteEnabled() && shouldWriteAlerts();
if (allEventsToIndex.length > 0 && writeAlerts) {
- logger.debug(`Preparing to index ${allEventsToIndex.length} alerts.`);
+ logger.debug(`[Rule Registry] Preparing to index ${allEventsToIndex.length} alerts.`);
await ruleDataClient.getWriter().bulk({
body: allEventsToIndex.flatMap(({ event, indexName }) => [
diff --git a/x-pack/plugins/rule_registry/server/utils/fetch_existing_alerts.ts b/x-pack/plugins/rule_registry/server/utils/fetch_existing_alerts.ts
new file mode 100644
index 00000000000000..892e237f8e301f
--- /dev/null
+++ b/x-pack/plugins/rule_registry/server/utils/fetch_existing_alerts.ts
@@ -0,0 +1,74 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { chunk } from 'lodash';
+import { PublicContract } from '@kbn/utility-types';
+import { IRuleDataClient } from '../rule_data_client';
+import {
+ ALERT_RULE_UUID,
+ ALERT_UUID,
+ TIMESTAMP,
+} from '../../common/technical_rule_data_field_names';
+
+const CHUNK_SIZE = 10000;
+
+interface TrackedAlertState {
+ alertId: string;
+ alertUuid: string;
+ started: string;
+}
+type RuleDataClient = PublicContract;
+
+const fetchAlertsForStates = async (
+ ruleDataClient: RuleDataClient,
+ states: TrackedAlertState[],
+ commonRuleFields: any
+) => {
+ const request = {
+ body: {
+ query: {
+ bool: {
+ filter: [
+ {
+ term: {
+ [ALERT_RULE_UUID]: commonRuleFields[ALERT_RULE_UUID],
+ },
+ },
+ {
+ terms: {
+ [ALERT_UUID]: states.map((state) => state.alertUuid),
+ },
+ },
+ ],
+ },
+ },
+ size: states.length,
+ collapse: {
+ field: ALERT_UUID,
+ },
+ sort: {
+ [TIMESTAMP]: 'desc' as const,
+ },
+ },
+ allow_no_indices: true,
+ } as any;
+ const { hits } = await ruleDataClient.getReader().search(request);
+ return hits.hits;
+};
+
+export const fetchExistingAlerts = async (
+ ruleDataClient: RuleDataClient,
+ trackedAlertStates: TrackedAlertState[],
+ commonRuleFields: any
+) => {
+ const results = await Promise.all(
+ chunk(trackedAlertStates, CHUNK_SIZE).map((states) =>
+ fetchAlertsForStates(ruleDataClient, states, commonRuleFields)
+ )
+ );
+ return results.flat();
+};
diff --git a/x-pack/plugins/runtime_fields/README.md b/x-pack/plugins/runtime_fields/README.md
index eb7b31e6e1154e..9c0e0e03f2fe5d 100644
--- a/x-pack/plugins/runtime_fields/README.md
+++ b/x-pack/plugins/runtime_fields/README.md
@@ -72,7 +72,7 @@ interface RuntimeField {
type: RuntimeType; // 'long' | 'boolean' ...
script: {
source: string;
- }
+ };
}
```
@@ -103,8 +103,8 @@ interface Context {
The runtime field editor is also exported as static React component that you can import into your components. The editor is exported in 2 flavours:
-* As the content of a ` ` (it contains a flyout header and footer)
-* As a standalone component that you can inline anywhere
+- As the content of a ` ` (it contains a flyout header and footer)
+- As a standalone component that you can inline anywhere
**Note:** The runtime field editor uses the ` ` that has a dependency on the `Provider` from the `"kibana_react"` plugin. If your app is not already wrapped by this provider you will need to add it at least around the runtime field editor. You can see an example in the ["Using the core.overlays.openFlyout()"](#using-the-coreoverlaysopenflyout) example below.
@@ -118,7 +118,7 @@ import { RuntimeFieldEditorFlyoutContent, RuntimeField } from '../runtime_fields
const MyComponent = () => {
const { docLinksStart } = useCoreContext(); // access the core start service
const [isFlyoutVisilbe, setIsFlyoutVisible] = useState(false);
-
+
const saveRuntimeField = useCallback((field: RuntimeField) => {
// Do something with the field
}, []);
@@ -139,7 +139,7 @@ const MyComponent = () => {
)}
>
- )
+ )
}
```
@@ -157,11 +157,11 @@ import { RuntimeFieldEditorFlyoutContent, RuntimeField } from '../runtime_fields
const MyComponent = () => {
// Access the core start service
- const { docLinksStart, overlays, uiSettings } = useCoreContext();
+ const { docLinksStart, theme, overlays, uiSettings } = useCoreContext();
const flyoutEditor = useRef(null);
const { openFlyout } = overlays;
-
+
const saveRuntimeField = useCallback((field: RuntimeField) => {
// Do something with the field
}, []);
@@ -179,7 +179,8 @@ const MyComponent = () => {
defaultValue={defaultRuntimeField}
ctx={/*optional context object -- see section above*/}
/>
-
+ ,
+ { theme$: theme.theme$ }
)
);
}, [openFlyout, saveRuntimeField, uiSettings]);
@@ -188,7 +189,7 @@ const MyComponent = () => {
<>
Create field
>
- )
+ )
}
```
@@ -208,7 +209,7 @@ const MyComponent = () => {
});
const { submit, isValid: isFormValid, isSubmitted } = runtimeFieldFormState;
-
+
const saveRuntimeField = useCallback(async () => {
const { isValid, data } = await submit();
if (isValid) {
@@ -233,6 +234,6 @@ const MyComponent = () => {
Save field
>
- )
+ )
}
-```
\ No newline at end of file
+```
diff --git a/x-pack/plugins/runtime_fields/public/load_editor.tsx b/x-pack/plugins/runtime_fields/public/load_editor.tsx
index 0cea90f33a54de..6aec33b90466f4 100644
--- a/x-pack/plugins/runtime_fields/public/load_editor.tsx
+++ b/x-pack/plugins/runtime_fields/public/load_editor.tsx
@@ -22,7 +22,7 @@ export const getRuntimeFieldEditorLoader =
(coreSetup: CoreSetup) => async (): Promise => {
const { RuntimeFieldEditorFlyoutContent } = await import('./components');
const [core] = await coreSetup.getStartServices();
- const { uiSettings, overlays, docLinks } = core;
+ const { uiSettings, theme, overlays, docLinks } = core;
const { Provider: KibanaReactContextProvider } = createKibanaReactContext({ uiSettings });
let overlayRef: OverlayRef | null = null;
@@ -50,7 +50,8 @@ export const getRuntimeFieldEditorLoader =
defaultValue={defaultValue}
ctx={ctx}
/>
-
+ ,
+ { theme$: theme.theme$ }
)
);
diff --git a/x-pack/plugins/runtime_fields/public/plugin.test.ts b/x-pack/plugins/runtime_fields/public/plugin.test.ts
index 0f72d99ec5d4f2..fc36eecc12f0a1 100644
--- a/x-pack/plugins/runtime_fields/public/plugin.test.ts
+++ b/x-pack/plugins/runtime_fields/public/plugin.test.ts
@@ -6,7 +6,7 @@
*/
import { CoreSetup } from 'src/core/public';
-import { coreMock } from 'src/core/public/mocks';
+import { coreMock, themeServiceMock } from 'src/core/public/mocks';
jest.mock('../../../../src/plugins/kibana_react/public', () => {
const original = jest.requireActual('../../../../src/plugins/kibana_react/public');
@@ -52,6 +52,7 @@ describe('RuntimeFieldsPlugin', () => {
openFlyout,
},
uiSettings: {},
+ theme: themeServiceMock.createStartContract(),
};
coreSetup.getStartServices = async () => [mockCore] as any;
const setupApi = await plugin.setup(coreSetup, {});
diff --git a/x-pack/plugins/security/server/audit/audit_events.test.ts b/x-pack/plugins/security/server/audit/audit_events.test.ts
index df796b0603176b..0a7337e4532747 100644
--- a/x-pack/plugins/security/server/audit/audit_events.test.ts
+++ b/x-pack/plugins/security/server/audit/audit_events.test.ts
@@ -15,6 +15,7 @@ import {
httpRequestEvent,
SavedObjectAction,
savedObjectEvent,
+ sessionCleanupEvent,
SpaceAuditAction,
spaceAuditEvent,
userLoginEvent,
@@ -352,6 +353,37 @@ describe('#userLogoutEvent', () => {
});
});
+describe('#sessionCleanupEvent', () => {
+ test('creates event with `unknown` outcome', () => {
+ expect(
+ sessionCleanupEvent({
+ usernameHash: 'abcdef',
+ sessionId: 'sid',
+ provider: { name: 'basic1', type: 'basic' },
+ })
+ ).toMatchInlineSnapshot(`
+ Object {
+ "event": Object {
+ "action": "session_cleanup",
+ "category": Array [
+ "authentication",
+ ],
+ "outcome": "unknown",
+ },
+ "kibana": Object {
+ "authentication_provider": "basic1",
+ "authentication_type": "basic",
+ "session_id": "sid",
+ },
+ "message": "Removing invalid or expired session for user [hash=abcdef]",
+ "user": Object {
+ "hash": "abcdef",
+ },
+ }
+ `);
+ });
+});
+
describe('#httpRequestEvent', () => {
test('creates event with `unknown` outcome', () => {
expect(
diff --git a/x-pack/plugins/security/server/audit/audit_events.ts b/x-pack/plugins/security/server/audit/audit_events.ts
index 96bc85c1be37ee..2dfaf8ece004fc 100644
--- a/x-pack/plugins/security/server/audit/audit_events.ts
+++ b/x-pack/plugins/security/server/audit/audit_events.ts
@@ -156,6 +156,35 @@ export function userLogoutEvent({ username, provider }: UserLogoutParams): Audit
};
}
+export interface SessionCleanupParams {
+ sessionId: string;
+ usernameHash?: string;
+ provider: AuthenticationProvider;
+}
+
+export function sessionCleanupEvent({
+ usernameHash,
+ sessionId,
+ provider,
+}: SessionCleanupParams): AuditEvent {
+ return {
+ message: `Removing invalid or expired session for user [hash=${usernameHash}]`,
+ event: {
+ action: 'session_cleanup',
+ category: ['authentication'],
+ outcome: 'unknown',
+ },
+ user: {
+ hash: usernameHash,
+ },
+ kibana: {
+ session_id: sessionId,
+ authentication_provider: provider.name,
+ authentication_type: provider.type,
+ },
+ };
+}
+
export interface AccessAgreementAcknowledgedParams {
username: string;
provider: AuthenticationProvider;
diff --git a/x-pack/plugins/security/server/audit/audit_service.test.ts b/x-pack/plugins/security/server/audit/audit_service.test.ts
index 493490a8e8b9f5..1815f617dceaea 100644
--- a/x-pack/plugins/security/server/audit/audit_service.test.ts
+++ b/x-pack/plugins/security/server/audit/audit_service.test.ts
@@ -67,6 +67,9 @@ describe('#setup', () => {
).toMatchInlineSnapshot(`
Object {
"asScoped": [Function],
+ "withoutRequest": Object {
+ "log": [Function],
+ },
}
`);
audit.stop();
@@ -254,6 +257,82 @@ describe('#asScoped', () => {
});
});
+describe('#withoutRequest', () => {
+ it('logs event without additional meta data', async () => {
+ const audit = new AuditService(logger);
+ const auditSetup = audit.setup({
+ license,
+ config,
+ logging,
+ http,
+ getCurrentUser,
+ getSpaceId,
+ getSID,
+ recordAuditLoggingUsage,
+ });
+
+ await auditSetup.withoutRequest.log({ message: 'MESSAGE', event: { action: 'ACTION' } });
+ expect(logger.info).toHaveBeenCalledWith('MESSAGE', {
+ event: { action: 'ACTION' },
+ });
+ audit.stop();
+ });
+
+ it('does not log to audit logger if event matches ignore filter', async () => {
+ const audit = new AuditService(logger);
+ const auditSetup = audit.setup({
+ license,
+ config: {
+ enabled: true,
+ appender: {
+ type: 'console',
+ layout: {
+ type: 'json',
+ },
+ },
+ ignore_filters: [{ actions: ['ACTION'] }],
+ },
+ logging,
+ http,
+ getCurrentUser,
+ getSpaceId,
+ getSID,
+ recordAuditLoggingUsage,
+ });
+
+ await auditSetup.withoutRequest.log({ message: 'MESSAGE', event: { action: 'ACTION' } });
+ expect(logger.info).not.toHaveBeenCalled();
+ audit.stop();
+ });
+
+ it('does not log to audit logger if no event was generated', async () => {
+ const audit = new AuditService(logger);
+ const auditSetup = audit.setup({
+ license,
+ config: {
+ enabled: true,
+ appender: {
+ type: 'console',
+ layout: {
+ type: 'json',
+ },
+ },
+ ignore_filters: [{ actions: ['ACTION'] }],
+ },
+ logging,
+ http,
+ getCurrentUser,
+ getSpaceId,
+ getSID,
+ recordAuditLoggingUsage,
+ });
+
+ await auditSetup.withoutRequest.log(undefined);
+ expect(logger.info).not.toHaveBeenCalled();
+ audit.stop();
+ });
+});
+
describe('#createLoggingConfig', () => {
test('sets log level to `info` when audit logging is enabled and appender is defined', async () => {
const features$ = of({
diff --git a/x-pack/plugins/security/server/audit/audit_service.ts b/x-pack/plugins/security/server/audit/audit_service.ts
index fb03669ca0fc5c..a29ec221b34744 100644
--- a/x-pack/plugins/security/server/audit/audit_service.ts
+++ b/x-pack/plugins/security/server/audit/audit_service.ts
@@ -26,11 +26,58 @@ export const ECS_VERSION = '1.6.0';
export const RECORD_USAGE_INTERVAL = 60 * 60 * 1000; // 1 hour
export interface AuditLogger {
+ /**
+ * Logs an {@link AuditEvent} and automatically adds meta data about the
+ * current user, space and correlation id.
+ *
+ * Guidelines around what events should be logged and how they should be
+ * structured can be found in: `/x-pack/plugins/security/README.md`
+ *
+ * @example
+ * ```typescript
+ * const auditLogger = securitySetup.audit.asScoped(request);
+ * auditLogger.log({
+ * message: 'User is updating dashboard [id=123]',
+ * event: {
+ * action: 'saved_object_update',
+ * outcome: 'unknown'
+ * },
+ * kibana: {
+ * saved_object: { type: 'dashboard', id: '123' }
+ * },
+ * });
+ * ```
+ */
log: (event: AuditEvent | undefined) => void;
}
export interface AuditServiceSetup {
+ /**
+ * Creates an {@link AuditLogger} scoped to the current request.
+ *
+ * This audit logger logs events with all required user and session info and should be used for
+ * all user-initiated actions.
+ *
+ * @example
+ * ```typescript
+ * const auditLogger = securitySetup.audit.asScoped(request);
+ * auditLogger.log(event);
+ * ```
+ */
asScoped: (request: KibanaRequest) => AuditLogger;
+
+ /**
+ * {@link AuditLogger} for background tasks only.
+ *
+ * This audit logger logs events without any user or session info and should never be used to log
+ * user-initiated actions.
+ *
+ * @example
+ * ```typescript
+ * securitySetup.audit.withoutRequest.log(event);
+ * ```
+ */
+ withoutRequest: AuditLogger;
}
interface AuditServiceSetupParams {
@@ -88,46 +135,25 @@ export class AuditService {
});
}
- /**
- * Creates an {@link AuditLogger} scoped to the current request.
- *
- * @example
- * ```typescript
- * const auditLogger = securitySetup.audit.asScoped(request);
- * auditLogger.log(event);
- * ```
- */
- const asScoped = (request: KibanaRequest): AuditLogger => {
- /**
- * Logs an {@link AuditEvent} and automatically adds meta data about the
- * current user, space and correlation id.
- *
- * Guidelines around what events should be logged and how they should be
- * structured can be found in: `/x-pack/plugins/security/README.md`
- *
- * @example
- * ```typescript
- * const auditLogger = securitySetup.audit.asScoped(request);
- * auditLogger.log({
- * message: 'User is updating dashboard [id=123]',
- * event: {
- * action: 'saved_object_update',
- * outcome: 'unknown'
- * },
- * kibana: {
- * saved_object: { type: 'dashboard', id: '123' }
- * },
- * });
- * ```
- */
- const log: AuditLogger['log'] = async (event) => {
+ const log = (event: AuditEvent | undefined) => {
+ if (!event) {
+ return;
+ }
+ if (filterEvent(event, config.ignore_filters)) {
+ const { message, ...eventMeta } = event;
+ this.logger.info(message, eventMeta);
+ }
+ };
+
+ const asScoped = (request: KibanaRequest): AuditLogger => ({
+ log: async (event) => {
if (!event) {
return;
}
const spaceId = getSpaceId(request);
const user = getCurrentUser(request);
const sessionId = await getSID(request);
- const meta: AuditEvent = {
+ log({
...event,
user:
(user && {
@@ -141,14 +167,9 @@ export class AuditService {
...event.kibana,
},
trace: { id: request.id },
- };
- if (filterEvent(meta, config.ignore_filters)) {
- const { message, ...eventMeta } = meta;
- this.logger.info(message, eventMeta);
- }
- };
- return { log };
- };
+ });
+ },
+ });
http.registerOnPostAuth((request, response, t) => {
if (request.auth.isAuthenticated) {
@@ -157,7 +178,10 @@ export class AuditService {
return t.next();
});
- return { asScoped };
+ return {
+ asScoped,
+ withoutRequest: { log },
+ };
}
stop() {
diff --git a/x-pack/plugins/security/server/audit/index.mock.ts b/x-pack/plugins/security/server/audit/index.mock.ts
index ce6885aee50def..c84faacff01478 100644
--- a/x-pack/plugins/security/server/audit/index.mock.ts
+++ b/x-pack/plugins/security/server/audit/index.mock.ts
@@ -14,6 +14,9 @@ export const auditServiceMock = {
asScoped: jest.fn().mockReturnValue({
log: jest.fn(),
}),
+ withoutRequest: {
+ log: jest.fn(),
+ },
} as jest.Mocked>;
},
};
diff --git a/x-pack/plugins/security/server/audit/index.ts b/x-pack/plugins/security/server/audit/index.ts
index f83c7a7f3bd8a4..0bd8492b796706 100644
--- a/x-pack/plugins/security/server/audit/index.ts
+++ b/x-pack/plugins/security/server/audit/index.ts
@@ -11,6 +11,7 @@ export type { AuditEvent } from './audit_events';
export {
userLoginEvent,
userLogoutEvent,
+ sessionCleanupEvent,
accessAgreementAcknowledgedEvent,
httpRequestEvent,
savedObjectEvent,
diff --git a/x-pack/plugins/security/server/plugin.test.ts b/x-pack/plugins/security/server/plugin.test.ts
index 3d43129b638098..85c2fff5a438ee 100644
--- a/x-pack/plugins/security/server/plugin.test.ts
+++ b/x-pack/plugins/security/server/plugin.test.ts
@@ -67,6 +67,9 @@ describe('Security Plugin', () => {
Object {
"audit": Object {
"asScoped": [Function],
+ "withoutRequest": Object {
+ "log": [Function],
+ },
},
"authc": Object {
"getCurrentUser": [Function],
diff --git a/x-pack/plugins/security/server/plugin.ts b/x-pack/plugins/security/server/plugin.ts
index 1fc3932bb551bf..36be02138289a5 100644
--- a/x-pack/plugins/security/server/plugin.ts
+++ b/x-pack/plugins/security/server/plugin.ts
@@ -310,9 +310,7 @@ export class SecurityPlugin
});
return Object.freeze({
- audit: {
- asScoped: this.auditSetup.asScoped,
- },
+ audit: this.auditSetup,
authc: { getCurrentUser: (request) => this.getAuthentication().getCurrentUser(request) },
authz: {
actions: this.authorizationSetup.actions,
@@ -347,6 +345,7 @@ export class SecurityPlugin
const clusterClient = core.elasticsearch.client;
const { watchOnlineStatus$ } = this.elasticsearchService.start();
const { session } = this.sessionManagementService.start({
+ auditLogger: this.auditSetup!.withoutRequest,
elasticsearchClient: clusterClient.asInternalUser,
kibanaIndexName: this.getKibanaIndexName(),
online$: watchOnlineStatus$(),
diff --git a/x-pack/plugins/security/server/session_management/session_index.test.ts b/x-pack/plugins/security/server/session_management/session_index.test.ts
index 251a0a3edb0613..45ce865de56357 100644
--- a/x-pack/plugins/security/server/session_management/session_index.test.ts
+++ b/x-pack/plugins/security/server/session_management/session_index.test.ts
@@ -6,11 +6,19 @@
*/
import { errors } from '@elastic/elasticsearch';
+import type {
+ BulkResponse,
+ ClosePointInTimeResponse,
+ OpenPointInTimeResponse,
+ SearchResponse,
+} from '@elastic/elasticsearch/lib/api/types';
import type { DeeplyMockedKeys } from '@kbn/utility-types/jest';
import type { ElasticsearchClient } from 'src/core/server';
import { elasticsearchServiceMock, loggingSystemMock } from 'src/core/server/mocks';
+import type { AuditLogger } from '../audit';
+import { auditServiceMock } from '../audit/index.mock';
import { ConfigSchema, createConfig } from '../config';
import { securityMock } from '../mocks';
import { getSessionIndexTemplate, SessionIndex } from './session_index';
@@ -19,11 +27,13 @@ import { sessionIndexMock } from './session_index.mock';
describe('Session index', () => {
let mockElasticsearchClient: DeeplyMockedKeys;
let sessionIndex: SessionIndex;
+ let auditLogger: AuditLogger;
const indexName = '.kibana_some_tenant_security_session_1';
const indexTemplateName = '.kibana_some_tenant_security_session_index_template_1';
beforeEach(() => {
mockElasticsearchClient = elasticsearchServiceMock.createElasticsearchClient();
- const sessionIndexOptions = {
+ auditLogger = auditServiceMock.create().withoutRequest;
+ sessionIndex = new SessionIndex({
logger: loggingSystemMock.createLogger(),
kibanaIndexName: '.kibana_some_tenant',
config: createConfig(
@@ -32,9 +42,8 @@ describe('Session index', () => {
{ isTLSEnabled: false }
),
elasticsearchClient: mockElasticsearchClient,
- };
-
- sessionIndex = new SessionIndex(sessionIndexOptions);
+ auditLogger,
+ });
});
describe('#initialize', () => {
@@ -219,74 +228,130 @@ describe('Session index', () => {
describe('#cleanUp', () => {
const now = 123456;
+ const sessionValue = {
+ _id: 'SESSION_ID',
+ _source: { usernameHash: 'USERNAME_HASH', provider: { name: 'basic1', type: 'basic' } },
+ sort: [0],
+ };
beforeEach(() => {
- mockElasticsearchClient.deleteByQuery.mockResolvedValue(
- securityMock.createApiResponse({ body: {} as any })
+ mockElasticsearchClient.openPointInTime.mockResolvedValue(
+ securityMock.createApiResponse({
+ body: { id: 'PIT_ID' } as OpenPointInTimeResponse,
+ })
+ );
+ mockElasticsearchClient.closePointInTime.mockResolvedValue(
+ securityMock.createApiResponse({
+ body: { succeeded: true, num_freed: 1 } as ClosePointInTimeResponse,
+ })
+ );
+ mockElasticsearchClient.search.mockResolvedValue(
+ securityMock.createApiResponse({
+ body: {
+ hits: { hits: [sessionValue] },
+ } as SearchResponse,
+ })
+ );
+ mockElasticsearchClient.bulk.mockResolvedValue(
+ securityMock.createApiResponse({
+ body: { items: [{}] } as BulkResponse,
+ })
);
jest.spyOn(Date, 'now').mockImplementation(() => now);
});
- it('throws if call to Elasticsearch fails', async () => {
+ it('throws if search call to Elasticsearch fails', async () => {
const failureReason = new errors.ResponseError(
securityMock.createApiResponse(securityMock.createApiResponse({ body: { type: 'Uh oh.' } }))
);
- mockElasticsearchClient.deleteByQuery.mockRejectedValue(failureReason);
+ mockElasticsearchClient.search.mockRejectedValue(failureReason);
await expect(sessionIndex.cleanUp()).rejects.toBe(failureReason);
+ expect(mockElasticsearchClient.openPointInTime).toHaveBeenCalledTimes(1);
+ expect(mockElasticsearchClient.search).toHaveBeenCalledTimes(1);
+ expect(mockElasticsearchClient.bulk).not.toHaveBeenCalled();
+ expect(mockElasticsearchClient.closePointInTime).toHaveBeenCalledTimes(1);
+ });
+
+ it('throws if bulk delete call to Elasticsearch fails', async () => {
+ const failureReason = new errors.ResponseError(
+ securityMock.createApiResponse(securityMock.createApiResponse({ body: { type: 'Uh oh.' } }))
+ );
+ mockElasticsearchClient.bulk.mockRejectedValue(failureReason);
+
+ await expect(sessionIndex.cleanUp()).rejects.toBe(failureReason);
+ expect(mockElasticsearchClient.openPointInTime).toHaveBeenCalledTimes(1);
+ expect(mockElasticsearchClient.search).toHaveBeenCalledTimes(1);
+ expect(mockElasticsearchClient.bulk).toHaveBeenCalledTimes(1);
+ expect(mockElasticsearchClient.closePointInTime).toHaveBeenCalledTimes(1);
});
it('when neither `lifespan` nor `idleTimeout` is configured', async () => {
await sessionIndex.cleanUp();
- expect(mockElasticsearchClient.deleteByQuery).toHaveBeenCalledTimes(1);
- expect(mockElasticsearchClient.deleteByQuery).toHaveBeenCalledWith(
- {
- index: indexName,
- refresh: true,
- body: {
- query: {
- bool: {
- should: [
- // All expired sessions based on the lifespan, no matter which provider they belong to.
- { range: { lifespanExpiration: { lte: now } } },
- // All sessions that belong to the providers that aren't configured.
- {
+ expect(mockElasticsearchClient.openPointInTime).toHaveBeenCalledTimes(1);
+ expect(mockElasticsearchClient.search).toHaveBeenCalledTimes(1);
+ expect(mockElasticsearchClient.search).toHaveBeenCalledWith({
+ _source_includes: 'usernameHash,provider',
+ sort: '_shard_doc',
+ track_total_hits: false,
+ search_after: undefined,
+ size: 10_000,
+ pit: {
+ id: 'PIT_ID',
+ keep_alive: '5m',
+ },
+ query: {
+ bool: {
+ should: [
+ // All expired sessions based on the lifespan, no matter which provider they belong to.
+ { range: { lifespanExpiration: { lte: now } } },
+ // All sessions that belong to the providers that aren't configured.
+ {
+ bool: {
+ must_not: {
bool: {
- must_not: {
- bool: {
- should: [
- {
- bool: {
- must: [
- { term: { 'provider.type': 'basic' } },
- { term: { 'provider.name': 'basic' } },
- ],
- },
- },
- ],
- minimum_should_match: 1,
+ should: [
+ {
+ bool: {
+ must: [
+ { term: { 'provider.type': 'basic' } },
+ { term: { 'provider.name': 'basic' } },
+ ],
+ },
},
- },
- },
- },
- // The sessions that belong to a particular provider that are expired based on the idle timeout.
- {
- bool: {
- must: [
- { term: { 'provider.type': 'basic' } },
- { term: { 'provider.name': 'basic' } },
],
- should: [{ range: { idleTimeoutExpiration: { lte: now } } }],
minimum_should_match: 1,
},
},
- ],
+ },
},
- },
+ // The sessions that belong to a particular provider that are expired based on the idle timeout.
+ {
+ bool: {
+ must: [
+ { term: { 'provider.type': 'basic' } },
+ { term: { 'provider.name': 'basic' } },
+ ],
+ should: [{ range: { idleTimeoutExpiration: { lte: now } } }],
+ minimum_should_match: 1,
+ },
+ },
+ ],
},
},
- { ignore: [409, 404] }
+ });
+ expect(mockElasticsearchClient.bulk).toHaveBeenCalledTimes(1);
+ expect(mockElasticsearchClient.bulk).toHaveBeenCalledWith(
+ {
+ index: indexName,
+ operations: [{ delete: { _id: sessionValue._id } }],
+ refresh: false,
+ },
+ {
+ ignore: [409, 404],
+ }
);
+ expect(mockElasticsearchClient.closePointInTime).toHaveBeenCalledTimes(1);
});
it('when only `lifespan` is configured', async () => {
@@ -299,69 +364,85 @@ describe('Session index', () => {
{ isTLSEnabled: false }
),
elasticsearchClient: mockElasticsearchClient,
+ auditLogger: auditServiceMock.create().withoutRequest,
});
await sessionIndex.cleanUp();
- expect(mockElasticsearchClient.deleteByQuery).toHaveBeenCalledTimes(1);
- expect(mockElasticsearchClient.deleteByQuery).toHaveBeenCalledWith(
- {
- index: indexName,
- refresh: true,
- body: {
- query: {
- bool: {
- should: [
- // All expired sessions based on the lifespan, no matter which provider they belong to.
- { range: { lifespanExpiration: { lte: now } } },
- // All sessions that belong to the providers that aren't configured.
- {
+ expect(mockElasticsearchClient.openPointInTime).toHaveBeenCalledTimes(1);
+ expect(mockElasticsearchClient.search).toHaveBeenCalledTimes(1);
+ expect(mockElasticsearchClient.search).toHaveBeenCalledWith({
+ _source_includes: 'usernameHash,provider',
+ sort: '_shard_doc',
+ track_total_hits: false,
+ search_after: undefined,
+ size: 10_000,
+ pit: {
+ id: 'PIT_ID',
+ keep_alive: '5m',
+ },
+ query: {
+ bool: {
+ should: [
+ // All expired sessions based on the lifespan, no matter which provider they belong to.
+ { range: { lifespanExpiration: { lte: now } } },
+ // All sessions that belong to the providers that aren't configured.
+ {
+ bool: {
+ must_not: {
bool: {
- must_not: {
- bool: {
- should: [
- {
- bool: {
- must: [
- { term: { 'provider.type': 'basic' } },
- { term: { 'provider.name': 'basic' } },
- ],
- },
- },
- ],
- minimum_should_match: 1,
+ should: [
+ {
+ bool: {
+ must: [
+ { term: { 'provider.type': 'basic' } },
+ { term: { 'provider.name': 'basic' } },
+ ],
+ },
},
- },
- },
- },
- // The sessions that belong to a particular provider but don't have a configured lifespan.
- {
- bool: {
- must: [
- { term: { 'provider.type': 'basic' } },
- { term: { 'provider.name': 'basic' } },
- ],
- must_not: { exists: { field: 'lifespanExpiration' } },
- },
- },
- // The sessions that belong to a particular provider that are expired based on the idle timeout.
- {
- bool: {
- must: [
- { term: { 'provider.type': 'basic' } },
- { term: { 'provider.name': 'basic' } },
],
- should: [{ range: { idleTimeoutExpiration: { lte: now } } }],
minimum_should_match: 1,
},
},
- ],
+ },
},
- },
+ // The sessions that belong to a particular provider but don't have a configured lifespan.
+ {
+ bool: {
+ must: [
+ { term: { 'provider.type': 'basic' } },
+ { term: { 'provider.name': 'basic' } },
+ ],
+ must_not: { exists: { field: 'lifespanExpiration' } },
+ },
+ },
+ // The sessions that belong to a particular provider that are expired based on the idle timeout.
+ {
+ bool: {
+ must: [
+ { term: { 'provider.type': 'basic' } },
+ { term: { 'provider.name': 'basic' } },
+ ],
+ should: [{ range: { idleTimeoutExpiration: { lte: now } } }],
+ minimum_should_match: 1,
+ },
+ },
+ ],
},
},
- { ignore: [409, 404] }
+ });
+ expect(mockElasticsearchClient.bulk).toHaveBeenCalledTimes(1);
+ expect(mockElasticsearchClient.bulk).toHaveBeenCalledWith(
+ {
+ index: indexName,
+ operations: [{ delete: { _id: sessionValue._id } }],
+ refresh: false,
+ },
+ {
+ ignore: [409, 404],
+ }
);
+ expect(mockElasticsearchClient.closePointInTime).toHaveBeenCalledTimes(1);
});
it('when only `idleTimeout` is configured', async () => {
@@ -375,63 +456,79 @@ describe('Session index', () => {
{ isTLSEnabled: false }
),
elasticsearchClient: mockElasticsearchClient,
+ auditLogger: auditServiceMock.create().withoutRequest,
});
await sessionIndex.cleanUp();
- expect(mockElasticsearchClient.deleteByQuery).toHaveBeenCalledTimes(1);
- expect(mockElasticsearchClient.deleteByQuery).toHaveBeenCalledWith(
- {
- index: indexName,
- refresh: true,
- body: {
- query: {
- bool: {
- should: [
- // All expired sessions based on the lifespan, no matter which provider they belong to.
- { range: { lifespanExpiration: { lte: now } } },
- // All sessions that belong to the providers that aren't configured.
- {
- bool: {
- must_not: {
- bool: {
- should: [
- {
- bool: {
- must: [
- { term: { 'provider.type': 'basic' } },
- { term: { 'provider.name': 'basic' } },
- ],
- },
- },
- ],
- minimum_should_match: 1,
- },
- },
- },
- },
- // The sessions that belong to a particular provider that are either expired based on the idle timeout
- // or don't have it configured at all.
- {
+ expect(mockElasticsearchClient.openPointInTime).toHaveBeenCalledTimes(1);
+ expect(mockElasticsearchClient.search).toHaveBeenCalledTimes(1);
+ expect(mockElasticsearchClient.search).toHaveBeenCalledWith({
+ _source_includes: 'usernameHash,provider',
+ sort: '_shard_doc',
+ track_total_hits: false,
+ search_after: undefined,
+ size: 10_000,
+ pit: {
+ id: 'PIT_ID',
+ keep_alive: '5m',
+ },
+ query: {
+ bool: {
+ should: [
+ // All expired sessions based on the lifespan, no matter which provider they belong to.
+ { range: { lifespanExpiration: { lte: now } } },
+ // All sessions that belong to the providers that aren't configured.
+ {
+ bool: {
+ must_not: {
bool: {
- must: [
- { term: { 'provider.type': 'basic' } },
- { term: { 'provider.name': 'basic' } },
- ],
should: [
- { range: { idleTimeoutExpiration: { lte: now - 3 * idleTimeout } } },
- { bool: { must_not: { exists: { field: 'idleTimeoutExpiration' } } } },
+ {
+ bool: {
+ must: [
+ { term: { 'provider.type': 'basic' } },
+ { term: { 'provider.name': 'basic' } },
+ ],
+ },
+ },
],
minimum_should_match: 1,
},
},
- ],
+ },
},
- },
+ // The sessions that belong to a particular provider that are either expired based on the idle timeout
+ // or don't have it configured at all.
+ {
+ bool: {
+ must: [
+ { term: { 'provider.type': 'basic' } },
+ { term: { 'provider.name': 'basic' } },
+ ],
+ should: [
+ { range: { idleTimeoutExpiration: { lte: now - 3 * idleTimeout } } },
+ { bool: { must_not: { exists: { field: 'idleTimeoutExpiration' } } } },
+ ],
+ minimum_should_match: 1,
+ },
+ },
+ ],
},
},
- { ignore: [409, 404] }
+ });
+ expect(mockElasticsearchClient.bulk).toHaveBeenCalledTimes(1);
+ expect(mockElasticsearchClient.bulk).toHaveBeenCalledWith(
+ {
+ index: indexName,
+ operations: [{ delete: { _id: sessionValue._id } }],
+ refresh: false,
+ },
+ {
+ ignore: [409, 404],
+ }
);
+ expect(mockElasticsearchClient.closePointInTime).toHaveBeenCalledTimes(1);
});
it('when both `lifespan` and `idleTimeout` are configured', async () => {
@@ -445,73 +542,89 @@ describe('Session index', () => {
{ isTLSEnabled: false }
),
elasticsearchClient: mockElasticsearchClient,
+ auditLogger: auditServiceMock.create().withoutRequest,
});
await sessionIndex.cleanUp();
- expect(mockElasticsearchClient.deleteByQuery).toHaveBeenCalledTimes(1);
- expect(mockElasticsearchClient.deleteByQuery).toHaveBeenCalledWith(
- {
- index: indexName,
- refresh: true,
- body: {
- query: {
- bool: {
- should: [
- // All expired sessions based on the lifespan, no matter which provider they belong to.
- { range: { lifespanExpiration: { lte: now } } },
- // All sessions that belong to the providers that aren't configured.
- {
- bool: {
- must_not: {
- bool: {
- should: [
- {
- bool: {
- must: [
- { term: { 'provider.type': 'basic' } },
- { term: { 'provider.name': 'basic' } },
- ],
- },
- },
- ],
- minimum_should_match: 1,
- },
- },
- },
- },
- // The sessions that belong to a particular provider but don't have a configured lifespan.
- {
- bool: {
- must: [
- { term: { 'provider.type': 'basic' } },
- { term: { 'provider.name': 'basic' } },
- ],
- must_not: { exists: { field: 'lifespanExpiration' } },
- },
- },
- // The sessions that belong to a particular provider that are either expired based on the idle timeout
- // or don't have it configured at all.
- {
+ expect(mockElasticsearchClient.openPointInTime).toHaveBeenCalledTimes(1);
+ expect(mockElasticsearchClient.search).toHaveBeenCalledTimes(1);
+ expect(mockElasticsearchClient.search).toHaveBeenCalledWith({
+ _source_includes: 'usernameHash,provider',
+ sort: '_shard_doc',
+ track_total_hits: false,
+ search_after: undefined,
+ size: 10_000,
+ pit: {
+ id: 'PIT_ID',
+ keep_alive: '5m',
+ },
+ query: {
+ bool: {
+ should: [
+ // All expired sessions based on the lifespan, no matter which provider they belong to.
+ { range: { lifespanExpiration: { lte: now } } },
+ // All sessions that belong to the providers that aren't configured.
+ {
+ bool: {
+ must_not: {
bool: {
- must: [
- { term: { 'provider.type': 'basic' } },
- { term: { 'provider.name': 'basic' } },
- ],
should: [
- { range: { idleTimeoutExpiration: { lte: now - 3 * idleTimeout } } },
- { bool: { must_not: { exists: { field: 'idleTimeoutExpiration' } } } },
+ {
+ bool: {
+ must: [
+ { term: { 'provider.type': 'basic' } },
+ { term: { 'provider.name': 'basic' } },
+ ],
+ },
+ },
],
minimum_should_match: 1,
},
},
- ],
+ },
},
- },
+ // The sessions that belong to a particular provider but don't have a configured lifespan.
+ {
+ bool: {
+ must: [
+ { term: { 'provider.type': 'basic' } },
+ { term: { 'provider.name': 'basic' } },
+ ],
+ must_not: { exists: { field: 'lifespanExpiration' } },
+ },
+ },
+ // The sessions that belong to a particular provider that are either expired based on the idle timeout
+ // or don't have it configured at all.
+ {
+ bool: {
+ must: [
+ { term: { 'provider.type': 'basic' } },
+ { term: { 'provider.name': 'basic' } },
+ ],
+ should: [
+ { range: { idleTimeoutExpiration: { lte: now - 3 * idleTimeout } } },
+ { bool: { must_not: { exists: { field: 'idleTimeoutExpiration' } } } },
+ ],
+ minimum_should_match: 1,
+ },
+ },
+ ],
},
},
- { ignore: [409, 404] }
+ });
+ expect(mockElasticsearchClient.bulk).toHaveBeenCalledTimes(1);
+ expect(mockElasticsearchClient.bulk).toHaveBeenCalledWith(
+ {
+ index: indexName,
+ operations: [{ delete: { _id: sessionValue._id } }],
+ refresh: false,
+ },
+ {
+ ignore: [409, 404],
+ }
);
+ expect(mockElasticsearchClient.closePointInTime).toHaveBeenCalledTimes(1);
});
it('when both `lifespan` and `idleTimeout` are configured and multiple providers are enabled', async () => {
@@ -540,105 +653,167 @@ describe('Session index', () => {
{ isTLSEnabled: false }
),
elasticsearchClient: mockElasticsearchClient,
+ auditLogger: auditServiceMock.create().withoutRequest,
});
await sessionIndex.cleanUp();
- expect(mockElasticsearchClient.deleteByQuery).toHaveBeenCalledTimes(1);
- expect(mockElasticsearchClient.deleteByQuery).toHaveBeenCalledWith(
- {
- index: indexName,
- refresh: true,
- body: {
- query: {
- bool: {
- should: [
- // All expired sessions based on the lifespan, no matter which provider they belong to.
- { range: { lifespanExpiration: { lte: now } } },
- // All sessions that belong to the providers that aren't configured.
- {
- bool: {
- must_not: {
- bool: {
- should: [
- {
- bool: {
- must: [
- { term: { 'provider.type': 'basic' } },
- { term: { 'provider.name': 'basic1' } },
- ],
- },
- },
- {
- bool: {
- must: [
- { term: { 'provider.type': 'saml' } },
- { term: { 'provider.name': 'saml1' } },
- ],
- },
- },
- ],
- minimum_should_match: 1,
- },
- },
- },
- },
- // The sessions that belong to a Basic provider but don't have a configured lifespan.
- {
- bool: {
- must: [
- { term: { 'provider.type': 'basic' } },
- { term: { 'provider.name': 'basic1' } },
- ],
- must_not: { exists: { field: 'lifespanExpiration' } },
- },
- },
- // The sessions that belong to a Basic provider that are either expired based on the idle timeout
- // or don't have it configured at all.
- {
- bool: {
- must: [
- { term: { 'provider.type': 'basic' } },
- { term: { 'provider.name': 'basic1' } },
- ],
- should: [
- { range: { idleTimeoutExpiration: { lte: now - 3 * globalIdleTimeout } } },
- { bool: { must_not: { exists: { field: 'idleTimeoutExpiration' } } } },
- ],
- minimum_should_match: 1,
- },
- },
- // The sessions that belong to a SAML provider but don't have a configured lifespan.
- {
- bool: {
- must: [
- { term: { 'provider.type': 'saml' } },
- { term: { 'provider.name': 'saml1' } },
- ],
- must_not: { exists: { field: 'lifespanExpiration' } },
- },
- },
- // The sessions that belong to a SAML provider that are either expired based on the idle timeout
- // or don't have it configured at all.
- {
+ expect(mockElasticsearchClient.openPointInTime).toHaveBeenCalledTimes(1);
+ expect(mockElasticsearchClient.search).toHaveBeenCalledTimes(1);
+ expect(mockElasticsearchClient.search).toHaveBeenCalledWith({
+ _source_includes: 'usernameHash,provider',
+ sort: '_shard_doc',
+ track_total_hits: false,
+ search_after: undefined,
+ size: 10_000,
+ pit: {
+ id: 'PIT_ID',
+ keep_alive: '5m',
+ },
+ query: {
+ bool: {
+ should: [
+ // All expired sessions based on the lifespan, no matter which provider they belong to.
+ { range: { lifespanExpiration: { lte: now } } },
+ // All sessions that belong to the providers that aren't configured.
+ {
+ bool: {
+ must_not: {
bool: {
- must: [
- { term: { 'provider.type': 'saml' } },
- { term: { 'provider.name': 'saml1' } },
- ],
should: [
- { range: { idleTimeoutExpiration: { lte: now - 3 * samlIdleTimeout } } },
- { bool: { must_not: { exists: { field: 'idleTimeoutExpiration' } } } },
+ {
+ bool: {
+ must: [
+ { term: { 'provider.type': 'basic' } },
+ { term: { 'provider.name': 'basic1' } },
+ ],
+ },
+ },
+ {
+ bool: {
+ must: [
+ { term: { 'provider.type': 'saml' } },
+ { term: { 'provider.name': 'saml1' } },
+ ],
+ },
+ },
],
minimum_should_match: 1,
},
},
- ],
+ },
},
- },
+ // The sessions that belong to a Basic provider but don't have a configured lifespan.
+ {
+ bool: {
+ must: [
+ { term: { 'provider.type': 'basic' } },
+ { term: { 'provider.name': 'basic1' } },
+ ],
+ must_not: { exists: { field: 'lifespanExpiration' } },
+ },
+ },
+ // The sessions that belong to a Basic provider that are either expired based on the idle timeout
+ // or don't have it configured at all.
+ {
+ bool: {
+ must: [
+ { term: { 'provider.type': 'basic' } },
+ { term: { 'provider.name': 'basic1' } },
+ ],
+ should: [
+ { range: { idleTimeoutExpiration: { lte: now - 3 * globalIdleTimeout } } },
+ { bool: { must_not: { exists: { field: 'idleTimeoutExpiration' } } } },
+ ],
+ minimum_should_match: 1,
+ },
+ },
+ // The sessions that belong to a SAML provider but don't have a configured lifespan.
+ {
+ bool: {
+ must: [
+ { term: { 'provider.type': 'saml' } },
+ { term: { 'provider.name': 'saml1' } },
+ ],
+ must_not: { exists: { field: 'lifespanExpiration' } },
+ },
+ },
+ // The sessions that belong to a SAML provider that are either expired based on the idle timeout
+ // or don't have it configured at all.
+ {
+ bool: {
+ must: [
+ { term: { 'provider.type': 'saml' } },
+ { term: { 'provider.name': 'saml1' } },
+ ],
+ should: [
+ { range: { idleTimeoutExpiration: { lte: now - 3 * samlIdleTimeout } } },
+ { bool: { must_not: { exists: { field: 'idleTimeoutExpiration' } } } },
+ ],
+ minimum_should_match: 1,
+ },
+ },
+ ],
},
},
- { ignore: [409, 404] }
+ });
+ expect(mockElasticsearchClient.bulk).toHaveBeenCalledTimes(1);
+ expect(mockElasticsearchClient.bulk).toHaveBeenCalledWith(
+ {
+ index: indexName,
+ operations: [{ delete: { _id: sessionValue._id } }],
+ refresh: false,
+ },
+ {
+ ignore: [409, 404],
+ }
+ );
+ expect(mockElasticsearchClient.closePointInTime).toHaveBeenCalledTimes(1);
+ });
+
+ it('should clean up sessions in batches of 10,000', async () => {
+ for (const count of [10_000, 1]) {
+ mockElasticsearchClient.search.mockResolvedValueOnce(
+ securityMock.createApiResponse({
+ body: {
+ hits: { hits: new Array(count).fill(sessionValue, 0) },
+ } as SearchResponse,
+ })
+ );
+ }
+
+ await sessionIndex.cleanUp();
+
+ expect(mockElasticsearchClient.openPointInTime).toHaveBeenCalledTimes(1);
+ expect(mockElasticsearchClient.search).toHaveBeenCalledTimes(2);
+ expect(mockElasticsearchClient.bulk).toHaveBeenCalledTimes(2);
+ expect(mockElasticsearchClient.closePointInTime).toHaveBeenCalledTimes(1);
+ });
+
+ it('should limit number of batches to 10', async () => {
+ mockElasticsearchClient.search.mockResolvedValue(
+ securityMock.createApiResponse({
+ body: {
+ hits: { hits: new Array(10_000).fill(sessionValue, 0) },
+ } as SearchResponse,
+ })
+ );
+
+ await sessionIndex.cleanUp();
+
+ expect(mockElasticsearchClient.openPointInTime).toHaveBeenCalledTimes(1);
+ expect(mockElasticsearchClient.search).toHaveBeenCalledTimes(10);
+ expect(mockElasticsearchClient.bulk).toHaveBeenCalledTimes(10);
+ expect(mockElasticsearchClient.closePointInTime).toHaveBeenCalledTimes(1);
+ });
+
+ it('should log audit event', async () => {
+ await sessionIndex.cleanUp();
+
+ expect(auditLogger.log).toHaveBeenCalledWith(
+ expect.objectContaining({
+ event: { action: 'session_cleanup', category: ['authentication'], outcome: 'unknown' },
+ })
);
});
});
diff --git a/x-pack/plugins/security/server/session_management/session_index.ts b/x-pack/plugins/security/server/session_management/session_index.ts
index 801597dad6bafd..e064a735bc0316 100644
--- a/x-pack/plugins/security/server/session_management/session_index.ts
+++ b/x-pack/plugins/security/server/session_management/session_index.ts
@@ -5,9 +5,16 @@
* 2.0.
*/
+import type {
+ BulkOperationContainer,
+ SortResults,
+} from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
+
import type { ElasticsearchClient, Logger } from 'src/core/server';
import type { AuthenticationProvider } from '../../common/model';
+import type { AuditLogger } from '../audit';
+import { sessionCleanupEvent } from '../audit';
import type { ConfigType } from '../config';
export interface SessionIndexOptions {
@@ -15,6 +22,7 @@ export interface SessionIndexOptions {
readonly kibanaIndexName: string;
readonly config: Pick;
readonly logger: Logger;
+ readonly auditLogger: AuditLogger;
}
/**
@@ -34,6 +42,22 @@ export type InvalidateSessionsFilter =
*/
const SESSION_INDEX_TEMPLATE_VERSION = 1;
+/**
+ * Number of sessions to remove per batch during cleanup.
+ */
+const SESSION_INDEX_CLEANUP_BATCH_SIZE = 10_000;
+
+/**
+ * Maximum number of batches per cleanup.
+ * If the batch size is 10,000 and this limit is 10, then Kibana will remove up to 100k sessions per cleanup.
+ */
+const SESSION_INDEX_CLEANUP_BATCH_LIMIT = 10;
+
+/**
+ * How long the session cleanup search point-in-time should be kept alive.
+ */
+const SESSION_INDEX_CLEANUP_KEEP_ALIVE = '5m';
+
/**
* Returns index template that is used for the current version of the session index.
*/
@@ -425,6 +449,56 @@ export class SessionIndex {
async cleanUp() {
this.options.logger.debug(`Running cleanup routine.`);
+ try {
+ for await (const sessionValues of this.getSessionValuesInBatches()) {
+ const operations: Array>> = [];
+ sessionValues.forEach(({ _id, _source }) => {
+ const { usernameHash, provider } = _source!;
+ this.options.auditLogger.log(
+ sessionCleanupEvent({ sessionId: _id, usernameHash, provider })
+ );
+ operations.push({ delete: { _id } });
+ });
+ if (operations.length > 0) {
+ const { body: bulkResponse } = await this.options.elasticsearchClient.bulk(
+ {
+ index: this.indexName,
+ operations,
+ refresh: false,
+ },
+ { ignore: [409, 404] }
+ );
+ if (bulkResponse.errors) {
+ const errorCount = bulkResponse.items.reduce(
+ (count, item) => (item.delete!.error ? count + 1 : count),
+ 0
+ );
+ if (errorCount < bulkResponse.items.length) {
+ this.options.logger.warn(
+ `Failed to clean up ${errorCount} of ${bulkResponse.items.length} invalid or expired sessions. The remaining sessions were cleaned up successfully.`
+ );
+ } else {
+ this.options.logger.error(
+ `Failed to clean up ${bulkResponse.items.length} invalid or expired sessions.`
+ );
+ }
+ } else {
+ this.options.logger.debug(
+ `Cleaned up ${bulkResponse.items.length} invalid or expired sessions.`
+ );
+ }
+ }
+ }
+ } catch (err) {
+ this.options.logger.error(`Failed to clean up sessions: ${err.message}`);
+ throw err;
+ }
+ }
+
+ /**
+ * Fetches session values from session index in batches of 10,000.
+ */
+ private async *getSessionValuesInBatches() {
const now = Date.now();
const providersSessionConfig = this.options.config.authc.sortedProviders.map((provider) => {
return {
@@ -484,24 +558,37 @@ export class SessionIndex {
});
}
- try {
- const { body: response } = await this.options.elasticsearchClient.deleteByQuery(
- {
- index: this.indexName,
- refresh: true,
- body: { query: { bool: { should: deleteQueries } } },
- },
- { ignore: [409, 404] }
- );
+ const { body: openPitResponse } = await this.options.elasticsearchClient.openPointInTime({
+ index: this.indexName,
+ keep_alive: SESSION_INDEX_CLEANUP_KEEP_ALIVE,
+ });
- if (response.deleted! > 0) {
- this.options.logger.debug(
- `Cleaned up ${response.deleted} invalid or expired session values.`
- );
+ try {
+ let searchAfter: SortResults | undefined;
+ for (let i = 0; i < SESSION_INDEX_CLEANUP_BATCH_LIMIT; i++) {
+ const { body: searchResponse } =
+ await this.options.elasticsearchClient.search({
+ pit: { id: openPitResponse.id, keep_alive: SESSION_INDEX_CLEANUP_KEEP_ALIVE },
+ _source_includes: 'usernameHash,provider',
+ query: { bool: { should: deleteQueries } },
+ search_after: searchAfter,
+ size: SESSION_INDEX_CLEANUP_BATCH_SIZE,
+ sort: '_shard_doc',
+ track_total_hits: false, // for performance
+ });
+ const { hits } = searchResponse.hits;
+ if (hits.length > 0) {
+ yield hits;
+ searchAfter = hits[hits.length - 1].sort;
+ }
+ if (hits.length < SESSION_INDEX_CLEANUP_BATCH_SIZE) {
+ break;
+ }
}
- } catch (err) {
- this.options.logger.error(`Failed to clean up sessions: ${err.message}`);
- throw err;
+ } finally {
+ await this.options.elasticsearchClient.closePointInTime({
+ id: openPitResponse.id,
+ });
}
}
}
diff --git a/x-pack/plugins/security/server/session_management/session_management_service.test.ts b/x-pack/plugins/security/server/session_management/session_management_service.test.ts
index 7e99181981e851..100d0b30082c6e 100644
--- a/x-pack/plugins/security/server/session_management/session_management_service.test.ts
+++ b/x-pack/plugins/security/server/session_management/session_management_service.test.ts
@@ -15,6 +15,8 @@ import type {
TaskRunCreatorFunction,
} from '../../../task_manager/server';
import { taskManagerMock } from '../../../task_manager/server/mocks';
+import type { AuditLogger } from '../audit';
+import { auditServiceMock } from '../audit/index.mock';
import { ConfigSchema, createConfig } from '../config';
import type { OnlineStatusRetryScheduler } from '../elasticsearch';
import { Session } from './session';
@@ -24,10 +26,23 @@ import {
SessionManagementService,
} from './session_management_service';
+const mockSessionIndexInitialize = jest.spyOn(SessionIndex.prototype, 'initialize');
+mockSessionIndexInitialize.mockResolvedValue();
+
+const mockSessionIndexCleanUp = jest.spyOn(SessionIndex.prototype, 'cleanUp');
+mockSessionIndexCleanUp.mockResolvedValue();
+
describe('SessionManagementService', () => {
let service: SessionManagementService;
+ let auditLogger: AuditLogger;
beforeEach(() => {
service = new SessionManagementService(loggingSystemMock.createLogger());
+ auditLogger = auditServiceMock.create().withoutRequest;
+ });
+
+ afterEach(() => {
+ mockSessionIndexInitialize.mockReset();
+ mockSessionIndexCleanUp.mockReset();
});
describe('setup()', () => {
@@ -56,12 +71,9 @@ describe('SessionManagementService', () => {
});
describe('start()', () => {
- let mockSessionIndexInitialize: jest.SpyInstance;
let mockTaskManager: jest.Mocked;
let sessionCleanupTaskRunCreator: TaskRunCreatorFunction;
beforeEach(() => {
- mockSessionIndexInitialize = jest.spyOn(SessionIndex.prototype, 'initialize');
-
mockTaskManager = taskManagerMock.createStart();
mockTaskManager.ensureScheduled.mockResolvedValue(undefined as any);
@@ -84,14 +96,11 @@ describe('SessionManagementService', () => {
sessionCleanupTaskRunCreator = createTaskRunner;
});
- afterEach(() => {
- mockSessionIndexInitialize.mockReset();
- });
-
it('exposes proper contract', () => {
const mockStatusSubject = new Subject();
expect(
service.start({
+ auditLogger,
elasticsearchClient: elasticsearchServiceMock.createElasticsearchClient(),
kibanaIndexName: '.kibana',
online$: mockStatusSubject.asObservable(),
@@ -100,10 +109,10 @@ describe('SessionManagementService', () => {
).toEqual({ session: expect.any(Session) });
});
- it('registers proper session index cleanup task runner', () => {
- const mockSessionIndexCleanUp = jest.spyOn(SessionIndex.prototype, 'cleanUp');
+ it('registers proper session index cleanup task runner', async () => {
const mockStatusSubject = new Subject();
service.start({
+ auditLogger,
elasticsearchClient: elasticsearchServiceMock.createElasticsearchClient(),
kibanaIndexName: '.kibana',
online$: mockStatusSubject.asObservable(),
@@ -113,16 +122,17 @@ describe('SessionManagementService', () => {
expect(mockSessionIndexCleanUp).not.toHaveBeenCalled();
const runner = sessionCleanupTaskRunCreator({} as any);
- runner.run();
+ await runner.run();
expect(mockSessionIndexCleanUp).toHaveBeenCalledTimes(1);
- runner.run();
+ await runner.run();
expect(mockSessionIndexCleanUp).toHaveBeenCalledTimes(2);
});
it('initializes session index and schedules session index cleanup task when Elasticsearch goes online', async () => {
const mockStatusSubject = new Subject();
service.start({
+ auditLogger,
elasticsearchClient: elasticsearchServiceMock.createElasticsearchClient(),
kibanaIndexName: '.kibana',
online$: mockStatusSubject.asObservable(),
@@ -160,6 +170,7 @@ describe('SessionManagementService', () => {
it('removes old cleanup task if cleanup interval changes', async () => {
const mockStatusSubject = new Subject();
service.start({
+ auditLogger,
elasticsearchClient: elasticsearchServiceMock.createElasticsearchClient(),
kibanaIndexName: '.kibana',
online$: mockStatusSubject.asObservable(),
@@ -195,6 +206,7 @@ describe('SessionManagementService', () => {
it('does not remove old cleanup task if cleanup interval does not change', async () => {
const mockStatusSubject = new Subject();
service.start({
+ auditLogger,
elasticsearchClient: elasticsearchServiceMock.createElasticsearchClient(),
kibanaIndexName: '.kibana',
online$: mockStatusSubject.asObservable(),
@@ -221,6 +233,7 @@ describe('SessionManagementService', () => {
it('schedules retry if index initialization fails', async () => {
const mockStatusSubject = new Subject();
service.start({
+ auditLogger,
elasticsearchClient: elasticsearchServiceMock.createElasticsearchClient(),
kibanaIndexName: '.kibana',
online$: mockStatusSubject.asObservable(),
@@ -257,6 +270,7 @@ describe('SessionManagementService', () => {
it('schedules retry if cleanup task registration fails', async () => {
const mockStatusSubject = new Subject();
service.start({
+ auditLogger,
elasticsearchClient: elasticsearchServiceMock.createElasticsearchClient(),
kibanaIndexName: '.kibana',
online$: mockStatusSubject.asObservable(),
@@ -291,11 +305,8 @@ describe('SessionManagementService', () => {
});
describe('stop()', () => {
- let mockSessionIndexInitialize: jest.SpyInstance;
let mockTaskManager: jest.Mocked;
beforeEach(() => {
- mockSessionIndexInitialize = jest.spyOn(SessionIndex.prototype, 'initialize');
-
mockTaskManager = taskManagerMock.createStart();
mockTaskManager.ensureScheduled.mockResolvedValue(undefined as any);
@@ -309,13 +320,10 @@ describe('SessionManagementService', () => {
});
});
- afterEach(() => {
- mockSessionIndexInitialize.mockReset();
- });
-
it('properly unsubscribes from status updates', () => {
const mockStatusSubject = new Subject();
service.start({
+ auditLogger,
elasticsearchClient: elasticsearchServiceMock.createElasticsearchClient(),
kibanaIndexName: '.kibana',
online$: mockStatusSubject.asObservable(),
diff --git a/x-pack/plugins/security/server/session_management/session_management_service.ts b/x-pack/plugins/security/server/session_management/session_management_service.ts
index fcd8e8c53cbe50..03a5d6130c3c17 100644
--- a/x-pack/plugins/security/server/session_management/session_management_service.ts
+++ b/x-pack/plugins/security/server/session_management/session_management_service.ts
@@ -14,6 +14,7 @@ import type {
TaskManagerSetupContract,
TaskManagerStartContract,
} from '../../../task_manager/server';
+import type { AuditLogger } from '../audit';
import type { ConfigType } from '../config';
import type { OnlineStatusRetryScheduler } from '../elasticsearch';
import { Session } from './session';
@@ -31,6 +32,7 @@ export interface SessionManagementServiceStartParams {
readonly kibanaIndexName: string;
readonly online$: Observable;
readonly taskManager: TaskManagerStartContract;
+ readonly auditLogger: AuditLogger;
}
export interface SessionManagementServiceStart {
@@ -78,12 +80,14 @@ export class SessionManagementService {
kibanaIndexName,
online$,
taskManager,
+ auditLogger,
}: SessionManagementServiceStartParams): SessionManagementServiceStart {
this.sessionIndex = new SessionIndex({
config: this.config,
elasticsearchClient,
kibanaIndexName,
logger: this.logger.get('index'),
+ auditLogger,
});
this.statusSubscription = online$.subscribe(async ({ scheduleRetry }) => {
diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts
index a99a3f8ee2fe99..9a9236d573fc4e 100644
--- a/x-pack/plugins/security_solution/common/constants.ts
+++ b/x-pack/plugins/security_solution/common/constants.ts
@@ -128,6 +128,7 @@ export const UEBA_PATH = '/ueba' as const;
export const NETWORK_PATH = '/network' as const;
export const MANAGEMENT_PATH = '/administration' as const;
export const ENDPOINTS_PATH = `${MANAGEMENT_PATH}/endpoints` as const;
+export const POLICIES_PATH = `${MANAGEMENT_PATH}/policy` as const;
export const TRUSTED_APPS_PATH = `${MANAGEMENT_PATH}/trusted_apps` as const;
export const EVENT_FILTERS_PATH = `${MANAGEMENT_PATH}/event_filters` as const;
export const HOST_ISOLATION_EXCEPTIONS_PATH =
@@ -146,6 +147,7 @@ export const APP_NETWORK_PATH = `${APP_PATH}${NETWORK_PATH}` as const;
export const APP_TIMELINES_PATH = `${APP_PATH}${TIMELINES_PATH}` as const;
export const APP_CASES_PATH = `${APP_PATH}${CASES_PATH}` as const;
export const APP_ENDPOINTS_PATH = `${APP_PATH}${ENDPOINTS_PATH}` as const;
+export const APP_POLICIES_PATH = `${APP_PATH}${POLICIES_PATH}` as const;
export const APP_TRUSTED_APPS_PATH = `${APP_PATH}${TRUSTED_APPS_PATH}` as const;
export const APP_EVENT_FILTERS_PATH = `${APP_PATH}${EVENT_FILTERS_PATH}` as const;
export const APP_HOST_ISOLATION_EXCEPTIONS_PATH =
diff --git a/x-pack/plugins/security_solution/common/experimental_features.ts b/x-pack/plugins/security_solution/common/experimental_features.ts
index 4f43e9b61faf96..3f19b888d39011 100644
--- a/x-pack/plugins/security_solution/common/experimental_features.ts
+++ b/x-pack/plugins/security_solution/common/experimental_features.ts
@@ -22,6 +22,7 @@ export const allowedExperimentalValues = Object.freeze({
riskyHostsEnabled: false,
securityRulesCancelEnabled: false,
pendingActionResponsesWithAck: true,
+ policyListEnabled: false,
});
type ExperimentalConfigKeys = Array;
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/expand_dotted.test.ts b/x-pack/plugins/security_solution/common/utils/expand_dotted.test.ts
similarity index 100%
rename from x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/expand_dotted.test.ts
rename to x-pack/plugins/security_solution/common/utils/expand_dotted.test.ts
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/expand_dotted.ts b/x-pack/plugins/security_solution/common/utils/expand_dotted.ts
similarity index 100%
rename from x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/expand_dotted.ts
rename to x-pack/plugins/security_solution/common/utils/expand_dotted.ts
diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts
index c4709d857d5d0e..3d93ad96706e38 100644
--- a/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts
+++ b/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts
@@ -511,7 +511,7 @@ describe('indicator match', () => {
cy.get(ALERT_RISK_SCORE).first().should('have.text', getNewThreatIndicatorRule().riskScore);
});
- it.skip('Investigate alert in timeline', () => {
+ it('Investigate alert in timeline', () => {
const accessibilityText = `Press enter for options, or press space to begin dragging.`;
loadPrepackagedTimelineTemplates();
@@ -540,7 +540,8 @@ describe('indicator match', () => {
getNewThreatIndicatorRule().indicatorMappingField
}${accessibilityText}matched${getNewThreatIndicatorRule().indicatorMappingField}${
getNewThreatIndicatorRule().atomic
- }${accessibilityText}threat.enrichments.matched.typeindicator_match_rule${accessibilityText}`
+ }${accessibilityText}threat.enrichments.matched.typeindicator_match_rule${accessibilityText}provided` +
+ ` byfeed.nameAbuseCH malware${accessibilityText}`
);
});
});
diff --git a/x-pack/plugins/security_solution/cypress/integration/timelines/fields_browser.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timelines/fields_browser.spec.ts
index 0a5db030f1dca6..df194136c6bb2c 100644
--- a/x-pack/plugins/security_solution/cypress/integration/timelines/fields_browser.spec.ts
+++ b/x-pack/plugins/security_solution/cypress/integration/timelines/fields_browser.spec.ts
@@ -104,12 +104,12 @@ describe('Fields Browser', () => {
});
});
- it.skip('displays a count of only the fields in the selected category that match the filter input', () => {
+ it('displays a count of only the fields in the selected category that match the filter input', () => {
const filterInput = 'host.geo.c';
filterFieldsBrowser(filterInput);
- cy.get(FIELDS_BROWSER_SELECTED_CATEGORY_COUNT).should('have.text', '5');
+ cy.get(FIELDS_BROWSER_SELECTED_CATEGORY_COUNT).should('have.text', '4');
});
});
diff --git a/x-pack/plugins/security_solution/public/app/deep_links/index.ts b/x-pack/plugins/security_solution/public/app/deep_links/index.ts
index 545210c788e8ce..d2308bf1b4cbc6 100644
--- a/x-pack/plugins/security_solution/public/app/deep_links/index.ts
+++ b/x-pack/plugins/security_solution/public/app/deep_links/index.ts
@@ -28,6 +28,7 @@ import {
HOST_ISOLATION_EXCEPTIONS,
EVENT_FILTERS,
TRUSTED_APPLICATIONS,
+ POLICIES,
ENDPOINTS,
} from '../translations';
import {
@@ -40,6 +41,7 @@ import {
TIMELINES_PATH,
CASES_PATH,
ENDPOINTS_PATH,
+ POLICIES_PATH,
TRUSTED_APPS_PATH,
EVENT_FILTERS_PATH,
UEBA_PATH,
@@ -327,6 +329,12 @@ export const securitySolutionsDeepLinks: SecuritySolutionDeepLink[] = [
order: 9006,
path: ENDPOINTS_PATH,
},
+ {
+ id: SecurityPageName.policies,
+ title: POLICIES,
+ path: POLICIES_PATH,
+ experimentalKey: 'policyListEnabled',
+ },
{
id: SecurityPageName.trustedApps,
title: TRUSTED_APPLICATIONS,
diff --git a/x-pack/plugins/security_solution/public/app/home/home_navigations.ts b/x-pack/plugins/security_solution/public/app/home/home_navigations.ts
index de76a570312a5c..15ec9a98b37fd6 100644
--- a/x-pack/plugins/security_solution/public/app/home/home_navigations.ts
+++ b/x-pack/plugins/security_solution/public/app/home/home_navigations.ts
@@ -22,6 +22,7 @@ import {
APP_CASES_PATH,
APP_MANAGEMENT_PATH,
APP_ENDPOINTS_PATH,
+ APP_POLICIES_PATH,
APP_TRUSTED_APPS_PATH,
APP_EVENT_FILTERS_PATH,
APP_UEBA_PATH,
@@ -107,6 +108,13 @@ export const navTabs: SecurityNav = {
disabled: false,
urlKey: 'administration',
},
+ [SecurityPageName.policies]: {
+ id: SecurityPageName.policies,
+ name: i18n.POLICIES,
+ href: APP_POLICIES_PATH,
+ disabled: false,
+ urlKey: 'administration',
+ },
[SecurityPageName.trustedApps]: {
id: SecurityPageName.trustedApps,
name: i18n.TRUSTED_APPLICATIONS,
diff --git a/x-pack/plugins/security_solution/public/app/translations.ts b/x-pack/plugins/security_solution/public/app/translations.ts
index 7287739566e68c..be341f98df8d84 100644
--- a/x-pack/plugins/security_solution/public/app/translations.ts
+++ b/x-pack/plugins/security_solution/public/app/translations.ts
@@ -49,6 +49,12 @@ export const ADMINISTRATION = i18n.translate('xpack.securitySolution.navigation.
export const ENDPOINTS = i18n.translate('xpack.securitySolution.search.administration.endpoints', {
defaultMessage: 'Endpoints',
});
+export const POLICIES = i18n.translate(
+ 'xpack.securitySolution.navigation.administration.policies',
+ {
+ defaultMessage: 'Policies',
+ }
+);
export const TRUSTED_APPLICATIONS = i18n.translate(
'xpack.securitySolution.search.administration.trustedApps',
{
diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/__snapshots__/alert_summary_view.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/event_details/__snapshots__/alert_summary_view.test.tsx.snap
index 8772def6861228..2c7c820cdd7a35 100644
--- a/x-pack/plugins/security_solution/public/common/components/event_details/__snapshots__/alert_summary_view.test.tsx.snap
+++ b/x-pack/plugins/security_solution/public/common/components/event_details/__snapshots__/alert_summary_view.test.tsx.snap
@@ -25,8 +25,6 @@ exports[`AlertSummaryView Behavior event code renders additional summary rows 1`
}
.c2 {
- min-width: 138px;
- padding: 0 8px;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
@@ -116,28 +114,30 @@ exports[`AlertSummaryView Behavior event code renders additional summary rows 1`
class="euiTableRow"
>
+
- Status
+ host.name
+
-
-
-
- You are in a dialog, containing options for field kibana.alert.workflow_status. Press tab to navigate options. Press escape to exit.
-
-
- Filter button
-
-
- Filter out button
-
-
- Overflow button
+ windows-native
-
-
-
-
-
-
-
- Timestamp
-
-
-
-
-
-
-
-
- Nov 25, 2020 @ 15:42:39.417
-
-
-
@@ -229,7 +158,7 @@ exports[`AlertSummaryView Behavior event code renders additional summary rows 1`
- You are in a dialog, containing options for field @timestamp. Press tab to navigate options. Press escape to exit.
+ You are in a dialog, containing options for field host.name. Press tab to navigate options. Press escape to exit.
Overflow button
@@ -258,28 +187,30 @@ exports[`AlertSummaryView Behavior event code renders additional summary rows 1`
class="euiTableRow"
>
+
- Rule
+ user.name
+
@@ -300,7 +231,7 @@ exports[`AlertSummaryView Behavior event code renders additional summary rows 1`
- You are in a dialog, containing options for field kibana.alert.rule.name. Press tab to navigate options. Press escape to exit.
+ You are in a dialog, containing options for field user.name. Press tab to navigate options. Press escape to exit.
Overflow button
@@ -329,37 +260,45 @@ exports[`AlertSummaryView Behavior event code renders additional summary rows 1`
class="euiTableRow"
>
+
- Severity
+ source.ip
+
- You are in a dialog, containing options for field kibana.alert.severity. Press tab to navigate options. Press escape to exit.
+ You are in a dialog, containing options for field source.ip. Press tab to navigate options. Press escape to exit.
Overflow button
@@ -396,776 +335,130 @@ exports[`AlertSummaryView Behavior event code renders additional summary rows 1`
-
+
+
+
+
+`;
+
+exports[`AlertSummaryView Memory event code renders additional summary rows 1`] = `
+.c0 .euiTableHeaderCell,
+.c0 .euiTableRowCell {
+ border: none;
+}
+
+.c0 .euiTableHeaderCell .euiTableCellContent {
+ padding: 0;
+}
+
+.c0 .flyoutOverviewDescription .hoverActions-active .timelines__hoverActionButton,
+.c0 .flyoutOverviewDescription .hoverActions-active .securitySolution__hoverActionButton {
+ opacity: 1;
+}
+
+.c0 .flyoutOverviewDescription:hover .timelines__hoverActionButton,
+.c0 .flyoutOverviewDescription:hover .securitySolution__hoverActionButton {
+ opacity: 1;
+}
+
+.c1 {
+ line-height: 1.7rem;
+}
+
+.c2 {
+ display: -webkit-box;
+ display: -webkit-flex;
+ display: -ms-flexbox;
+ display: flex;
+}
+
+.c2:focus-within .timelines__hoverActionButton,
+.c2:focus-within .securitySolution__hoverActionButton {
+ opacity: 1;
+}
+
+.c2:hover .timelines__hoverActionButton,
+.c2:hover .securitySolution__hoverActionButton {
+ opacity: 1;
+}
+
+.c2 .timelines__hoverActionButton,
+.c2 .securitySolution__hoverActionButton {
+ opacity: 0;
+}
+
+.c2 .timelines__hoverActionButton:focus,
+.c2 .securitySolution__hoverActionButton:focus {
+ opacity: 1;
+}
+
+
+
+
+
+
+
+
-
-
- Risk Score
-
-
+
+
-
-
-
-
-
- You are in a dialog, containing options for field kibana.alert.risk_score. Press tab to navigate options. Press escape to exit.
-
-
- Filter button
-
-
- Filter out button
-
-
- Overflow button
-
-
-
-
-
-
-
-
-
-
- host.name
-
-
-
-
-
-
-
-
-
- You are in a dialog, containing options for field host.name. Press tab to navigate options. Press escape to exit.
-
-
- Filter button
-
-
- Filter out button
-
-
- Overflow button
-
-
-
-
-
-
-
-
-
-
- user.name
-
-
-
-
-
-
-
-
-
- You are in a dialog, containing options for field user.name. Press tab to navigate options. Press escape to exit.
-
-
- Filter button
-
-
- Filter out button
-
-
- Overflow button
-
-
-
-
-
-
-
-
-
-
- source.ip
-
-
-
-
-
-
-
-
-
- 185.156.74.3
-
-
-
-
-
-
-
- You are in a dialog, containing options for field source.ip. Press tab to navigate options. Press escape to exit.
-
-
- Filter button
-
-
- Filter out button
-
-
- Overflow button
-
-
-
-
-
-
-
-
-
-
-`;
-
-exports[`AlertSummaryView Memory event code renders additional summary rows 1`] = `
-.c0 .euiTableHeaderCell,
-.c0 .euiTableRowCell {
- border: none;
-}
-
-.c0 .euiTableHeaderCell .euiTableCellContent {
- padding: 0;
-}
-
-.c0 .flyoutOverviewDescription .hoverActions-active .timelines__hoverActionButton,
-.c0 .flyoutOverviewDescription .hoverActions-active .securitySolution__hoverActionButton {
- opacity: 1;
-}
-
-.c0 .flyoutOverviewDescription:hover .timelines__hoverActionButton,
-.c0 .flyoutOverviewDescription:hover .securitySolution__hoverActionButton {
- opacity: 1;
-}
-
-.c1 {
- line-height: 1.7rem;
-}
-
-.c2 {
- min-width: 138px;
- padding: 0 8px;
- display: -webkit-box;
- display: -webkit-flex;
- display: -ms-flexbox;
- display: flex;
-}
-
-.c2:focus-within .timelines__hoverActionButton,
-.c2:focus-within .securitySolution__hoverActionButton {
- opacity: 1;
-}
-
-.c2:hover .timelines__hoverActionButton,
-.c2:hover .securitySolution__hoverActionButton {
- opacity: 1;
-}
-
-.c2 .timelines__hoverActionButton,
-.c2 .securitySolution__hoverActionButton {
- opacity: 0;
-}
-
-.c2 .timelines__hoverActionButton:focus,
-.c2 .securitySolution__hoverActionButton:focus {
- opacity: 1;
-}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Status
-
-
-
-
-
-
-
-
-
- You are in a dialog, containing options for field kibana.alert.workflow_status. Press tab to navigate options. Press escape to exit.
-
-
- Filter button
-
-
- Filter out button
-
-
- Overflow button
-
-
-
-
-
-
-
-
-
-
- Timestamp
-
-
-
-
-
-
-
-
- Nov 25, 2020 @ 15:42:39.417
-
-
-
-
-
-
- You are in a dialog, containing options for field @timestamp. Press tab to navigate options. Press escape to exit.
-
-
- Filter button
-
-
- Filter out button
-
-
- Overflow button
-
-
-
-
-
-
-
-
-
-
- Rule
-
-
-
-
-
-
-
-
-
- You are in a dialog, containing options for field kibana.alert.rule.name. Press tab to navigate options. Press escape to exit.
-
-
- Filter button
-
-
- Filter out button
-
-
- Overflow button
-
-
-
-
-
-
-
-
-
-
- Severity
-
-
-
-
-
-
-
-
-
- You are in a dialog, containing options for field kibana.alert.severity. Press tab to navigate options. Press escape to exit.
-
-
- Filter button
-
-
- Filter out button
-
-
- Overflow button
-
-
-
-
-
-
-
-
-
-
- Risk Score
-
-
-
-
-
-
-
-
-
- You are in a dialog, containing options for field kibana.alert.risk_score. Press tab to navigate options. Press escape to exit.
-
-
- Filter button
-
-
- Filter out button
-
-
- Overflow button
-
-
-
-
+
+
+
+
+
@@ -1177,8 +470,9 @@ exports[`AlertSummaryView Memory event code renders additional summary rows 1`]
+
@@ -1234,9 +528,10 @@ exports[`AlertSummaryView Memory event code renders additional summary rows 1`]
class="euiTableRow"
>
+
@@ -1248,8 +543,9 @@ exports[`AlertSummaryView Memory event code renders additional summary rows 1`]
+
@@ -1305,9 +601,10 @@ exports[`AlertSummaryView Memory event code renders additional summary rows 1`]
class="euiTableRow"
>
+
@@ -1319,8 +616,9 @@ exports[`AlertSummaryView Memory event code renders additional summary rows 1`]
+
diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/alert_summary_view.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/alert_summary_view.test.tsx
index c397ac313c48ce..1afba4184c4123 100644
--- a/x-pack/plugins/security_solution/public/common/components/event_details/alert_summary_view.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/event_details/alert_summary_view.test.tsx
@@ -85,7 +85,7 @@ describe('AlertSummaryView', () => {
expect(queryByTestId('summary-view-guide')).not.toBeInTheDocument();
});
});
- test.skip('Memory event code renders additional summary rows', () => {
+ test('Memory event code renders additional summary rows', () => {
const renderProps = {
...props,
data: mockAlertDetailsData.map((item) => {
@@ -107,7 +107,7 @@ describe('AlertSummaryView', () => {
);
expect(container.querySelector('div[data-test-subj="summary-view"]')).toMatchSnapshot();
});
- test.skip('Behavior event code renders additional summary rows', () => {
+ test('Behavior event code renders additional summary rows', () => {
const renderProps = {
...props,
data: mockAlertDetailsData.map((item) => {
diff --git a/x-pack/plugins/security_solution/public/common/components/header_page/index.tsx b/x-pack/plugins/security_solution/public/common/components/header_page/index.tsx
index deba6c739fe34b..2647827c0d1b00 100644
--- a/x-pack/plugins/security_solution/public/common/components/header_page/index.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/header_page/index.tsx
@@ -5,13 +5,7 @@
* 2.0.
*/
-import {
- EuiBadge,
- EuiProgress,
- EuiPageHeader,
- EuiPageHeaderSection,
- EuiSpacer,
-} from '@elastic/eui';
+import { EuiProgress, EuiPageHeader, EuiPageHeaderSection, EuiSpacer } from '@elastic/eui';
import React from 'react';
import styled, { css } from 'styled-components';
@@ -47,11 +41,6 @@ const LinkBack = styled.div.attrs({
`;
LinkBack.displayName = 'LinkBack';
-const Badge = styled(EuiBadge)`
- letter-spacing: 0;
-` as unknown as typeof EuiBadge;
-Badge.displayName = 'Badge';
-
const HeaderSection = styled(EuiPageHeaderSection)`
// Without min-width: 0, as a flex child, it wouldn't shrink properly
// and could overflow its parent.
diff --git a/x-pack/plugins/security_solution/public/common/components/header_page/title.tsx b/x-pack/plugins/security_solution/public/common/components/header_page/title.tsx
index 5e622c0cf63550..c52fd7bc34e82f 100644
--- a/x-pack/plugins/security_solution/public/common/components/header_page/title.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/header_page/title.tsx
@@ -21,6 +21,7 @@ StyledEuiBetaBadge.displayName = 'StyledEuiBetaBadge';
const Badge = styled(EuiBadge)`
letter-spacing: 0;
+ margin-left: 10px;
` as unknown as typeof EuiBadge;
Badge.displayName = 'Badge';
diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/types.ts b/x-pack/plugins/security_solution/public/common/components/navigation/types.ts
index 76b5034ce31650..ea2b692b2b3b76 100644
--- a/x-pack/plugins/security_solution/public/common/components/navigation/types.ts
+++ b/x-pack/plugins/security_solution/public/common/components/navigation/types.ts
@@ -45,6 +45,7 @@ export type SecurityNavKey =
| SecurityPageName.alerts
| SecurityPageName.case
| SecurityPageName.endpoints
+ | SecurityPageName.policies
| SecurityPageName.eventFilters
| SecurityPageName.exceptions
| SecurityPageName.hostIsolationExceptions
diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/use_navigation_items.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/use_navigation_items.tsx
index d012945a23e27c..3c52efcc06567f 100644
--- a/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/use_navigation_items.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/use_navigation_items.tsx
@@ -16,6 +16,7 @@ import { useGetUserCasesPermissions } from '../../../lib/kibana';
import { useNavigation } from '../../../lib/kibana/hooks';
import { NavTab } from '../types';
import { useCanSeeHostIsolationExceptionsMenu } from '../../../../management/pages/host_isolation_exceptions/view/hooks';
+import { useIsExperimentalFeatureEnabled } from '../../../hooks/use_experimental_features';
export const usePrimaryNavigationItems = ({
navTabs,
@@ -65,6 +66,7 @@ export const usePrimaryNavigationItems = ({
function usePrimaryNavigationItemsToDisplay(navTabs: Record
) {
const hasCasesReadPermissions = useGetUserCasesPermissions()?.read;
const canSeeHostIsolationExceptions = useCanSeeHostIsolationExceptionsMenu();
+ const isPolicyListEnabled = useIsExperimentalFeatureEnabled('policyListEnabled');
const uiCapabilities = useKibana().services.application.capabilities;
return useMemo(
() =>
@@ -97,6 +99,7 @@ function usePrimaryNavigationItemsToDisplay(navTabs: Record) {
...securityNavGroup.manage,
items: [
navTabs.endpoints,
+ ...(isPolicyListEnabled ? [navTabs.policies] : []),
navTabs.trusted_apps,
navTabs.event_filters,
...(canSeeHostIsolationExceptions ? [navTabs.host_isolation_exceptions] : []),
@@ -111,6 +114,12 @@ function usePrimaryNavigationItemsToDisplay(navTabs: Record) {
},
]
: [],
- [uiCapabilities.siem.show, navTabs, hasCasesReadPermissions, canSeeHostIsolationExceptions]
+ [
+ uiCapabilities.siem.show,
+ navTabs,
+ hasCasesReadPermissions,
+ canSeeHostIsolationExceptions,
+ isPolicyListEnabled,
+ ]
);
}
diff --git a/x-pack/plugins/security_solution/public/common/lib/cell_actions/default_cell_actions.test.tsx b/x-pack/plugins/security_solution/public/common/lib/cell_actions/default_cell_actions.test.tsx
index a4e6f0a66dfc92..8a7d6c3b27b8d3 100644
--- a/x-pack/plugins/security_solution/public/common/lib/cell_actions/default_cell_actions.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/lib/cell_actions/default_cell_actions.test.tsx
@@ -53,7 +53,6 @@ describe('default cell actions', () => {
});
expect(columnsWithCellActions[0]?.cellActions?.length).toEqual(5);
- expect(columnsWithCellActions[0]?.cellActions![4]).toEqual(EmptyComponent);
});
const columnHeadersToTest = COLUMNS_WITH_LINKS.map((c) => [
diff --git a/x-pack/plugins/security_solution/public/common/lib/cell_actions/default_cell_actions.tsx b/x-pack/plugins/security_solution/public/common/lib/cell_actions/default_cell_actions.tsx
index 3526724a54b414..64473cfddd2992 100644
--- a/x-pack/plugins/security_solution/public/common/lib/cell_actions/default_cell_actions.tsx
+++ b/x-pack/plugins/security_solution/public/common/lib/cell_actions/default_cell_actions.tsx
@@ -9,10 +9,7 @@ import { EuiDataGridColumnCellActionProps } from '@elastic/eui';
import { head, getOr, get, isEmpty } from 'lodash/fp';
import React, { useMemo } from 'react';
-import type {
- BrowserFields,
- TimelineNonEcsData,
-} from '../../../../../timelines/common/search_strategy';
+import type { TimelineNonEcsData } from '../../../../../timelines/common/search_strategy';
import {
ColumnHeaderOptions,
DataProvider,
@@ -20,13 +17,14 @@ import {
} from '../../../../../timelines/common/types';
import { getPageRowIndex } from '../../../../../timelines/public';
import { Ecs } from '../../../../common/ecs';
-import { getMappedNonEcsValue } from '../../../timelines/components/timeline/body/data_driven_columns';
+import { useGetMappedNonEcsValue } from '../../../timelines/components/timeline/body/data_driven_columns';
import { FormattedFieldValue } from '../../../timelines/components/timeline/body/renderers/formatted_field';
import { parseValue } from '../../../timelines/components/timeline/body/renderers/parse_value';
import { IS_OPERATOR } from '../../../timelines/components/timeline/data_providers/data_provider';
import { escapeDataProviderId } from '../../components/drag_and_drop/helpers';
import { useKibana } from '../kibana';
-import { getLink } from './helpers';
+import { getLinkColumnDefinition } from './helpers';
+import { getField, getFieldKey } from '../../../helpers';
/** a noop required by the filter in / out buttons */
const onFilterAdded = () => {};
@@ -45,139 +43,167 @@ const useKibanaServices = () => {
export const EmptyComponent = () => <>>;
-const cellActionLink = [
- ({
- browserFields,
- data,
- ecsData,
- header,
- timelineId,
- pageSize,
- }: {
- browserFields: BrowserFields;
- data: TimelineNonEcsData[][];
- ecsData: Ecs[];
- header?: ColumnHeaderOptions;
- timelineId: string;
- pageSize: number;
- }) => {
- return getLink(header?.id, header?.type, header?.linkField)
- ? ({ rowIndex, columnId, Component, closePopover }: EuiDataGridColumnCellActionProps) => {
- const pageRowIndex = getPageRowIndex(rowIndex, pageSize);
- const ecs = pageRowIndex < ecsData.length ? ecsData[pageRowIndex] : null;
- const link = getLink(columnId, header?.type, header?.linkField);
- const linkField = header?.linkField ? header?.linkField : link?.linkField;
- const linkValues = header && getOr([], linkField ?? '', ecs);
- const eventId = header && get('_id' ?? '', ecs);
- if (pageRowIndex >= data.length) {
- // data grid expects each cell action always return an element, it crashes if returns null
- return <>>;
- }
+const useFormattedFieldProps = ({
+ rowIndex,
+ pageSize,
+ ecsData,
+ columnId,
+ header,
+ data,
+}: {
+ rowIndex: number;
+ data: TimelineNonEcsData[][];
+ ecsData: Ecs[];
+ header?: ColumnHeaderOptions;
+ columnId: string;
+ pageSize: number;
+}) => {
+ const pageRowIndex = getPageRowIndex(rowIndex, pageSize);
+ const ecs = ecsData[pageRowIndex];
+ const link = getLinkColumnDefinition(columnId, header?.type, header?.linkField);
+ const linkField = header?.linkField ? header?.linkField : link?.linkField;
+ const linkValues = header && getOr([], linkField ?? '', ecs);
+ const eventId = (header && get('_id' ?? '', ecs)) || '';
+ const rowData = useMemo(() => {
+ return {
+ data: data[pageRowIndex],
+ fieldName: columnId,
+ };
+ }, [pageRowIndex, columnId, data]);
- const values = getMappedNonEcsValue({
- data: data[pageRowIndex],
- fieldName: columnId,
- });
-
- const value = parseValue(head(values));
- return link && eventId && values && !isEmpty(value) ? (
- 1 ? `${link?.label}: ${value}` : link?.label}
- linkValue={head(linkValues)}
- onClick={closePopover}
- />
- ) : (
- // data grid expects each cell action always return an element, it crashes if returns null
- <>>
- );
- }
- : EmptyComponent;
- },
-];
+ const values = useGetMappedNonEcsValue(rowData);
+ const value = parseValue(head(values));
+ const title = values && values.length > 1 ? `${link?.label}: ${value}` : link?.label;
+ // if linkField is defined but link values is empty, it's possible we are trying to look for a column definition for an old event set
+ if (linkField !== undefined && linkValues.length === 0 && values !== undefined) {
+ const normalizedLinkValue = getField(ecs, linkField);
+ const normalizedLinkField = getFieldKey(ecs, linkField);
+ const normalizedColumnId = getFieldKey(ecs, columnId);
+ const normalizedLink = getLinkColumnDefinition(
+ normalizedColumnId,
+ header?.type,
+ normalizedLinkField
+ );
+ return {
+ pageRowIndex,
+ link: normalizedLink,
+ eventId,
+ fieldFormat: header?.format || '',
+ fieldName: normalizedColumnId,
+ fieldType: header?.type || '',
+ value: parseValue(head(normalizedColumnId)),
+ values,
+ title,
+ linkValue: head(normalizedLinkValue),
+ };
+ } else {
+ return {
+ pageRowIndex,
+ link,
+ eventId,
+ fieldFormat: header?.format || '',
+ fieldName: columnId,
+ fieldType: header?.type || '',
+ value,
+ values,
+ title,
+ linkValue: head(linkValues),
+ };
+ }
+};
export const cellActions: TGridCellAction[] = [
({ data, pageSize }: { data: TimelineNonEcsData[][]; pageSize: number }) =>
- ({ rowIndex, columnId, Component }: EuiDataGridColumnCellActionProps) => {
+ function FilterFor({ rowIndex, columnId, Component }: EuiDataGridColumnCellActionProps) {
const { timelines, filterManager } = useKibanaServices();
const pageRowIndex = getPageRowIndex(rowIndex, pageSize);
+ const rowData = useMemo(() => {
+ return {
+ data: data[pageRowIndex],
+ fieldName: columnId,
+ };
+ }, [pageRowIndex, columnId]);
+
+ const value = useGetMappedNonEcsValue(rowData);
+ const filterForButton = useMemo(
+ () => timelines.getHoverActions().getFilterForValueButton,
+ [timelines]
+ );
+
+ const filterForProps = useMemo(() => {
+ return {
+ Component,
+ field: columnId,
+ filterManager,
+ onFilterAdded,
+ ownFocus: false,
+ showTooltip: false,
+ value,
+ };
+ }, [Component, columnId, filterManager, value]);
if (pageRowIndex >= data.length) {
// data grid expects each cell action always return an element, it crashes if returns null
return <>>;
}
- const value = getMappedNonEcsValue({
- data: data[pageRowIndex],
- fieldName: columnId,
- });
-
- return (
- <>
- {timelines.getHoverActions().getFilterForValueButton({
- Component,
- field: columnId,
- filterManager,
- onFilterAdded,
- ownFocus: false,
- showTooltip: false,
- value,
- })}
- >
- );
+ return <>{filterForButton(filterForProps)}>;
},
({ data, pageSize }: { data: TimelineNonEcsData[][]; pageSize: number }) =>
- ({ rowIndex, columnId, Component }) => {
+ function FilterOut({ rowIndex, columnId, Component }) {
const { timelines, filterManager } = useKibanaServices();
-
const pageRowIndex = getPageRowIndex(rowIndex, pageSize);
+
+ const rowData = useMemo(() => {
+ return {
+ data: data[pageRowIndex],
+ fieldName: columnId,
+ };
+ }, [pageRowIndex, columnId]);
+
+ const value = useGetMappedNonEcsValue(rowData);
+
+ const filterOutButton = useMemo(
+ () => timelines.getHoverActions().getFilterOutValueButton,
+ [timelines]
+ );
+
+ const filterOutProps = useMemo(() => {
+ return {
+ Component,
+ field: columnId,
+ filterManager,
+ onFilterAdded,
+ ownFocus: false,
+ showTooltip: false,
+ value,
+ };
+ }, [Component, columnId, filterManager, value]);
if (pageRowIndex >= data.length) {
// data grid expects each cell action always return an element, it crashes if returns null
return <>>;
}
- const value = getMappedNonEcsValue({
- data: data[pageRowIndex],
- fieldName: columnId,
- });
-
- return (
- <>
- {timelines.getHoverActions().getFilterOutValueButton({
- Component,
- field: columnId,
- filterManager,
- onFilterAdded,
- ownFocus: false,
- showTooltip: false,
- value,
- })}
- >
- );
+ return <>{filterOutButton(filterOutProps)}>;
},
({ data, pageSize }: { data: TimelineNonEcsData[][]; pageSize: number }) =>
- ({ rowIndex, columnId, Component }) => {
+ function AddToTimeline({ rowIndex, columnId, Component }) {
const { timelines } = useKibanaServices();
const pageRowIndex = getPageRowIndex(rowIndex, pageSize);
- if (pageRowIndex >= data.length) {
- // data grid expects each cell action always return an element, it crashes if returns null
- return <>>;
- }
+ const rowData = useMemo(() => {
+ return {
+ data: data[pageRowIndex],
+ fieldName: columnId,
+ };
+ }, [pageRowIndex, columnId]);
+
+ const value = useGetMappedNonEcsValue(rowData);
- const value = getMappedNonEcsValue({
- data: data[pageRowIndex],
- fieldName: columnId,
- });
+ const addToTimelineButton = useMemo(
+ () => timelines.getHoverActions().getAddToTimelineButton,
+ [timelines]
+ );
const dataProvider: DataProvider[] = useMemo(
() =>
@@ -196,48 +222,124 @@ export const cellActions: TGridCellAction[] = [
})) ?? [],
[columnId, rowIndex, value]
);
+ const addToTimelineProps = useMemo(() => {
+ return {
+ Component,
+ dataProvider,
+ field: columnId,
+ ownFocus: false,
+ showTooltip: false,
+ };
+ }, [Component, columnId, dataProvider]);
+ if (pageRowIndex >= data.length) {
+ // data grid expects each cell action always return an element, it crashes if returns null
+ return <>>;
+ }
- return (
- <>
- {timelines.getHoverActions().getAddToTimelineButton({
- Component,
- dataProvider,
- field: columnId,
- ownFocus: false,
- showTooltip: false,
- })}
- >
- );
+ return <>{addToTimelineButton(addToTimelineProps)}>;
},
({ data, pageSize }: { data: TimelineNonEcsData[][]; pageSize: number }) =>
- ({ rowIndex, columnId, Component }) => {
+ function CopyButton({ rowIndex, columnId, Component }) {
const { timelines } = useKibanaServices();
const pageRowIndex = getPageRowIndex(rowIndex, pageSize);
+
+ const copyButton = useMemo(() => timelines.getHoverActions().getCopyButton, [timelines]);
+
+ const rowData = useMemo(() => {
+ return {
+ data: data[pageRowIndex],
+ fieldName: columnId,
+ };
+ }, [pageRowIndex, columnId]);
+
+ const value = useGetMappedNonEcsValue(rowData);
+
+ const copyButtonProps = useMemo(() => {
+ return {
+ Component,
+ field: columnId,
+ isHoverAction: false,
+ ownFocus: false,
+ showTooltip: false,
+ value,
+ };
+ }, [Component, columnId, value]);
if (pageRowIndex >= data.length) {
// data grid expects each cell action always return an element, it crashes if returns null
return <>>;
}
- const value = getMappedNonEcsValue({
- data: data[pageRowIndex],
- fieldName: columnId,
- });
-
- return (
- <>
- {timelines.getHoverActions().getCopyButton({
- Component,
- field: columnId,
- isHoverAction: false,
- ownFocus: false,
- showTooltip: false,
- value,
- })}
- >
- );
+ return <>{copyButton(copyButtonProps)}>;
},
+ ({
+ data,
+ ecsData,
+ header,
+ timelineId,
+ pageSize,
+ }: {
+ data: TimelineNonEcsData[][];
+ ecsData: Ecs[];
+ header?: ColumnHeaderOptions;
+ timelineId: string;
+ pageSize: number;
+ }) => {
+ if (header !== undefined) {
+ return function FieldValue({
+ rowIndex,
+ columnId,
+ Component,
+ closePopover,
+ }: EuiDataGridColumnCellActionProps) {
+ const {
+ pageRowIndex,
+ link,
+ eventId,
+ value,
+ values,
+ title,
+ fieldName,
+ fieldFormat,
+ fieldType,
+ linkValue,
+ } = useFormattedFieldProps({ rowIndex, pageSize, ecsData, columnId, header, data });
+
+ const showEmpty = useMemo(() => {
+ const hasLink = link !== undefined && values && !isEmpty(value);
+ if (pageRowIndex >= data.length) {
+ return true;
+ } else {
+ return hasLink !== true;
+ }
+ }, [link, pageRowIndex, value, values]);
+
+ return showEmpty === false ? (
+
+ ) : (
+ // data grid expects each cell action always return an element, it crashes if returns null
+ <>>
+ );
+ };
+ } else {
+ return EmptyComponent;
+ }
+ },
];
/** the default actions shown in `EuiDataGrid` cells */
-export const defaultCellActions = [...cellActions, ...cellActionLink];
+export const defaultCellActions = [...cellActions];
diff --git a/x-pack/plugins/security_solution/public/common/lib/cell_actions/helpers.ts b/x-pack/plugins/security_solution/public/common/lib/cell_actions/helpers.ts
index 2db645106fc5cb..55347b156f4cb6 100644
--- a/x-pack/plugins/security_solution/public/common/lib/cell_actions/helpers.ts
+++ b/x-pack/plugins/security_solution/public/common/lib/cell_actions/helpers.ts
@@ -35,6 +35,11 @@ export const COLUMNS_WITH_LINKS = [
{
columnId: SIGNAL_RULE_NAME_FIELD_NAME,
label: i18n.VIEW_RULE_DETAILS,
+ linkField: 'kibana.alert.rule.uuid',
+ },
+ {
+ columnId: 'signal.rule.name',
+ label: i18n.VIEW_RULE_DETAILS,
linkField: 'signal.rule.id',
},
...PORT_NAMES.map((p) => ({
@@ -59,9 +64,22 @@ export const COLUMNS_WITH_LINKS = [
},
];
-export const getLink = (cId?: string, fieldType?: string, linkField?: string) =>
- COLUMNS_WITH_LINKS.find(
- (c) =>
- (cId && c.columnId === cId) ||
- (c.fieldType && fieldType === c.fieldType && (linkField != null || c.linkField !== undefined))
- );
+export const getLinkColumnDefinition = (
+ columnIdToFind: string,
+ fieldType?: string,
+ linkField?: string
+) => {
+ return COLUMNS_WITH_LINKS.find((column) => {
+ if (column.columnId === columnIdToFind) {
+ return true;
+ } else if (
+ column.fieldType &&
+ fieldType === column.fieldType &&
+ (linkField !== undefined || column.linkField !== undefined)
+ ) {
+ return true;
+ } else {
+ return false;
+ }
+ });
+};
diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.test.tsx
index 01ba47f728e430..3c34897fe2e657 100644
--- a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.test.tsx
@@ -44,8 +44,7 @@ jest.mock('@elastic/eui', () => {
};
});
-// Failing with rule registry enabled
-describe.skip('StepAboutRuleComponent', () => {
+describe('StepAboutRuleComponent', () => {
let formHook: RuleStepsFormHooks[RuleStep.aboutRule] | null = null;
const setFormHook = (
step: K,
@@ -149,14 +148,19 @@ describe.skip('StepAboutRuleComponent', () => {
);
+ wrapper
+ .find('[data-test-subj="detectionEngineStepAboutRuleDescription"] textarea')
+ .first()
+ .simulate('change', { target: { value: 'Test description text' } });
+ wrapper
+ .find('[data-test-subj="detectionEngineStepAboutRuleName"] input')
+ .first()
+ .simulate('change', { target: { value: 'Test name text' } });
+
await act(async () => {
if (!formHook) {
throw new Error('Form hook not set, but tests depend on it');
}
- wrapper
- .find('[data-test-subj="detectionEngineStepAboutThreatIndicatorPath"] input')
- .first()
- .simulate('change', { target: { value: '' } });
const result = await formHook();
expect(result?.isValid).toEqual(true);
diff --git a/x-pack/plugins/security_solution/public/detections/configurations/examples/observablity_alerts/render_cell_value.tsx b/x-pack/plugins/security_solution/public/detections/configurations/examples/observablity_alerts/render_cell_value.tsx
index cffe7bce4b5d3c..443aeef5f4f81a 100644
--- a/x-pack/plugins/security_solution/public/detections/configurations/examples/observablity_alerts/render_cell_value.tsx
+++ b/x-pack/plugins/security_solution/public/detections/configurations/examples/observablity_alerts/render_cell_value.tsx
@@ -13,7 +13,7 @@ import { ALERT_DURATION, ALERT_REASON, ALERT_SEVERITY, ALERT_STATUS } from '@kbn
import { TruncatableText } from '../../../../common/components/truncatable_text';
import { Severity } from '../../../components/severity';
-import { getMappedNonEcsValue } from '../../../../timelines/components/timeline/body/data_driven_columns';
+import { useGetMappedNonEcsValue } from '../../../../timelines/components/timeline/body/data_driven_columns';
import { CellValueElementProps } from '../../../../timelines/components/timeline/cell_rendering';
import { DefaultCellRenderer } from '../../../../timelines/components/timeline/cell_rendering/default_cell_renderer';
import { Status } from '../../../components/status';
@@ -43,7 +43,7 @@ export const RenderCellValue: React.FC<
timelineId,
}) => {
const value =
- getMappedNonEcsValue({
+ useGetMappedNonEcsValue({
data,
fieldName: columnId,
})?.reduce((x) => x[0]) ?? '';
diff --git a/x-pack/plugins/security_solution/public/detections/configurations/examples/security_solution_rac/render_cell_value.tsx b/x-pack/plugins/security_solution/public/detections/configurations/examples/security_solution_rac/render_cell_value.tsx
index 8c50e24cc3305e..3ee8b2fbee54df 100644
--- a/x-pack/plugins/security_solution/public/detections/configurations/examples/security_solution_rac/render_cell_value.tsx
+++ b/x-pack/plugins/security_solution/public/detections/configurations/examples/security_solution_rac/render_cell_value.tsx
@@ -12,7 +12,7 @@ import React from 'react';
import { DefaultDraggable } from '../../../../common/components/draggables';
import { TruncatableText } from '../../../../common/components/truncatable_text';
import { Severity } from '../../../components/severity';
-import { getMappedNonEcsValue } from '../../../../timelines/components/timeline/body/data_driven_columns';
+import { useGetMappedNonEcsValue } from '../../../../timelines/components/timeline/body/data_driven_columns';
import { CellValueElementProps } from '../../../../timelines/components/timeline/cell_rendering';
import { DefaultCellRenderer } from '../../../../timelines/components/timeline/cell_rendering/default_cell_renderer';
@@ -40,7 +40,7 @@ export const RenderCellValue: React.FC<
timelineId,
}) => {
const value =
- getMappedNonEcsValue({
+ useGetMappedNonEcsValue({
data,
fieldName: columnId,
})?.reduce((x) => x[0]) ?? '';
diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/mock.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/mock.ts
index 7aba8fa4ac10f1..58ec564c6911b3 100644
--- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/mock.ts
+++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/mock.ts
@@ -945,6 +945,289 @@ export const alertsMock: AlertSearchResponse = {
},
};
+export const alertsMock8x: AlertSearchResponse = {
+ took: 3,
+ timeout: false,
+ _shards: {
+ total: 1,
+ successful: 1,
+ skipped: 1,
+ failed: 0,
+ },
+ hits: {
+ total: {
+ value: 10000,
+ relation: 'gte',
+ },
+ hits: [
+ {
+ _index: '.internal.alerts-security.alerts-default-000001',
+ _id: 'f8946a2cb00640d079dcf3d1007f792a794974674cedfd7a42c047ba029f311d',
+ _score: null,
+ _source: {
+ 'kibana.alert.severity': 'low',
+ 'kibana.alert.rule.updated_by': 'elastic',
+ 'kibana.alert.rule.references': ['http://www.example.com/1'],
+ 'kibana.alert.rule.threat': [
+ {
+ framework: 'MITRE ATT&CK',
+ technique: [
+ {
+ reference: 'https://attack.mitre.org/techniques/T1217',
+ name: 'Browser Bookmark Discovery',
+ subtechnique: [],
+ id: 'T1217',
+ },
+ {
+ reference: 'https://attack.mitre.org/techniques/T1580',
+ name: 'Cloud Infrastructure Discovery',
+ subtechnique: [],
+ id: 'T1580',
+ },
+ {
+ reference: 'https://attack.mitre.org/techniques/T1033',
+ name: 'System Owner/User Discovery',
+ subtechnique: [],
+ id: 'T1033',
+ },
+ ],
+ tactic: {
+ reference: 'https://attack.mitre.org/tactics/TA0007',
+ name: 'Discovery',
+ id: 'TA0007',
+ },
+ },
+ {
+ framework: 'MITRE ATT&CK',
+ technique: [],
+ tactic: {
+ reference: 'https://attack.mitre.org/tactics/TA0007',
+ name: 'Discovery',
+ id: 'TA0007',
+ },
+ },
+ ],
+ 'kibana.alert.rule.rule_name_override': 'host.id',
+ 'kibana.alert.rule.description': '8.1: To Be Deleted',
+ 'kibana.alert.rule.tags': ['8.0-tag'],
+ 'kibana.alert.rule.producer': 'siem',
+ 'kibana.alert.rule.to': 'now',
+ 'kibana.alert.rule.created_by': 'elastic',
+ 'kibana.alert.original_event.ingested': '2022-01-11T22:43:03Z',
+ 'kibana.alert.risk_score': 37,
+ 'kibana.alert.rule.name': '944edf04-ea2d-44f9-b89a-574e9a9301da',
+ 'kibana.alert.original_event.id': '751afb02-94ee-46b7-9aea-1a7529374df9',
+ 'kibana.alert.workflow_status': 'open',
+ 'kibana.alert.rule.uuid': '63136880-7335-11ec-9f1b-9db9315083e9',
+ 'kibana.alert.original_event.category': 'driver',
+ 'kibana.alert.rule.risk_score_mapping': [
+ {
+ field: 'Responses.process.pid',
+ value: '',
+ operator: 'equals',
+ },
+ ],
+ 'kibana.alert.rule.interval': '5m',
+ 'kibana.alert.reason':
+ 'driver event with process powershell.exe, by 6nmm77jt8p on Host-7luvv0bmdn created low alert 944edf04-ea2d-44f9-b89a-574e9a9301da.',
+ 'kibana.alert.rule.type': 'query',
+ 'kibana.alert.rule.immutable': false,
+ 'kibana.alert.original_event.type': 'start',
+ 'kibana.alert.depth': 1,
+ 'kibana.alert.rule.enabled': true,
+ 'kibana.alert.rule.version': 1,
+ 'kibana.alert.rule.from': 'now-360s',
+ 'kibana.alert.rule.parameters': {
+ note: 'Investigation guuuide',
+ severity_mapping: [
+ {
+ severity: 'low',
+ field: 'host.name',
+ value: '',
+ operator: 'equals',
+ },
+ ],
+ references: ['http://www.example.com/1'],
+ description: '8.1: To Be Deleted',
+ language: 'kuery',
+ type: 'query',
+ rule_name_override: 'host.id',
+ exceptions_list: [],
+ from: 'now-360s',
+ severity: 'low',
+ max_signals: 100,
+ risk_score: 37,
+ risk_score_mapping: [
+ {
+ field: 'Responses.process.pid',
+ value: '',
+ operator: 'equals',
+ },
+ ],
+ author: ['author'],
+ query: 'host.name:*',
+ index: [
+ 'apm-*-transaction*',
+ 'traces-apm*',
+ 'auditbeat-*',
+ 'endgame-*',
+ 'filebeat-*',
+ 'logs-*',
+ 'packetbeat-*',
+ 'winlogbeat-*',
+ ],
+ filters: [],
+ version: 1,
+ rule_id: 'a2490dbb-33f6-4b03-88d8-b7d009ef58db',
+ license: 'license',
+ immutable: false,
+ meta: {
+ from: '1m',
+ kibana_siem_app_url: 'http://localhost:5601/kbn/app/security',
+ },
+ false_positives: ['fp'],
+ threat: [
+ {
+ framework: 'MITRE ATT&CK',
+ technique: [
+ {
+ reference: 'https://attack.mitre.org/techniques/T1217',
+ name: 'Browser Bookmark Discovery',
+ subtechnique: [],
+ id: 'T1217',
+ },
+ {
+ reference: 'https://attack.mitre.org/techniques/T1580',
+ name: 'Cloud Infrastructure Discovery',
+ subtechnique: [],
+ id: 'T1580',
+ },
+ {
+ reference: 'https://attack.mitre.org/techniques/T1033',
+ name: 'System Owner/User Discovery',
+ subtechnique: [],
+ id: 'T1033',
+ },
+ ],
+ tactic: {
+ reference: 'https://attack.mitre.org/tactics/TA0007',
+ name: 'Discovery',
+ id: 'TA0007',
+ },
+ },
+ {
+ framework: 'MITRE ATT&CK',
+ technique: [],
+ tactic: {
+ reference: 'https://attack.mitre.org/tactics/TA0007',
+ name: 'Discovery',
+ id: 'TA0007',
+ },
+ },
+ ],
+ to: 'now',
+ },
+ 'kibana.alert.status': 'active',
+ 'kibana.alert.ancestors': [
+ {
+ depth: 0,
+ index: '.ds-logs-endpoint.events.process-default-2022.01.11-000001',
+ id: 'VWxPS34B7OkM56GXH627',
+ type: 'event',
+ },
+ ],
+ 'kibana.alert.rule.exceptions_list': [],
+ 'kibana.alert.rule.actions': [],
+ 'kibana.alert.rule.rule_type_id': 'siem.queryRule',
+ 'kibana.alert.rule.license': 'license',
+ 'kibana.alert.original_event.kind': 'event',
+ 'kibana.alert.rule.note': 'Investigation guuuide',
+ 'kibana.alert.rule.severity_mapping': [
+ {
+ severity: 'low',
+ field: 'host.name',
+ value: '',
+ operator: 'equals',
+ },
+ ],
+ 'kibana.alert.rule.max_signals': 100,
+ 'kibana.alert.rule.updated_at': '2022-01-11T23:22:47.678Z',
+ 'kibana.alert.rule.risk_score': 37,
+ 'kibana.alert.rule.author': ['author'],
+ 'kibana.alert.rule.false_positives': ['fp'],
+ 'kibana.alert.rule.consumer': 'siem',
+ 'kibana.alert.rule.category': 'Custom Query Rule',
+ 'kibana.alert.original_event.sequence': 20,
+ 'kibana.alert.rule.created_at': '2022-01-11T23:22:47.678Z',
+ 'kibana.alert.rule.severity': 'low',
+ 'kibana.alert.original_event.agent_id_status': 'auth_metadata_missing',
+ 'kibana.alert.rule.meta.kibana_siem_app_url': 'http://localhost:5601/kbn/app/security',
+ 'kibana.alert.uuid': 'f8946a2cb00640d079dcf3d1007f792a794974674cedfd7a42c047ba029f311d',
+ 'kibana.alert.rule.meta.from': '1m',
+ 'kibana.alert.rule.rule_id': 'a2490dbb-33f6-4b03-88d8-b7d009ef58db',
+ 'kibana.alert.original_time': '2022-01-11T23:18:39.714Z',
+ },
+ fields: {
+ 'kibana.alert.severity': ['low'],
+ 'process.hash.md5': ['33d3568e-cf11-42fb-b36e-08aec99570e9'],
+ 'event.category': ['driver'],
+ 'user.name': ['6nmm77jt8p'],
+ 'process.parent.pid': [1975],
+ 'process.pid': [2121],
+ 'kibana.alert.rule.producer': ['siem'],
+ 'kibana.alert.rule.to': ['now'],
+ 'process.entity_id': ['3fadfesdk0'],
+ 'host.ip': ['10.248.183.44'],
+ 'agent.type': ['endpoint'],
+ 'kibana.alert.risk_score': [37],
+ 'kibana.alert.rule.name': ['944edf04-ea2d-44f9-b89a-574e9a9301da'],
+ 'host.name': ['Host-7luvv0bmdn'],
+ 'user.domain': ['epjr8uvmrj'],
+ 'event.kind': ['signal'],
+ 'kibana.alert.original_event.kind': ['event'],
+ 'host.id': ['944edf04-ea2d-44f9-b89a-574e9a9301da'],
+ 'process.executable': ['C:\\powershell.exe'],
+ 'kibana.alert.rule.note': ['Investigation guuuide'],
+ 'kibana.alert.workflow_status': ['open'],
+ 'kibana.alert.rule.uuid': ['63136880-7335-11ec-9f1b-9db9315083e9'],
+ 'kibana.alert.rule.risk_score': [37],
+ 'process.args': ['"C:\\powershell.exe" \\fzw'],
+ 'kibana.alert.reason': [
+ 'driver event with process powershell.exe, by 6nmm77jt8p on Host-7luvv0bmdn created low alert 944edf04-ea2d-44f9-b89a-574e9a9301da.',
+ ],
+ 'kibana.alert.rule.type': ['query'],
+ 'kibana.alert.rule.consumer': ['siem'],
+ 'kibana.alert.rule.category': ['Custom Query Rule'],
+ 'process.name': ['powershell.exe'],
+ '@timestamp': ['2022-01-11T23:22:52.034Z'],
+ 'kibana.alert.rule.severity': ['low'],
+ 'event.type': ['start'],
+ 'kibana.alert.uuid': ['f8946a2cb00640d079dcf3d1007f792a794974674cedfd7a42c047ba029f311d'],
+ 'kibana.alert.rule.version': ['1'],
+ 'event.id': ['751afb02-94ee-46b7-9aea-1a7529374df9'],
+ 'host.os.family': ['windows'],
+ 'kibana.alert.rule.from': ['now-360s'],
+ 'kibana.alert.rule.rule_id': ['a2490dbb-33f6-4b03-88d8-b7d009ef58db'],
+ 'kibana.alert.original_time': ['2022-01-11T23:18:39.714Z'],
+ },
+ sort: [1641943372034],
+ },
+ ],
+ },
+ aggregations: {
+ producers: {
+ doc_count_error_upper_bound: 0,
+ sum_other_doc_count: 0,
+ buckets: [
+ {
+ key: 'siem',
+ doc_count: 3,
+ },
+ ],
+ },
+ },
+};
+
export const mockAlertsQuery: object = {
aggs: {
alertsByGrouping: {
diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_with_fallback.test.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_with_fallback.test.tsx
index 1f08a356602152..40d2e8663b618c 100644
--- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_with_fallback.test.tsx
+++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_with_fallback.test.tsx
@@ -7,8 +7,11 @@
import { renderHook, act } from '@testing-library/react-hooks';
import { SecurityAppError } from '@kbn/securitysolution-t-grid';
+import { alertsMock8x } from '../alerts/mock';
+import { AlertSearchResponse } from '../alerts/types';
import { useRuleWithFallback } from './use_rule_with_fallback';
import * as api from './api';
+import * as alertsAPI from '../alerts/api';
import { useAppToastsMock } from '../../../../common/hooks/use_app_toasts.mock';
import { useAppToasts } from '../../../../common/hooks/use_app_toasts';
@@ -95,7 +98,7 @@ describe('useRuleWithFallback', () => {
});
});
- it("should fallback to fetching rule data from a signal if the rule doesn't exist", async () => {
+ it("should fallback to fetching rule data from a 7.x signal if the rule doesn't exist", async () => {
(api.fetchRuleById as jest.Mock).mockImplementation(async () => {
const err = new Error('Not found') as SecurityAppError;
err.body = { status_code: 404, message: 'Rule Not found' };
@@ -206,4 +209,148 @@ describe('useRuleWithFallback', () => {
`);
});
});
+
+ it("should fallback to fetching rule data from an 8.0 alert if the rule doesn't exist", async () => {
+ // Override default mock coming from ../alerts/__mocks__/api.ts
+ const spy = jest.spyOn(alertsAPI, 'fetchQueryAlerts').mockImplementation(async () => {
+ return alertsMock8x as AlertSearchResponse;
+ });
+
+ (api.fetchRuleById as jest.Mock).mockImplementation(async () => {
+ const err = new Error('Not found') as SecurityAppError;
+ err.body = { status_code: 404, message: 'Rule Not found' };
+ throw err;
+ });
+
+ await act(async () => {
+ const { result, waitForNextUpdate } = renderHook((id) => useRuleWithFallback(id), {
+ initialProps: 'testRuleId',
+ });
+ await waitForNextUpdate();
+ await waitForNextUpdate();
+
+ expect(result.current).toMatchInlineSnapshot(`
+ Object {
+ "error": [Error: Not found],
+ "isExistingRule": false,
+ "loading": false,
+ "refresh": [Function],
+ "rule": Object {
+ "actions": Array [],
+ "author": Array [
+ "author",
+ ],
+ "category": "Custom Query Rule",
+ "consumer": "siem",
+ "created_at": "2022-01-11T23:22:47.678Z",
+ "created_by": "elastic",
+ "description": "8.1: To Be Deleted",
+ "enabled": true,
+ "exceptions_list": Array [],
+ "false_positives": Array [
+ "fp",
+ ],
+ "filters": Array [],
+ "from": "now-360s",
+ "immutable": false,
+ "index": Array [
+ "apm-*-transaction*",
+ "traces-apm*",
+ "auditbeat-*",
+ "endgame-*",
+ "filebeat-*",
+ "logs-*",
+ "packetbeat-*",
+ "winlogbeat-*",
+ ],
+ "interval": "5m",
+ "language": "kuery",
+ "license": "license",
+ "max_signals": 100,
+ "meta": Object {
+ "from": "1m",
+ "kibana_siem_app_url": "http://localhost:5601/kbn/app/security",
+ },
+ "name": "944edf04-ea2d-44f9-b89a-574e9a9301da",
+ "note": "Investigation guuuide",
+ "producer": "siem",
+ "query": "host.name:*",
+ "references": Array [
+ "http://www.example.com/1",
+ ],
+ "risk_score": 37,
+ "risk_score_mapping": Array [
+ Object {
+ "field": "Responses.process.pid",
+ "operator": "equals",
+ "value": "",
+ },
+ ],
+ "rule_id": "a2490dbb-33f6-4b03-88d8-b7d009ef58db",
+ "rule_name_override": "host.id",
+ "rule_type_id": "siem.queryRule",
+ "severity": "low",
+ "severity_mapping": Array [
+ Object {
+ "field": "host.name",
+ "operator": "equals",
+ "severity": "low",
+ "value": "",
+ },
+ ],
+ "tags": Array [
+ "8.0-tag",
+ ],
+ "threat": Array [
+ Object {
+ "framework": "MITRE ATT&CK",
+ "tactic": Object {
+ "id": "TA0007",
+ "name": "Discovery",
+ "reference": "https://attack.mitre.org/tactics/TA0007",
+ },
+ "technique": Array [
+ Object {
+ "id": "T1217",
+ "name": "Browser Bookmark Discovery",
+ "reference": "https://attack.mitre.org/techniques/T1217",
+ "subtechnique": Array [],
+ },
+ Object {
+ "id": "T1580",
+ "name": "Cloud Infrastructure Discovery",
+ "reference": "https://attack.mitre.org/techniques/T1580",
+ "subtechnique": Array [],
+ },
+ Object {
+ "id": "T1033",
+ "name": "System Owner/User Discovery",
+ "reference": "https://attack.mitre.org/techniques/T1033",
+ "subtechnique": Array [],
+ },
+ ],
+ },
+ Object {
+ "framework": "MITRE ATT&CK",
+ "tactic": Object {
+ "id": "TA0007",
+ "name": "Discovery",
+ "reference": "https://attack.mitre.org/tactics/TA0007",
+ },
+ "technique": Array [],
+ },
+ ],
+ "to": "now",
+ "type": "query",
+ "updated_at": "2022-01-11T23:22:47.678Z",
+ "updated_by": "elastic",
+ "uuid": "63136880-7335-11ec-9f1b-9db9315083e9",
+ "version": 1,
+ },
+ }
+ `);
+ });
+ // Reset back to default mock coming from ../alerts/__mocks__/api.ts
+ spy.mockRestore();
+ });
});
diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_with_fallback.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_with_fallback.tsx
index 8c8736b03b2294..cda7b7aab0af2f 100644
--- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_with_fallback.tsx
+++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_with_fallback.tsx
@@ -9,8 +9,10 @@ import { useCallback, useEffect, useMemo } from 'react';
import { ALERT_RULE_UUID } from '@kbn/rule-data-utils';
import { useAsync, withOptionalSignal } from '@kbn/securitysolution-hook-utils';
import { isNotFoundError } from '@kbn/securitysolution-t-grid';
+import { expandDottedObject } from '../../../../../common/utils/expand_dotted';
import { useAppToasts } from '../../../../common/hooks/use_app_toasts';
+import { AlertSearchResponse } from '../alerts/types';
import { useQueryAlerts } from '../alerts/use_query';
import { fetchRuleById } from './api';
import { transformInput } from './transforms';
@@ -41,9 +43,20 @@ interface AlertHit {
};
}
-const fetchWithOptionslSignal = withOptionalSignal(fetchRuleById);
+// TODO: Create proper types for nested/flattened RACRule once contract w/ Fields API is finalized.
+interface RACRule {
+ kibana: {
+ alert: {
+ rule: {
+ parameters?: {};
+ };
+ };
+ };
+}
-const useFetchRule = () => useAsync(fetchWithOptionslSignal);
+const fetchWithOptionsSignal = withOptionalSignal(fetchRuleById);
+
+const useFetchRule = () => useAsync(fetchWithOptionsSignal);
const buildLastAlertQuery = (ruleId: string) => ({
query: {
@@ -94,10 +107,11 @@ export const useRuleWithFallback = (ruleId: string): UseRuleWithFallback => {
}, [addError, error]);
const rule = useMemo(() => {
- const hit = alertsData?.hits.hits[0];
const result = isExistingRule
? ruleData
- : hit?._source.signal?.rule ?? hit?._source.kibana?.alert?.rule;
+ : alertsData == null
+ ? undefined
+ : transformRuleFromAlertHit(alertsData);
if (result) {
return transformInput(result);
}
@@ -111,3 +125,27 @@ export const useRuleWithFallback = (ruleId: string): UseRuleWithFallback => {
isExistingRule,
};
};
+
+/**
+ * Transforms an alertHit into a Rule
+ * @param data raw response containing single alert
+ */
+export const transformRuleFromAlertHit = (data: AlertSearchResponse): Rule | undefined => {
+ const hit = data?.hits.hits[0] as AlertHit | undefined;
+
+ // If pre 8.x alert, pull directly from alertHit
+ const rule = hit?._source.signal?.rule ?? hit?._source.kibana?.alert?.rule;
+
+ // If rule undefined, response likely flattened
+ if (rule == null) {
+ const expandedRuleWithParams = expandDottedObject(hit?._source ?? {}) as RACRule;
+ const expandedRule = {
+ ...expandedRuleWithParams?.kibana?.alert?.rule,
+ ...expandedRuleWithParams?.kibana?.alert?.rule?.parameters,
+ };
+ delete expandedRule.parameters;
+ return expandedRule as Rule;
+ }
+
+ return rule;
+};
diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/columns.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/columns.tsx
index 17eafecbae34f9..2f5f347b0e8f61 100644
--- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/columns.tsx
+++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/columns.tsx
@@ -154,16 +154,19 @@ export const getAllExceptionListsColumns = (
),
},
{
- render: ({ id, list_id: listId, namespace_type: namespaceType }: ExceptionListInfo) => (
-
- ),
+ render: ({ id, list_id: listId, namespace_type: namespaceType }: ExceptionListInfo) => {
+ return listId === 'endpoint_list' ? (
+ <>>
+ ) : (
+
+ );
+ },
},
],
},
diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.test.tsx
index 71ffc1076a2270..dae79e8f552f6c 100644
--- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.test.tsx
+++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.test.tsx
@@ -88,7 +88,7 @@ describe('ExceptionListsTable', () => {
]);
});
- it('renders delete option disabled if list is "endpoint_list"', async () => {
+ it('does not render delete option disabled if list is "endpoint_list"', async () => {
const wrapper = mount(
@@ -97,15 +97,13 @@ describe('ExceptionListsTable', () => {
expect(wrapper.find('[data-test-subj="exceptionsTableListId"]').at(0).text()).toEqual(
'endpoint_list'
);
- expect(
- wrapper.find('[data-test-subj="exceptionsTableDeleteButton"] button').at(0).prop('disabled')
- ).toBeTruthy();
expect(wrapper.find('[data-test-subj="exceptionsTableListId"]').at(1).text()).toEqual(
'not_endpoint_list'
);
+ expect(wrapper.find('[data-test-subj="exceptionsTableDeleteButton"] button')).toHaveLength(1);
expect(
- wrapper.find('[data-test-subj="exceptionsTableDeleteButton"] button').at(1).prop('disabled')
+ wrapper.find('[data-test-subj="exceptionsTableDeleteButton"] button').at(0).prop('disabled')
).toBeFalsy();
});
});
diff --git a/x-pack/plugins/security_solution/public/helpers.tsx b/x-pack/plugins/security_solution/public/helpers.tsx
index 2236b1802df297..40feb9e0b3edac 100644
--- a/x-pack/plugins/security_solution/public/helpers.tsx
+++ b/x-pack/plugins/security_solution/public/helpers.tsx
@@ -5,7 +5,7 @@
* 2.0.
*/
-import { ALERT_RULE_UUID, ALERT_RULE_PARAMETERS } from '@kbn/rule-data-utils';
+import { ALERT_RULE_UUID, ALERT_RULE_NAME, ALERT_RULE_PARAMETERS } from '@kbn/rule-data-utils';
import { has, get, isEmpty } from 'lodash/fp';
import React from 'react';
import { matchPath, RouteProps, Redirect } from 'react-router-dom';
@@ -209,6 +209,7 @@ RedirectRoute.displayName = 'RedirectRoute';
const siemSignalsFieldMappings: Record = {
[ALERT_RULE_UUID]: 'signal.rule.id',
+ [ALERT_RULE_NAME]: 'signal.rule.name',
[`${ALERT_RULE_PARAMETERS}.filters`]: 'signal.rule.filters',
[`${ALERT_RULE_PARAMETERS}.language`]: 'signal.rule.language',
[`${ALERT_RULE_PARAMETERS}.query`]: 'signal.rule.query',
@@ -216,6 +217,7 @@ const siemSignalsFieldMappings: Record = {
const alertFieldMappings: Record = {
'signal.rule.id': ALERT_RULE_UUID,
+ 'signal.rule.name': ALERT_RULE_NAME,
'signal.rule.filters': `${ALERT_RULE_PARAMETERS}.filters`,
'signal.rule.language': `${ALERT_RULE_PARAMETERS}.language`,
'signal.rule.query': `${ALERT_RULE_PARAMETERS}.query`,
diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/index.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/index.tsx
index 249345a0a0ad85..48356808a50434 100644
--- a/x-pack/plugins/security_solution/public/management/pages/policy/index.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/policy/index.tsx
@@ -7,18 +7,21 @@
import React, { memo } from 'react';
import { Route, Switch, Redirect } from 'react-router-dom';
-import { PolicyDetails } from './view';
+import { PolicyDetails, PolicyList } from './view';
import {
MANAGEMENT_ROUTING_POLICY_DETAILS_FORM_PATH,
MANAGEMENT_ROUTING_POLICY_DETAILS_TRUSTED_APPS_PATH,
MANAGEMENT_ROUTING_POLICY_DETAILS_EVENT_FILTERS_PATH,
MANAGEMENT_ROUTING_POLICY_DETAILS_PATH_OLD,
MANAGEMENT_ROUTING_POLICY_DETAILS_HOST_ISOLATION_EXCEPTIONS_PATH,
+ MANAGEMENT_ROUTING_POLICIES_PATH,
} from '../../common/constants';
import { NotFoundPage } from '../../../app/404';
import { getPolicyDetailPath } from '../../common/routing';
+import { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_experimental_features';
export const PolicyContainer = memo(() => {
+ const isPolicyListEnabled = useIsExperimentalFeatureEnabled('policyListEnabled');
return (
{
exact
render={(props) => }
/>
+ {isPolicyListEnabled && (
+
+ )}
);
diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/index.ts b/x-pack/plugins/security_solution/public/management/pages/policy/view/index.ts
index d2bea12741b991..540484a7109135 100644
--- a/x-pack/plugins/security_solution/public/management/pages/policy/view/index.ts
+++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/index.ts
@@ -5,5 +5,6 @@
* 2.0.
*/
+export * from './policy_list';
export * from './policy_details';
export * from './policy_advanced';
diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.tsx
new file mode 100644
index 00000000000000..472f4a7ba6c6b6
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.tsx
@@ -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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { memo } from 'react';
+import { AdministrationListPage } from '../../../components/administration_list_page';
+
+export const PolicyList = memo(() => {
+ return (
+
+ );
+});
+
+PolicyList.displayName = 'PolicyList';
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/index.tsx
index 82207906a62953..a2638c7b8eb0b5 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/index.tsx
@@ -448,3 +448,13 @@ export const getMappedNonEcsValue = ({
}
return undefined;
};
+
+export const useGetMappedNonEcsValue = ({
+ data,
+ fieldName,
+}: {
+ data: TimelineNonEcsData[];
+ fieldName: string;
+}): string[] | undefined => {
+ return useMemo(() => getMappedNonEcsValue({ data, fieldName }), [data, fieldName]);
+};
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/stateful_cell.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/stateful_cell.test.tsx
index 3e22cba208ca2d..6f9bcc61a96931 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/stateful_cell.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/stateful_cell.test.tsx
@@ -18,7 +18,7 @@ import {
} from '../../../../../../common/types/timeline';
import { StatefulCell } from './stateful_cell';
-import { getMappedNonEcsValue } from '.';
+import { useGetMappedNonEcsValue } from '.';
/**
* This (test) component implement's `EuiDataGrid`'s `renderCellValue` interface,
@@ -30,14 +30,13 @@ import { getMappedNonEcsValue } from '.';
* https://codesandbox.io/s/zhxmo
*/
const RenderCellValue: React.FC = ({ columnId, data, setCellProps }) => {
+ const value = useGetMappedNonEcsValue({
+ data,
+ fieldName: columnId,
+ });
useEffect(() => {
// branching logic that conditionally renders a specific cell green:
if (columnId === defaultHeaders[0].id) {
- const value = getMappedNonEcsValue({
- data,
- fieldName: columnId,
- });
-
if (value?.length) {
setCellProps({
style: {
@@ -46,16 +45,9 @@ const RenderCellValue: React.FC = ({ columnId, data, setC
});
}
}
- }, [columnId, data, setCellProps]);
+ }, [columnId, data, setCellProps, value]);
- return (
-
- {getMappedNonEcsValue({
- data,
- fieldName: columnId,
- })}
-
- );
+ return {value}
;
};
describe('StatefulCell', () => {
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx
index 82f9cedc57a9cd..3f85551d005d0f 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx
@@ -39,7 +39,7 @@ import { getRowRenderer } from '../renderers/get_row_renderer';
import { StatefulRowRenderer } from './stateful_row_renderer';
import { NOTES_BUTTON_CLASS_NAME } from '../../properties/helpers';
import { timelineDefaults } from '../../../../store/timeline/defaults';
-import { getMappedNonEcsValue } from '../data_driven_columns';
+import { useGetMappedNonEcsValue } from '../data_driven_columns';
import { StatefulEventContext } from '../../../../../../../timelines/public';
interface Props {
@@ -115,21 +115,23 @@ const StatefulEventComponent: React.FC = ({
const expandedDetail = useDeepEqualSelector(
(state) => (getTimeline(state, timelineId) ?? timelineDefaults).expandedDetail ?? {}
);
+ const hostNameArr = useGetMappedNonEcsValue({ data: event?.data, fieldName: 'host.name' });
+
const hostName = useMemo(() => {
- const hostNameArr = getMappedNonEcsValue({ data: event?.data, fieldName: 'host.name' });
return hostNameArr && hostNameArr.length > 0 ? hostNameArr[0] : null;
- }, [event?.data]);
-
+ }, [hostNameArr]);
+ const hostIpList = useGetMappedNonEcsValue({ data: event?.data, fieldName: 'host.ip' });
+ const sourceIpList = useGetMappedNonEcsValue({ data: event?.data, fieldName: 'source.ip' });
+ const destinationIpList = useGetMappedNonEcsValue({
+ data: event?.data,
+ fieldName: 'destination.ip',
+ });
const hostIPAddresses = useMemo(() => {
- const hostIpList = getMappedNonEcsValue({ data: event?.data, fieldName: 'host.ip' }) ?? [];
- const sourceIpList = getMappedNonEcsValue({ data: event?.data, fieldName: 'source.ip' }) ?? [];
- const destinationIpList =
- getMappedNonEcsValue({
- data: event?.data,
- fieldName: 'destination.ip',
- }) ?? [];
- return new Set([...hostIpList, ...sourceIpList, ...destinationIpList]);
- }, [event?.data]);
+ const hostIps = hostIpList ?? [];
+ const sourceIps = sourceIpList ?? [];
+ const destinationIps = destinationIpList ?? [];
+ return new Set([...hostIps, ...sourceIps, ...destinationIps]);
+ }, [destinationIpList, sourceIpList, hostIpList]);
const activeTab = tabType ?? TimelineTabs.query;
const activeExpandedDetail = expandedDetail[activeTab];
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field.tsx
index ffd8da99bb6079..ce120f10d2fb39 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field.tsx
@@ -221,7 +221,6 @@ const FormattedFieldValueComponent: React.FC<{
Component,
eventId,
fieldName,
- linkValue,
isDraggable,
truncate,
title,
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field_helpers.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field_helpers.tsx
index f25f80aee1d691..267126843b5179 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field_helpers.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field_helpers.tsx
@@ -250,7 +250,6 @@ export const renderUrl = ({
eventId,
fieldName,
isDraggable,
- linkValue,
truncate,
title,
value,
@@ -261,7 +260,6 @@ export const renderUrl = ({
eventId: string;
fieldName: string;
isDraggable: boolean;
- linkValue: string | null | undefined;
truncate?: boolean;
title?: string;
value: string | number | null | undefined;
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/cell_rendering/default_cell_renderer.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/cell_rendering/default_cell_renderer.tsx
index c5b63cb5989696..7abd2c8ef04833 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/cell_rendering/default_cell_renderer.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/cell_rendering/default_cell_renderer.tsx
@@ -5,14 +5,14 @@
* 2.0.
*/
-import React from 'react';
+import React, { useMemo } from 'react';
-import { getMappedNonEcsValue } from '../body/data_driven_columns';
+import { useGetMappedNonEcsValue } from '../body/data_driven_columns';
import { columnRenderers } from '../body/renderers';
import { getColumnRenderer } from '../body/renderers/get_column_renderer';
import { CellValueElementProps } from '.';
-import { getLink } from '../../../../common/lib/cell_actions/helpers';
+import { getLinkColumnDefinition } from '../../../../common/lib/cell_actions/helpers';
import { FIELDS_WITHOUT_CELL_ACTIONS } from '../../../../common/lib/cell_actions/constants';
import {
ExpandedCellValueActions,
@@ -39,7 +39,10 @@ export const DefaultCellRenderer: React.FC = ({
timelineId,
truncate,
}) => {
- const values = getMappedNonEcsValue({
+ const asPlainText = useMemo(() => {
+ return getLinkColumnDefinition(header.id, header.type) !== undefined && !isTimeline;
+ }, [header.id, header.type, isTimeline]);
+ const values = useGetMappedNonEcsValue({
data,
fieldName: header.id,
});
@@ -50,7 +53,7 @@ export const DefaultCellRenderer: React.FC = ({
<>
{getColumnRenderer(header.id, columnRenderers, data).renderColumn({
- asPlainText: !!getLink(header.id, header.type) && !isTimeline, // we want to render value with links as plain text but keep other formatters like badge.
+ asPlainText, // we want to render value with links as plain text but keep other formatters like badge.
browserFields,
columnName: header.id,
ecsData,
@@ -62,10 +65,7 @@ export const DefaultCellRenderer: React.FC = ({
rowRenderers,
timelineId,
truncate,
- values: getMappedNonEcsValue({
- data,
- fieldName: header.id,
- }),
+ values,
})}
{isDetails && browserFields && hasCellActions(header.id) && (
diff --git a/x-pack/plugins/security_solution/server/endpoint/mocks.ts b/x-pack/plugins/security_solution/server/endpoint/mocks.ts
index 95a1f92ea94cd0..b697b54994adcc 100644
--- a/x-pack/plugins/security_solution/server/endpoint/mocks.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/mocks.ts
@@ -11,12 +11,13 @@ import { listMock } from '../../../lists/server/mocks';
import { securityMock } from '../../../security/server/mocks';
import { alertsMock } from '../../../alerting/server/mocks';
import { xpackMocks } from '../fixtures';
-import { FleetStartContract, ExternalCallback, PackageService } from '../../../fleet/server';
+import { FleetStartContract, ExternalCallback } from '../../../fleet/server';
import {
createPackagePolicyServiceMock,
createMockAgentPolicyService,
createMockAgentService,
createArtifactsClientMock,
+ createMockPackageService,
} from '../../../fleet/server/mocks';
import { createMockConfig, requestContextMock } from '../lib/detection_engine/routes/__mocks__';
import {
@@ -135,17 +136,6 @@ export const createMockEndpointAppContextServiceStartContract =
};
};
-/**
- * Create mock PackageService
- */
-
-export const createMockPackageService = (): jest.Mocked => {
- return {
- getInstallation: jest.fn(),
- ensureInstalledPackage: jest.fn(),
- };
-};
-
/**
* Creates a mock IndexPatternService for use in tests that need to interact with the Fleet's
* ESIndexPatternService.
@@ -168,7 +158,6 @@ export const createMockFleetStartContract = (indexPattern: string): FleetStartCo
registerExternalCallback: jest.fn((...args: ExternalCallback) => {}),
packagePolicyService: createPackagePolicyServiceMock(),
createArtifactsClient: jest.fn().mockReturnValue(createArtifactsClientMock()),
- fetchFindLatestPackage: jest.fn().mockReturnValue('8.0.0'),
};
};
diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/actions/isolation.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/actions/isolation.test.ts
index 06b85b4d08c8a6..559ecf0a621d64 100644
--- a/x-pack/plugins/security_solution/server/endpoint/routes/actions/isolation.test.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/routes/actions/isolation.test.ts
@@ -20,7 +20,6 @@ import { EndpointAppContextService } from '../../endpoint_app_context_services';
import {
createMockEndpointAppContextServiceSetupContract,
createMockEndpointAppContextServiceStartContract,
- createMockPackageService,
createRouteHandlerContext,
} from '../../mocks';
import { HostIsolationRequestSchema } from '../../../../common/endpoint/schema/actions';
@@ -49,6 +48,8 @@ import { legacyMetadataSearchResponseMock } from '../metadata/support/test_suppo
import { AGENT_ACTIONS_INDEX, ElasticsearchAssetType } from '../../../../../fleet/common';
import { CasesClientMock } from '../../../../../cases/server/client/mocks';
import { EndpointAuthz } from '../../../../common/endpoint/types/authz';
+import type { PackageClient } from '../../../../../fleet/server';
+import { createMockPackageService } from '../../../../../fleet/server/mocks';
interface CallRouteInterface {
body?: HostIsolationRequestBody;
@@ -135,31 +136,29 @@ describe('Host Isolation', () => {
endpointAppContextService = new EndpointAppContextService();
const mockSavedObjectClient = savedObjectsClientMock.create();
const mockPackageService = createMockPackageService();
- mockPackageService.getInstallation.mockReturnValue(
- Promise.resolve({
- installed_kibana: [],
- package_assets: [],
- es_index_patterns: {},
- name: '',
- version: '',
- install_status: 'installed',
- install_version: '',
- install_started_at: '',
- install_source: 'registry',
- installed_es: [
- {
- dupa: true,
- id: 'logs-endpoint.events.security',
- type: ElasticsearchAssetType.indexTemplate,
- },
- {
- id: `${metadataTransformPrefix}-0.16.0-dev.0`,
- type: ElasticsearchAssetType.transform,
- },
- ],
- keep_policies_up_to_date: false,
- })
- );
+ const mockedPackageClient = mockPackageService.asInternalUser as jest.Mocked;
+ mockedPackageClient.getInstallation.mockResolvedValue({
+ installed_kibana: [],
+ package_assets: [],
+ es_index_patterns: {},
+ name: '',
+ version: '',
+ install_status: 'installed',
+ install_version: '',
+ install_started_at: '',
+ install_source: 'registry',
+ installed_es: [
+ {
+ id: 'logs-endpoint.events.security',
+ type: ElasticsearchAssetType.indexTemplate,
+ },
+ {
+ id: `${metadataTransformPrefix}-0.16.0-dev.0`,
+ type: ElasticsearchAssetType.transform,
+ },
+ ],
+ keep_policies_up_to_date: false,
+ });
licenseEmitter = new Subject();
licenseService = new LicenseService();
diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata.test.ts
index 54bd4532fc0645..ee5a79b1aefff1 100644
--- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata.test.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata.test.ts
@@ -24,7 +24,6 @@ import { registerEndpointRoutes } from './index';
import {
createMockEndpointAppContextServiceSetupContract,
createMockEndpointAppContextServiceStartContract,
- createMockPackageService,
createRouteHandlerContext,
} from '../../mocks';
import {
@@ -38,7 +37,7 @@ import {
legacyMetadataSearchResponseMock,
unitedMetadataSearchResponseMock,
} from './support/test_support';
-import { AgentClient, PackageService } from '../../../../../fleet/server/services';
+import type { AgentClient, PackageService, PackageClient } from '../../../../../fleet/server';
import {
HOST_METADATA_GET_ROUTE,
HOST_METADATA_LIST_ROUTE,
@@ -57,7 +56,7 @@ import {
} from '../../../../../../../src/core/server/elasticsearch/client/mocks';
import { EndpointHostNotFoundError } from '../../services/metadata';
import { FleetAgentGenerator } from '../../../../common/endpoint/data_generators/fleet_agent_generator';
-import { createMockAgentClient } from '../../../../../fleet/server/mocks';
+import { createMockAgentClient, createMockPackageService } from '../../../../../fleet/server/mocks';
import { TransformGetTransformStatsResponse } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { getEndpointAuthzInitialStateMock } from '../../../../common/endpoint/service/authz';
@@ -76,7 +75,7 @@ describe('test endpoint routes', () => {
let mockClusterClient: ClusterClientMock;
let mockScopedClient: ScopedClusterClientMock;
let mockSavedObjectClient: jest.Mocked;
- let mockPackageService: jest.Mocked;
+ let mockPackageService: PackageService;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let routeHandler: RequestHandler;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -121,30 +120,29 @@ describe('test endpoint routes', () => {
endpointAppContextService = new EndpointAppContextService();
mockPackageService = createMockPackageService();
- mockPackageService.getInstallation.mockReturnValue(
- Promise.resolve({
- installed_kibana: [],
- package_assets: [],
- es_index_patterns: {},
- name: '',
- version: '',
- install_status: 'installed',
- install_version: '',
- install_started_at: '',
- install_source: 'registry',
- installed_es: [
- {
- id: 'logs-endpoint.events.security',
- type: ElasticsearchAssetType.indexTemplate,
- },
- {
- id: `${metadataTransformPrefix}-0.16.0-dev.0`,
- type: ElasticsearchAssetType.transform,
- },
- ],
- keep_policies_up_to_date: false,
- })
- );
+ const mockPackageClient = mockPackageService.asInternalUser as jest.Mocked;
+ mockPackageClient.getInstallation.mockResolvedValue({
+ installed_kibana: [],
+ package_assets: [],
+ es_index_patterns: {},
+ name: '',
+ version: '',
+ install_status: 'installed',
+ install_version: '',
+ install_started_at: '',
+ install_source: 'registry',
+ installed_es: [
+ {
+ id: 'logs-endpoint.events.security',
+ type: ElasticsearchAssetType.indexTemplate,
+ },
+ {
+ id: `${metadataTransformPrefix}-0.16.0-dev.0`,
+ type: ElasticsearchAssetType.transform,
+ },
+ ],
+ keep_policies_up_to_date: false,
+ });
endpointAppContextService.setup(createMockEndpointAppContextServiceSetupContract());
endpointAppContextService.start({ ...startContract, packageService: mockPackageService });
mockAgentService = startContract.agentService!;
diff --git a/x-pack/plugins/security_solution/server/endpoint/services/endpoint_fleet_services.ts b/x-pack/plugins/security_solution/server/endpoint/services/endpoint_fleet_services.ts
index 0c26582f920b1a..915070a9b064fd 100644
--- a/x-pack/plugins/security_solution/server/endpoint/services/endpoint_fleet_services.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/services/endpoint_fleet_services.ts
@@ -11,7 +11,7 @@ import type {
AgentPolicyServiceInterface,
FleetStartContract,
PackagePolicyServiceInterface,
- PackageService,
+ PackageClient,
} from '../../../../fleet/server';
export interface EndpointFleetServicesFactoryInterface {
@@ -33,13 +33,13 @@ export class EndpointFleetServicesFactory implements EndpointFleetServicesFactor
agentPolicyService: agentPolicy,
packagePolicyService: packagePolicy,
agentService,
- packageService: packages,
+ packageService,
} = this.fleetDependencies;
return {
agent: agentService.asScoped(req),
agentPolicy,
- packages,
+ packages: packageService.asScoped(req),
packagePolicy,
asInternal: this.asInternalUser.bind(this),
@@ -51,13 +51,13 @@ export class EndpointFleetServicesFactory implements EndpointFleetServicesFactor
agentPolicyService: agentPolicy,
packagePolicyService: packagePolicy,
agentService,
- packageService: packages,
+ packageService,
} = this.fleetDependencies;
return {
agent: agentService.asInternalUser,
agentPolicy,
- packages,
+ packages: packageService.asInternalUser,
packagePolicy,
asScoped: this.asScoped.bind(this),
@@ -71,7 +71,7 @@ export class EndpointFleetServicesFactory implements EndpointFleetServicesFactor
export interface EndpointFleetServicesInterface {
agent: AgentClient;
agentPolicy: AgentPolicyServiceInterface;
- packages: PackageService;
+ packages: PackageClient;
packagePolicy: PackagePolicyServiceInterface;
}
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_rules_notification_alert_type.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_rules_notification_alert_type.test.ts
index 6c0ffa65a9afa2..f064380cc4a13a 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_rules_notification_alert_type.test.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_rules_notification_alert_type.test.ts
@@ -88,7 +88,7 @@ describe('legacyRules_notification_alert_type', () => {
});
await alert.executor(payload);
expect(logger.error).toHaveBeenCalledWith(
- `Security Solution notification (Legacy) saved object for alert ${payload.params.ruleAlertId} was not found`
+ `Security Solution notification (Legacy) saved object for alert ${payload.params.ruleAlertId} was not found with id: \"1111\". space id: \"\" This indicates a dangling (Legacy) notification alert. You should delete this rule through \"Kibana UI -> Stack Management -> Rules and Connectors\" to remove this error message.`
);
});
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_rules_notification_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_rules_notification_alert_type.ts
index bbcee3897d23e2..6a5a9478681f3c 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_rules_notification_alert_type.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_rules_notification_alert_type.ts
@@ -51,11 +51,7 @@ export const legacyRulesNotificationAlertType = ({
},
minimumLicenseRequired: 'basic',
isExportable: false,
- async executor({ startedAt, previousStartedAt, alertId, services, params }) {
- // TODO: Change this to be a link to documentation on how to migrate: https://github.com/elastic/kibana/issues/113055
- logger.warn(
- 'Security Solution notification (Legacy) system detected still running. Please see documentation on how to migrate to the new notification system.'
- );
+ async executor({ startedAt, previousStartedAt, alertId, services, params, spaceId }) {
const ruleAlertSavedObject = await services.savedObjectsClient.get(
'alert',
params.ruleAlertId
@@ -63,17 +59,26 @@ export const legacyRulesNotificationAlertType = ({
if (!ruleAlertSavedObject.attributes.params) {
logger.error(
- `Security Solution notification (Legacy) saved object for alert ${params.ruleAlertId} was not found`
+ [
+ `Security Solution notification (Legacy) saved object for alert ${params.ruleAlertId} was not found with`,
+ `id: "${alertId}".`,
+ `space id: "${spaceId}"`,
+ 'This indicates a dangling (Legacy) notification alert.',
+ 'You should delete this rule through "Kibana UI -> Stack Management -> Rules and Connectors" to remove this error message.',
+ ].join(' ')
);
return;
}
+
logger.warn(
[
'Security Solution notification (Legacy) system still active for alert with',
`name: "${ruleAlertSavedObject.attributes.name}"`,
`description: "${ruleAlertSavedObject.attributes.params.description}"`,
`id: "${ruleAlertSavedObject.id}".`,
- `Please see documentation on how to migrate to the new notification system.`,
+ `space id: "${spaceId}"`,
+ 'Editing or updating this rule through "Kibana UI -> Security -> Alerts -> Manage Rules"',
+ 'will auto-migrate the rule to the new notification system and remove this warning message.',
].join(' ')
);
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/schedule_notification_actions.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/schedule_notification_actions.ts
index 2362a6a392a56e..9b20b031eea0f1 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/schedule_notification_actions.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/schedule_notification_actions.ts
@@ -7,7 +7,7 @@
import { mapKeys, snakeCase } from 'lodash/fp';
import { AlertInstance } from '../../../../../alerting/server';
-import { expandDottedObject } from '../rule_types/utils';
+import { expandDottedObject } from '../../../../common/utils/expand_dotted';
import { RuleParams } from '../schemas/rule_schemas';
import aadFieldConversion from '../routes/index/signal_aad_mapping.json';
import { isRACAlert } from '../signals/utils';
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.test.ts
index a094ea84e9bf1a..3ec8cb733aa287 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.test.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.test.ts
@@ -11,7 +11,6 @@ import {
getFindResultWithSingleHit,
getAlertMock,
getBasicEmptySearchResponse,
- getBasicNoShardsSearchResponse,
} from '../__mocks__/request_responses';
import { configMock, requestContextMock, serverMock } from '../__mocks__';
import { AddPrepackagedRulesSchemaDecoded } from '../../../../../common/detection_engine/schemas/request/add_prepackaged_rules_schema';
@@ -71,15 +70,10 @@ jest.mock('../../../timeline/routes/prepackaged_timelines/install_prepackaged_ti
};
});
-// Failing with rule registry enabled
-describe.skip.each([
- ['Legacy', false],
- ['RAC', true],
-])('add_prepackaged_rules_route - %s', (_, isRuleRegistryEnabled) => {
+describe('add_prepackaged_rules_route', () => {
let server: ReturnType;
let { clients, context } = requestContextMock.createTools();
let mockExceptionsClient: ExceptionListClient;
- const testif = isRuleRegistryEnabled ? test.skip : test;
const defaultConfig = context.securitySolution.getConfig();
beforeEach(() => {
@@ -88,13 +82,11 @@ describe.skip.each([
mockExceptionsClient = listMock.getExceptionListClient();
context.securitySolution.getConfig.mockImplementation(() =>
- configMock.withRuleRegistryEnabled(defaultConfig, isRuleRegistryEnabled)
+ configMock.withRuleRegistryEnabled(defaultConfig, true)
);
- clients.rulesClient.find.mockResolvedValue(getFindResultWithSingleHit(isRuleRegistryEnabled));
- clients.rulesClient.update.mockResolvedValue(
- getAlertMock(isRuleRegistryEnabled, getQueryRuleParams())
- );
+ clients.rulesClient.find.mockResolvedValue(getFindResultWithSingleHit(true));
+ clients.rulesClient.update.mockResolvedValue(getAlertMock(true, getQueryRuleParams()));
(installPrepackagedTimelines as jest.Mock).mockReset();
(installPrepackagedTimelines as jest.Mock).mockResolvedValue({
@@ -131,26 +123,6 @@ describe.skip.each([
});
});
- test('it returns a 400 if the index does not exist when rule registry not enabled', async () => {
- const request = addPrepackagedRulesRequest();
- context.core.elasticsearch.client.asCurrentUser.search.mockResolvedValueOnce(
- elasticsearchClientMock.createSuccessTransportRequestPromise(
- getBasicNoShardsSearchResponse()
- )
- );
- const response = await server.inject(request, context);
-
- expect(response.status).toEqual(isRuleRegistryEnabled ? 200 : 400);
- if (!isRuleRegistryEnabled) {
- expect(response.body).toEqual({
- status_code: 400,
- message: expect.stringContaining(
- 'Pre-packaged rules cannot be installed until the signals index is created'
- ),
- });
- }
- });
-
test('returns 404 if siem client is unavailable', async () => {
const { securitySolution, ...contextWithoutSecuritySolution } = context;
const response = await server.inject(
@@ -190,20 +162,6 @@ describe.skip.each([
timelines_updated: 0,
});
});
-
- testif(
- 'catches errors if signals index does not exist when rule registry not enabled',
- async () => {
- context.core.elasticsearch.client.asCurrentUser.search.mockResolvedValue(
- elasticsearchClientMock.createErrorTransportRequestPromise(new Error('Test error'))
- );
- const request = addPrepackagedRulesRequest();
- const response = await server.inject(request, context);
-
- expect(response.status).toEqual(500);
- expect(response.body).toEqual({ message: 'Test error', status_code: 500 });
- }
- );
});
test('should install prepackaged timelines', async () => {
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.ts
index 054238cf6fa45d..149227084ace06 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.ts
@@ -25,6 +25,7 @@ import { transformValidateBulkError } from './validate';
import { transformBulkError, buildSiemResponse, createBulkErrorObject } from '../utils';
import { deleteRules } from '../../rules/delete_rules';
import { readRules } from '../../rules/read_rules';
+import { legacyMigrate } from '../../rules/utils';
type Config = RouteConfig;
type Handler = RequestHandler<
@@ -60,6 +61,7 @@ export const deleteRulesBulkRoute = (
}
const ruleStatusClient = context.securitySolution.getExecutionLogClient();
+ const savedObjectsClient = context.core.savedObjects.client;
const rules = await Promise.all(
request.body.map(async (payloadRule) => {
@@ -76,22 +78,27 @@ export const deleteRulesBulkRoute = (
try {
const rule = await readRules({ rulesClient, id, ruleId, isRuleRegistryEnabled });
- if (!rule) {
+ const migratedRule = await legacyMigrate({
+ rulesClient,
+ savedObjectsClient,
+ rule,
+ });
+ if (!migratedRule) {
return getIdBulkError({ id, ruleId });
}
const ruleStatus = await ruleStatusClient.getCurrentStatus({
- ruleId: rule.id,
+ ruleId: migratedRule.id,
spaceId: context.securitySolution.getSpaceId(),
});
await deleteRules({
- ruleId: rule.id,
+ ruleId: migratedRule.id,
rulesClient,
ruleStatusClient,
});
return transformValidateBulkError(
idOrRuleIdOrUnknown,
- rule,
+ migratedRule,
ruleStatus,
isRuleRegistryEnabled
);
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/delete_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/delete_rules_route.ts
index abcf0d07a33b68..3bb7778e5bc5ee 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/delete_rules_route.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/delete_rules_route.ts
@@ -19,6 +19,7 @@ import { getIdError, transform } from './utils';
import { buildSiemResponse } from '../utils';
import { readRules } from '../../rules/read_rules';
+import { legacyMigrate } from '../../rules/utils';
export const deleteRulesRoute = (
router: SecuritySolutionPluginRouter,
@@ -47,14 +48,20 @@ export const deleteRulesRoute = (
const { id, rule_id: ruleId } = request.query;
const rulesClient = context.alerting?.getRulesClient();
-
+ const savedObjectsClient = context.core.savedObjects.client;
if (!rulesClient) {
return siemResponse.error({ statusCode: 404 });
}
const ruleStatusClient = context.securitySolution.getExecutionLogClient();
const rule = await readRules({ isRuleRegistryEnabled, rulesClient, id, ruleId });
- if (!rule) {
+ const migratedRule = await legacyMigrate({
+ rulesClient,
+ savedObjectsClient,
+ rule,
+ });
+
+ if (!migratedRule) {
const error = getIdError({ id, ruleId });
return siemResponse.error({
body: error.message,
@@ -63,15 +70,16 @@ export const deleteRulesRoute = (
}
const currentStatus = await ruleStatusClient.getCurrentStatus({
- ruleId: rule.id,
+ ruleId: migratedRule.id,
spaceId: context.securitySolution.getSpaceId(),
});
+
await deleteRules({
- ruleId: rule.id,
+ ruleId: migratedRule.id,
rulesClient,
ruleStatusClient,
});
- const transformed = transform(rule, currentStatus, isRuleRegistryEnabled);
+ const transformed = transform(migratedRule, currentStatus, isRuleRegistryEnabled);
if (transformed == null) {
return siemResponse.error({ statusCode: 500, body: 'failed to transform alert' });
} else {
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/index.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/index.ts
index d60a190f94d19b..ac2f6495b8b46e 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/index.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/index.ts
@@ -24,5 +24,4 @@ export const createResultObject = (state: TState)
return result;
};
-export * from './expand_dotted';
export * from './get_list_client';
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.test.ts
index 79371aa6e68b6b..ecf625ceaee174 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.test.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.test.ts
@@ -12,18 +12,12 @@ import { RulesClientMock } from '../../../../../alerting/server/rules_client.moc
import { getMlRuleParams, getQueryRuleParams } from '../schemas/rule_schemas.mock';
// Failing with rule registry enabled
-describe.skip.each([
- ['Legacy', false],
- ['RAC', true],
-])('updateRules - %s', (_, isRuleRegistryEnabled) => {
+describe('updateRules', () => {
it('should call rulesClient.disable if the rule was enabled and enabled is false', async () => {
- const rulesOptionsMock = getUpdateRulesOptionsMock(isRuleRegistryEnabled);
+ const rulesOptionsMock = getUpdateRulesOptionsMock(true);
rulesOptionsMock.ruleUpdate.enabled = false;
- (rulesOptionsMock.rulesClient as unknown as RulesClientMock).resolve.mockResolvedValue(
- resolveAlertMock(isRuleRegistryEnabled, getQueryRuleParams())
- );
(rulesOptionsMock.rulesClient as unknown as RulesClientMock).update.mockResolvedValue(
- getAlertMock(isRuleRegistryEnabled, getQueryRuleParams())
+ getAlertMock(true, getQueryRuleParams())
);
await updateRules(rulesOptionsMock);
@@ -36,15 +30,18 @@ describe.skip.each([
});
it('should call rulesClient.enable if the rule was disabled and enabled is true', async () => {
- const rulesOptionsMock = getUpdateRulesOptionsMock(isRuleRegistryEnabled);
+ const baseRulesOptionsMock = getUpdateRulesOptionsMock(true);
+ const rulesOptionsMock = {
+ ...baseRulesOptionsMock,
+ existingRule: {
+ ...baseRulesOptionsMock.existingRule,
+ enabled: false,
+ },
+ };
rulesOptionsMock.ruleUpdate.enabled = true;
- (rulesOptionsMock.rulesClient as unknown as RulesClientMock).resolve.mockResolvedValue({
- ...resolveAlertMock(isRuleRegistryEnabled, getQueryRuleParams()),
- enabled: false,
- });
(rulesOptionsMock.rulesClient as unknown as RulesClientMock).update.mockResolvedValue(
- getAlertMock(isRuleRegistryEnabled, getQueryRuleParams())
+ getAlertMock(true, getQueryRuleParams())
);
await updateRules(rulesOptionsMock);
@@ -57,15 +54,15 @@ describe.skip.each([
});
it('calls the rulesClient with params', async () => {
- const rulesOptionsMock = getUpdateMlRulesOptionsMock(isRuleRegistryEnabled);
+ const rulesOptionsMock = getUpdateMlRulesOptionsMock(true);
rulesOptionsMock.ruleUpdate.enabled = true;
(rulesOptionsMock.rulesClient as unknown as RulesClientMock).update.mockResolvedValue(
- getAlertMock(isRuleRegistryEnabled, getMlRuleParams())
+ getAlertMock(true, getMlRuleParams())
);
(rulesOptionsMock.rulesClient as unknown as RulesClientMock).resolve.mockResolvedValue(
- resolveAlertMock(isRuleRegistryEnabled, getMlRuleParams())
+ resolveAlertMock(true, getMlRuleParams())
);
await updateRules(rulesOptionsMock);
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/utils.ts
index dee2006669f85d..73039697268e62 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/utils.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/utils.ts
@@ -316,7 +316,7 @@ export const legacyMigrate = async ({
}
/**
* On update / patch I'm going to take the actions as they are, better off taking rules client.find (siem.notification) result
- * and putting that into the actions array of the rule, then set the rules onThrottle property, notifyWhen and throttle from null -> actualy value (1hr etc..)
+ * and putting that into the actions array of the rule, then set the rules onThrottle property, notifyWhen and throttle from null -> actual value (1hr etc..)
* Then use the rules client to delete the siem.notification
* Then with the legacy Rule Actions saved object type, just delete it.
*/
@@ -325,6 +325,7 @@ export const legacyMigrate = async ({
const [siemNotification, legacyRuleActionsSO] = await Promise.all([
rulesClient.find({
options: {
+ filter: 'alert.attributes.alertTypeId:(siem.notifications)',
hasReference: {
type: 'alert',
id: rule.id,
diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/index.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/index.tsx
index 33eb87de7a0ce2..0abe40405c4ef3 100644
--- a/x-pack/plugins/timelines/public/components/t_grid/body/index.tsx
+++ b/x-pack/plugins/timelines/public/components/t_grid/body/index.tsx
@@ -672,7 +672,6 @@ export const BodyComponent = React.memo(
pageSize,
timelineId: id,
});
-
return {
...header,
actions: {
diff --git a/x-pack/plugins/timelines/public/components/t_grid/integrated/index.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/integrated/index.test.tsx
index e0c221c95e6a71..0dc8ff58d2ef1a 100644
--- a/x-pack/plugins/timelines/public/components/t_grid/integrated/index.test.tsx
+++ b/x-pack/plugins/timelines/public/components/t_grid/integrated/index.test.tsx
@@ -5,6 +5,7 @@
* 2.0.
*/
+import { euiDarkVars } from '@kbn/ui-shared-deps-src/theme';
import React from 'react';
import { render, screen } from '@testing-library/react';
import { TGridIntegrated, TGridIntegratedProps } from './index';
@@ -64,4 +65,17 @@ describe('integrated t_grid', () => {
);
expect(screen.queryByTestId(dataTestSubj)).not.toBeNull();
});
+
+ it(`prevents view selection from overlapping EuiDataGrid's 'Full screen' button`, () => {
+ render(
+
+
+
+ );
+
+ expect(screen.queryByTestId('updated-flex-group')).toHaveStyleRule(
+ `margin-right`,
+ euiDarkVars.paddingSizes.xl
+ );
+ });
});
diff --git a/x-pack/plugins/timelines/public/components/t_grid/integrated/index.tsx b/x-pack/plugins/timelines/public/components/t_grid/integrated/index.tsx
index 7cdee1748ef4b5..1abef58feeadea 100644
--- a/x-pack/plugins/timelines/public/components/t_grid/integrated/index.tsx
+++ b/x-pack/plugins/timelines/public/components/t_grid/integrated/index.tsx
@@ -327,7 +327,13 @@ const TGridIntegratedComponent: React.FC = ({
data-timeline-id={id}
data-test-subj={`events-container-loading-${loading}`}
>
-
+
diff --git a/x-pack/plugins/timelines/public/components/t_grid/styles.tsx b/x-pack/plugins/timelines/public/components/t_grid/styles.tsx
index ceb19837c434d7..7f4767e45fec7f 100644
--- a/x-pack/plugins/timelines/public/components/t_grid/styles.tsx
+++ b/x-pack/plugins/timelines/public/components/t_grid/styles.tsx
@@ -10,6 +10,7 @@ import { IS_TIMELINE_FIELD_DRAGGING_CLASS_NAME } from '@kbn/securitysolution-t-g
import { rgba } from 'polished';
import styled, { createGlobalStyle } from 'styled-components';
import type { TimelineEventsType } from '../../../common/types/timeline';
+import type { ViewSelection } from './event_rendered_view/selector';
import { ACTIONS_COLUMN_ARIA_COL_INDEX } from './helpers';
import { EVENTS_TABLE_ARIA_LABEL } from './translations';
@@ -466,7 +467,9 @@ export const FullWidthFlexGroup = styled(EuiFlexGroup)<{ $visible?: boolean }>`
display: ${({ $visible = true }) => ($visible ? 'flex' : 'none')};
`;
-export const UpdatedFlexGroup = styled(EuiFlexGroup)`
+export const UpdatedFlexGroup = styled(EuiFlexGroup)<{ $view?: ViewSelection }>`
+ ${({ $view, theme }) =>
+ $view === 'gridView' ? `margin-right: ${theme.eui.paddingSizes.xl};` : ''}
position: absolute;
z-index: ${({ theme }) => theme.eui.euiZLevel1};
right: 0px;
diff --git a/x-pack/plugins/upgrade_assistant/server/lib/reindexing/worker.ts b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/worker.ts
index 3491c92ef59533..579e4ddb213376 100644
--- a/x-pack/plugins/upgrade_assistant/server/lib/reindexing/worker.ts
+++ b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/worker.ts
@@ -49,7 +49,31 @@ export class ReindexWorker {
private readonly log: Logger;
private readonly security: SecurityPluginStart;
- constructor(
+ public static create(
+ client: SavedObjectsClientContract,
+ credentialStore: CredentialStore,
+ clusterClient: IClusterClient,
+ log: Logger,
+ licensing: LicensingPluginSetup,
+ security: SecurityPluginStart
+ ): ReindexWorker {
+ if (ReindexWorker.workerSingleton) {
+ log.debug(`More than one ReindexWorker cannot be created, returning existing worker.`);
+ } else {
+ ReindexWorker.workerSingleton = new ReindexWorker(
+ client,
+ credentialStore,
+ clusterClient,
+ log,
+ licensing,
+ security
+ );
+ }
+
+ return ReindexWorker.workerSingleton;
+ }
+
+ private constructor(
private client: SavedObjectsClientContract,
private credentialStore: CredentialStore,
private clusterClient: IClusterClient,
@@ -60,10 +84,6 @@ export class ReindexWorker {
this.log = log.get('reindex_worker');
this.security = security;
- if (ReindexWorker.workerSingleton) {
- throw new Error(`More than one ReindexWorker cannot be created.`);
- }
-
const callAsInternalUser = this.clusterClient.asInternalUser;
this.reindexService = reindexServiceFactory(
@@ -72,8 +92,6 @@ export class ReindexWorker {
log,
this.licensing
);
-
- ReindexWorker.workerSingleton = this;
}
/**
diff --git a/x-pack/plugins/upgrade_assistant/server/routes/ml_snapshots.ts b/x-pack/plugins/upgrade_assistant/server/routes/ml_snapshots.ts
index e618ef4b97ba82..fb65f6d41c43e8 100644
--- a/x-pack/plugins/upgrade_assistant/server/routes/ml_snapshots.ts
+++ b/x-pack/plugins/upgrade_assistant/server/routes/ml_snapshots.ts
@@ -78,10 +78,12 @@ const verifySnapshotUpgrade = async (
const isSuccessful = Boolean(
mlSnapshotDeprecations.find((snapshotDeprecation) => {
+ // This regex will match all the bracket pairs from the deprecation message, at the moment
+ // that should match 3 pairs: snapshotId, jobId and version in which the snapshot was made.
const regex = /(?<=\[).*?(?=\])/g;
const matches = snapshotDeprecation.message.match(regex);
- if (matches?.length === 2) {
+ if (matches?.length === 3) {
// If there is no matching snapshot, we assume the deprecation was resolved successfully
return matches[0] === snapshotId && matches[1] === jobId ? false : true;
}
@@ -130,7 +132,11 @@ const getModelSnapshotUpgradeStatus = async (
}
};
-export function registerMlSnapshotRoutes({ router, lib: { handleEsError } }: RouteDependencies) {
+export function registerMlSnapshotRoutes({
+ router,
+ log,
+ lib: { handleEsError },
+}: RouteDependencies) {
// Upgrade ML model snapshot
router.post(
{
@@ -256,7 +262,7 @@ export function registerMlSnapshotRoutes({ router, lib: { handleEsError } }: Rou
'xpack.upgradeAssistant.ml_snapshots.modelSnapshotUpgradeFailed',
{
defaultMessage:
- "The upgrade that was started for this model snapshot doesn't exist anymore.",
+ 'The upgrade process for this model snapshot failed. Check the Elasticsearch logs for more details.',
}
),
},
@@ -286,7 +292,7 @@ export function registerMlSnapshotRoutes({ router, lib: { handleEsError } }: Rou
body: {
message:
upgradeSnapshotError?.body?.error?.reason ||
- 'There was an error upgrading your snapshot. Check the Elasticsearch logs for more details.',
+ 'The upgrade process for this model snapshot stopped yet the snapshot is not upgraded. Check the Elasticsearch logs for more details.',
},
});
}
@@ -310,12 +316,15 @@ export function registerMlSnapshotRoutes({ router, lib: { handleEsError } }: Rou
});
}
+ log.error(
+ `Failed to determine status of the ML model upgrade, upgradeStatus is not defined and snapshot upgrade is not completed. snapshotId=${snapshotId} and jobId=${jobId}`
+ );
return response.customError({
statusCode: upgradeSnapshotError ? upgradeSnapshotError.statusCode! : 500,
body: {
message:
upgradeSnapshotError?.body?.error?.reason ||
- 'There was an error upgrading your snapshot. Check the Elasticsearch logs for more details.',
+ 'The upgrade process for this model snapshot completed yet the snapshot is not upgraded. Check the Elasticsearch logs for more details.',
},
});
}
diff --git a/x-pack/plugins/upgrade_assistant/server/routes/reindex_indices/create_reindex_worker.ts b/x-pack/plugins/upgrade_assistant/server/routes/reindex_indices/create_reindex_worker.ts
index 72d68fc132cb68..d20912e56fe662 100644
--- a/x-pack/plugins/upgrade_assistant/server/routes/reindex_indices/create_reindex_worker.ts
+++ b/x-pack/plugins/upgrade_assistant/server/routes/reindex_indices/create_reindex_worker.ts
@@ -34,5 +34,5 @@ export function createReindexWorker({
security,
}: CreateReindexWorker) {
const esClient = elasticsearchService.client;
- return new ReindexWorker(savedObjects, credentialStore, esClient, logger, licensing, security);
+ return ReindexWorker.create(savedObjects, credentialStore, esClient, logger, licensing, security);
}
diff --git a/x-pack/plugins/uptime/server/lib/synthetics_service/synthetics_service.ts b/x-pack/plugins/uptime/server/lib/synthetics_service/synthetics_service.ts
index d9aa0c664defaa..82a901192b0eeb 100644
--- a/x-pack/plugins/uptime/server/lib/synthetics_service/synthetics_service.ts
+++ b/x-pack/plugins/uptime/server/lib/synthetics_service/synthetics_service.ts
@@ -7,12 +7,7 @@
/* eslint-disable max-classes-per-file */
-import {
- CoreStart,
- KibanaRequest,
- Logger,
- SavedObjectsClient,
-} from '../../../../../../src/core/server';
+import { KibanaRequest, Logger } from '../../../../../../src/core/server';
import {
ConcreteTaskInstance,
TaskManagerSetupContract,
@@ -62,7 +57,7 @@ export class SyntheticsService {
this.esHosts = getEsHosts({ config: this.config, cloud: server.cloud });
}
- public init(coreStart: CoreStart) {
+ public init() {
// TODO: Figure out fake kibana requests to handle API keys on start up
// getAPIKeyForSyntheticsService({ server: this.server }).then((apiKey) => {
// if (apiKey) {
@@ -70,20 +65,11 @@ export class SyntheticsService {
// }
// });
- this.setupIndexTemplates(coreStart);
+ this.setupIndexTemplates();
}
- private setupIndexTemplates(coreStart: CoreStart) {
- const esClient = coreStart.elasticsearch.client.asInternalUser;
- const savedObjectsClient = new SavedObjectsClient(
- coreStart.savedObjects.createInternalRepository()
- );
-
- installSyntheticsIndexTemplates({
- esClient,
- server: this.server,
- savedObjectsClient,
- }).then(
+ private setupIndexTemplates() {
+ installSyntheticsIndexTemplates(this.server).then(
(result) => {
if (result.name === 'synthetics' && result.install_status === 'installed') {
this.logger.info('Installed synthetics index templates');
diff --git a/x-pack/plugins/uptime/server/plugin.ts b/x-pack/plugins/uptime/server/plugin.ts
index eedb9385d44d8c..692607041ea807 100644
--- a/x-pack/plugins/uptime/server/plugin.ts
+++ b/x-pack/plugins/uptime/server/plugin.ts
@@ -112,7 +112,7 @@ export class Plugin implements PluginType {
}
if (this.server?.config?.unsafe?.service.enabled) {
- this.syntheticService?.init(coreStart);
+ this.syntheticService?.init();
this.syntheticService?.scheduleSyncTask(plugins.taskManager);
if (this.server && this.syntheticService) {
this.server.syntheticsService = this.syntheticService;
diff --git a/x-pack/plugins/uptime/server/rest_api/synthetics_service/install_index_templates.ts b/x-pack/plugins/uptime/server/rest_api/synthetics_service/install_index_templates.ts
index 185e526d148feb..5c70729fc76202 100644
--- a/x-pack/plugins/uptime/server/rest_api/synthetics_service/install_index_templates.ts
+++ b/x-pack/plugins/uptime/server/rest_api/synthetics_service/install_index_templates.ts
@@ -4,7 +4,7 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
-import { ElasticsearchClient, SavedObjectsClientContract } from 'kibana/server';
+
import { UMRestApiRouteFactory } from '../types';
import { API_URLS } from '../../../common/constants';
import { UptimeServerSetup } from '../../lib/adapters';
@@ -13,31 +13,23 @@ export const installIndexTemplatesRoute: UMRestApiRouteFactory = () => ({
method: 'GET',
path: API_URLS.INDEX_TEMPLATES,
validate: {},
- handler: async ({ server, request, savedObjectsClient, uptimeEsClient }): Promise => {
- return installSyntheticsIndexTemplates({
- server,
- savedObjectsClient,
- esClient: uptimeEsClient.baseESClient,
- });
+ handler: async ({ server }): Promise => {
+ return installSyntheticsIndexTemplates(server);
},
});
-export async function installSyntheticsIndexTemplates({
- esClient,
- server,
- savedObjectsClient,
-}: {
- server: UptimeServerSetup;
- esClient: ElasticsearchClient;
- savedObjectsClient: SavedObjectsClientContract;
-}) {
+export async function installSyntheticsIndexTemplates(server: UptimeServerSetup) {
// no need to add error handling here since fleetSetupCompleted is already wrapped in try/catch and will log
// warning if setup fails to complete
await server.fleet.fleetSetupCompleted();
- return await server.fleet.packageService.ensureInstalledPackage({
- esClient,
- savedObjectsClient,
+ const installation = await server.fleet.packageService.asInternalUser.ensureInstalledPackage({
pkgName: 'synthetics',
});
+
+ if (!installation) {
+ return Promise.reject('No package installation found.');
+ }
+
+ return installation;
}
diff --git a/x-pack/test/accessibility/apps/upgrade_assistant.ts b/x-pack/test/accessibility/apps/upgrade_assistant.ts
index 49f2bd33910837..850cb5de52bbad 100644
--- a/x-pack/test/accessibility/apps/upgrade_assistant.ts
+++ b/x-pack/test/accessibility/apps/upgrade_assistant.ts
@@ -97,11 +97,22 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
});
it('with logs collection disabled', async () => {
+ const loggingEnabled = await PageObjects.upgradeAssistant.isDeprecationLoggingEnabled();
+ if (loggingEnabled) {
+ await PageObjects.upgradeAssistant.clickDeprecationLoggingToggle();
+ }
+
+ await retry.waitFor('Deprecation logging to be disabled', async () => {
+ return !(await PageObjects.upgradeAssistant.isDeprecationLoggingEnabled());
+ });
await a11y.testAppSnapshot();
});
it('with logs collection enabled', async () => {
- await PageObjects.upgradeAssistant.clickDeprecationLoggingToggle();
+ const loggingEnabled = await PageObjects.upgradeAssistant.isDeprecationLoggingEnabled();
+ if (!loggingEnabled) {
+ await PageObjects.upgradeAssistant.clickDeprecationLoggingToggle();
+ }
await retry.waitFor('UA external links title to be present', async () => {
return testSubjects.isDisplayed('externalLinksTitle');
diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/delete_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/delete_rules.ts
index e5f828d0f862d4..19076506998a97 100644
--- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/delete_rules.ts
+++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/delete_rules.ts
@@ -7,9 +7,11 @@
import expect from '@kbn/expect';
+import { BASE_ALERTING_API_PATH } from '../../../../plugins/alerting/common';
import { DETECTION_ENGINE_RULES_URL } from '../../../../plugins/security_solution/common/constants';
import { FtrProviderContext } from '../../common/ftr_provider_context';
import {
+ createLegacyRuleAction,
createRule,
createSignalsIndex,
deleteAllAlerts,
@@ -18,6 +20,7 @@ import {
getSimpleRuleOutput,
getSimpleRuleOutputWithoutRuleId,
getSimpleRuleWithoutRuleId,
+ getWebHookAction,
removeServerGeneratedProperties,
removeServerGeneratedPropertiesIncludingRuleId,
} from '../../utils';
@@ -100,6 +103,119 @@ export default ({ getService }: FtrProviderContext): void => {
status_code: 404,
});
});
+
+ /**
+ * @deprecated Once the legacy notification system is removed, remove this test too.
+ */
+ it('should have exactly 1 legacy action before a delete within alerting', async () => {
+ // create an action
+ const { body: hookAction } = await supertest
+ .post('/api/actions/action')
+ .set('kbn-xsrf', 'true')
+ .send(getWebHookAction())
+ .expect(200);
+
+ // create a rule without actions
+ const createRuleBody = await createRule(supertest, log, getSimpleRule('rule-1'));
+
+ // Add a legacy rule action to the body of the rule
+ await createLegacyRuleAction(supertest, createRuleBody.id, hookAction.id);
+
+ // Test to ensure that we have exactly 1 legacy action by querying the Alerting client REST API directly
+ // See: https://www.elastic.co/guide/en/kibana/current/find-rules-api.html
+ // Note: We specifically query for both the filter of type "siem.notifications" and the "has_reference" to keep it very specific
+ const { body: alertFind } = await supertest
+ .get(`${BASE_ALERTING_API_PATH}/rules/_find`)
+ .query({
+ page: 1,
+ per_page: 10,
+ filter: 'alert.attributes.alertTypeId:(siem.notifications)',
+ has_reference: JSON.stringify({ id: createRuleBody.id, type: 'alert' }),
+ })
+ .set('kbn-xsrf', 'true')
+ .send()
+ .expect(200);
+
+ // Expect that we have exactly 1 legacy rule before the deletion
+ expect(alertFind.total).to.eql(1);
+ });
+
+ /**
+ * @deprecated Once the legacy notification system is removed, remove this test too.
+ */
+ it('should return the legacy action in the response body when it deletes a rule that has one', async () => {
+ // create an action
+ const { body: hookAction } = await supertest
+ .post('/api/actions/action')
+ .set('kbn-xsrf', 'true')
+ .send(getWebHookAction())
+ .expect(200);
+
+ // create a rule without actions
+ const createRuleBody = await createRule(supertest, log, getSimpleRule('rule-1'));
+
+ // Add a legacy rule action to the body of the rule
+ await createLegacyRuleAction(supertest, createRuleBody.id, hookAction.id);
+
+ // delete the rule with the legacy action
+ const { body } = await supertest
+ .delete(`${DETECTION_ENGINE_RULES_URL}?id=${createRuleBody.id}`)
+ .set('kbn-xsrf', 'true')
+ .expect(200);
+
+ // ensure the actions contains the response
+ expect(body.actions).to.eql([
+ {
+ id: hookAction.id,
+ action_type_id: hookAction.actionTypeId,
+ group: 'default',
+ params: {
+ message:
+ 'Hourly\nRule {{context.rule.name}} generated {{state.signals_count}} alerts',
+ },
+ },
+ ]);
+ });
+
+ /**
+ * @deprecated Once the legacy notification system is removed, remove this test too.
+ */
+ it('should delete a legacy action when it deletes a rule that has one', async () => {
+ // create an action
+ const { body: hookAction } = await supertest
+ .post('/api/actions/action')
+ .set('kbn-xsrf', 'true')
+ .send(getWebHookAction())
+ .expect(200);
+
+ // create a rule without actions
+ const createRuleBody = await createRule(supertest, log, getSimpleRule('rule-1'));
+
+ // Add a legacy rule action to the body of the rule
+ await createLegacyRuleAction(supertest, createRuleBody.id, hookAction.id);
+
+ await supertest
+ .delete(`${DETECTION_ENGINE_RULES_URL}?id=${createRuleBody.id}`)
+ .set('kbn-xsrf', 'true')
+ .expect(200);
+
+ // Test to ensure that we have exactly 0 legacy actions by querying the Alerting client REST API directly
+ // See: https://www.elastic.co/guide/en/kibana/current/find-rules-api.html
+ // Note: We specifically query for both the filter of type "siem.notifications" and the "has_reference" to keep it very specific
+ const { body: bodyAfterDelete } = await supertest
+ .get(`${BASE_ALERTING_API_PATH}/rules/_find`)
+ .query({
+ page: 1,
+ per_page: 10,
+ filter: 'alert.attributes.alertTypeId:(siem.notifications)',
+ has_reference: JSON.stringify({ id: createRuleBody.id, type: 'alert' }),
+ })
+ .set('kbn-xsrf', 'true')
+ .send();
+
+ // Expect that we have exactly 0 legacy rules after the deletion
+ expect(bodyAfterDelete.total).to.eql(0);
+ });
});
});
};
diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/delete_rules_bulk.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/delete_rules_bulk.ts
index b7517697ad2a94..69be1f2eb0affa 100644
--- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/delete_rules_bulk.ts
+++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/delete_rules_bulk.ts
@@ -7,9 +7,11 @@
import expect from '@kbn/expect';
+import { BASE_ALERTING_API_PATH } from '../../../../plugins/alerting/common';
import { DETECTION_ENGINE_RULES_URL } from '../../../../plugins/security_solution/common/constants';
import { FtrProviderContext } from '../../common/ftr_provider_context';
import {
+ createLegacyRuleAction,
createRule,
createSignalsIndex,
deleteAllAlerts,
@@ -18,6 +20,7 @@ import {
getSimpleRuleOutput,
getSimpleRuleOutputWithoutRuleId,
getSimpleRuleWithoutRuleId,
+ getWebHookAction,
removeServerGeneratedProperties,
removeServerGeneratedPropertiesIncludingRuleId,
} from '../../utils';
@@ -249,6 +252,148 @@ export default ({ getService }: FtrProviderContext): void => {
},
]);
});
+
+ /**
+ * @deprecated Once the legacy notification system is removed, remove this test too.
+ */
+ it('should return the legacy action in the response body when it deletes a rule that has one', async () => {
+ // create an action
+ const { body: hookAction } = await supertest
+ .post('/api/actions/action')
+ .set('kbn-xsrf', 'true')
+ .send(getWebHookAction())
+ .expect(200);
+
+ // create a rule without actions
+ const createRuleBody = await createRule(supertest, log, getSimpleRule('rule-1'));
+
+ // Add a legacy rule action to the body of the rule
+ await createLegacyRuleAction(supertest, createRuleBody.id, hookAction.id);
+
+ // delete the rule with the legacy action
+ const { body } = await supertest
+ .delete(`${DETECTION_ENGINE_RULES_URL}/_bulk_delete`)
+ .send([{ id: createRuleBody.id }])
+ .set('kbn-xsrf', 'true')
+ .expect(200);
+
+ // ensure we only get one body back
+ expect(body.length).to.eql(1);
+
+ // ensure that its actions equal what we expect
+ expect(body[0].actions).to.eql([
+ {
+ id: hookAction.id,
+ action_type_id: hookAction.actionTypeId,
+ group: 'default',
+ params: {
+ message:
+ 'Hourly\nRule {{context.rule.name}} generated {{state.signals_count}} alerts',
+ },
+ },
+ ]);
+ });
+
+ /**
+ * @deprecated Once the legacy notification system is removed, remove this test too.
+ */
+ it('should return 2 legacy actions in the response body when it deletes 2 rules', async () => {
+ // create two different actions
+ const { body: hookAction1 } = await supertest
+ .post('/api/actions/action')
+ .set('kbn-xsrf', 'true')
+ .send(getWebHookAction())
+ .expect(200);
+ const { body: hookAction2 } = await supertest
+ .post('/api/actions/action')
+ .set('kbn-xsrf', 'true')
+ .send(getWebHookAction())
+ .expect(200);
+
+ // create 2 rules without actions
+ const createRuleBody1 = await createRule(supertest, log, getSimpleRule('rule-1'));
+ const createRuleBody2 = await createRule(supertest, log, getSimpleRule('rule-2'));
+
+ // Add a legacy rule action to the body of the 2 rules
+ await createLegacyRuleAction(supertest, createRuleBody1.id, hookAction1.id);
+ await createLegacyRuleAction(supertest, createRuleBody2.id, hookAction2.id);
+
+ // delete 2 rules where both have legacy actions
+ const { body } = await supertest
+ .delete(`${DETECTION_ENGINE_RULES_URL}/_bulk_delete`)
+ .send([{ id: createRuleBody1.id }, { id: createRuleBody2.id }])
+ .set('kbn-xsrf', 'true')
+ .expect(200);
+
+ // ensure we only get two bodies back
+ expect(body.length).to.eql(2);
+
+ // ensure that its actions equal what we expect for both responses
+ expect(body[0].actions).to.eql([
+ {
+ id: hookAction1.id,
+ action_type_id: hookAction1.actionTypeId,
+ group: 'default',
+ params: {
+ message:
+ 'Hourly\nRule {{context.rule.name}} generated {{state.signals_count}} alerts',
+ },
+ },
+ ]);
+ expect(body[1].actions).to.eql([
+ {
+ id: hookAction2.id,
+ action_type_id: hookAction2.actionTypeId,
+ group: 'default',
+ params: {
+ message:
+ 'Hourly\nRule {{context.rule.name}} generated {{state.signals_count}} alerts',
+ },
+ },
+ ]);
+ });
+
+ /**
+ * @deprecated Once the legacy notification system is removed, remove this test too.
+ */
+ it('should delete a legacy action when it deletes a rule that has one', async () => {
+ // create an action
+ const { body: hookAction } = await supertest
+ .post('/api/actions/action')
+ .set('kbn-xsrf', 'true')
+ .send(getWebHookAction())
+ .expect(200);
+
+ // create a rule without actions
+ const createRuleBody = await createRule(supertest, log, getSimpleRule('rule-1'));
+
+ // Add a legacy rule action to the body of the rule
+ await createLegacyRuleAction(supertest, createRuleBody.id, hookAction.id);
+
+ // bulk delete the rule
+ await supertest
+ .delete(`${DETECTION_ENGINE_RULES_URL}/_bulk_delete`)
+ .send([{ id: createRuleBody.id }])
+ .set('kbn-xsrf', 'true')
+ .expect(200);
+
+ // Test to ensure that we have exactly 0 legacy actions by querying the Alerting client REST API directly
+ // See: https://www.elastic.co/guide/en/kibana/current/find-rules-api.html
+ // Note: We specifically query for both the filter of type "siem.notifications" and the "has_reference" to keep it very specific
+ const { body: bodyAfterDelete } = await supertest
+ .get(`${BASE_ALERTING_API_PATH}/rules/_find`)
+ .query({
+ page: 1,
+ per_page: 10,
+ filter: 'alert.attributes.alertTypeId:(siem.notifications)',
+ has_reference: JSON.stringify({ id: createRuleBody.id, type: 'alert' }),
+ })
+ .set('kbn-xsrf', 'true')
+ .send();
+
+ // Expect that we have exactly 0 legacy rules after the deletion
+ expect(bodyAfterDelete.total).to.eql(0);
+ });
});
});
};
diff --git a/x-pack/test/detection_engine_api_integration/utils.ts b/x-pack/test/detection_engine_api_integration/utils.ts
index 2ebaed7defe674..2f9fba7430d593 100644
--- a/x-pack/test/detection_engine_api_integration/utils.ts
+++ b/x-pack/test/detection_engine_api_integration/utils.ts
@@ -604,6 +604,7 @@ export const createLegacyRuleAction = async (
},
],
});
+
/**
* Deletes the signals index for use inside of afterEach blocks of tests
* @param supertest The supertest client library
diff --git a/x-pack/test/functional/apps/ml/data_frame_analytics/regression_creation.ts b/x-pack/test/functional/apps/ml/data_frame_analytics/regression_creation.ts
index 7a84c41aa4a661..1d5c6d1bf84a36 100644
--- a/x-pack/test/functional/apps/ml/data_frame_analytics/regression_creation.ts
+++ b/x-pack/test/functional/apps/ml/data_frame_analytics/regression_creation.ts
@@ -13,7 +13,8 @@ export default function ({ getService }: FtrProviderContext) {
const ml = getService('ml');
const editedDescription = 'Edited description';
- describe('regression creation', function () {
+ // FLAKY: https://github.com/elastic/kibana/issues/122927
+ describe.skip('regression creation', function () {
before(async () => {
await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/egs_regression');
await ml.testResources.createIndexPatternIfNeeded('ft_egs_regression', '@timestamp');
diff --git a/x-pack/test/functional/apps/reporting_management/report_listing.ts b/x-pack/test/functional/apps/reporting_management/report_listing.ts
index 9cb00bb78d07ee..4e5aaaeba538fe 100644
--- a/x-pack/test/functional/apps/reporting_management/report_listing.ts
+++ b/x-pack/test/functional/apps/reporting_management/report_listing.ts
@@ -18,8 +18,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
const testSubjects = getService('testSubjects');
const esArchiver = getService('esArchiver');
- // FLAKY: https://github.com/elastic/kibana/issues/75044
- describe.skip('Listing of Reports', function () {
+ describe('Listing of Reports', function () {
before(async () => {
await security.testUser.setRoles([
'kibana_admin', // to access stack management
@@ -47,8 +46,9 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
it('Confirm single report deletion works', async () => {
log.debug('Checking for reports.');
await retry.try(async () => {
- await testSubjects.click('checkboxSelectRow-krb7arhe164k0763b50bjm29');
+ await testSubjects.click('checkboxSelectRow-krazcyw4156m0763b503j7f9');
});
+
const deleteButton = await testSubjects.find('deleteReportButton');
await retry.waitFor('delete button to become enabled', async () => {
return await deleteButton.isEnabled();
@@ -57,7 +57,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
await testSubjects.exists('confirmModalBodyText');
await testSubjects.click('confirmModalConfirmButton');
await retry.try(async () => {
- await testSubjects.waitForDeleted('checkboxSelectRow-krb7arhe164k0763b50bjm29');
+ await testSubjects.waitForDeleted('checkboxSelectRow-krazcyw4156m0763b503j7f9');
});
});
@@ -66,13 +66,10 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
const previousButton = await testSubjects.find('pagination-button-previous');
expect(await previousButton.getAttribute('disabled')).to.be('true');
- await testSubjects.find('checkboxSelectRow-krb7arhe164k0763b50bjm29'); // find first row of page 1
+ await testSubjects.find('checkboxSelectRow-krazcyw4156m0763b503j7f9'); // find first row of page 1
await testSubjects.click('pagination-button-1'); // click page 2
- await testSubjects.find('checkboxSelectRow-kraz0qle154g0763b569zz83'); // wait for first row of page 2
-
- await testSubjects.click('pagination-button-2'); // click page 3
- await testSubjects.find('checkboxSelectRow-k9a9p1840gpe1457b1ghfxw5'); // wait for first row of page 3
+ await testSubjects.find('checkboxSelectRow-k9a9xj3i0gpe1457b16qaduc'); // wait for first row of page 2
// previous CAN be clicked
expect(await previousButton.getAttribute('disabled')).to.be(null);
@@ -82,12 +79,6 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
const list = await pageObjects.reporting.getManagementList();
expectSnapshot(list).toMatchInline(`
Array [
- Object {
- "actions": "",
- "createdAt": "2021-07-19 @ 10:29 PM",
- "report": "Automated report",
- "status": "Done, warnings detected",
- },
Object {
"actions": "",
"createdAt": "2021-07-19 @ 06:47 PM",
@@ -142,6 +133,12 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
"report": "[Flights] Global Flight Dashboard",
"status": "Done",
},
+ Object {
+ "actions": "",
+ "createdAt": "2021-07-19 @ 02:41 PM",
+ "report": "[Flights] Global Flight Dashboard",
+ "status": "Failed",
+ },
]
`);
});
diff --git a/x-pack/test/functional/page_objects/upgrade_assistant_page.ts b/x-pack/test/functional/page_objects/upgrade_assistant_page.ts
index f795a5fd441cd2..f59cf660139b9e 100644
--- a/x-pack/test/functional/page_objects/upgrade_assistant_page.ts
+++ b/x-pack/test/functional/page_objects/upgrade_assistant_page.ts
@@ -56,6 +56,10 @@ export class UpgradeAssistantPageObject extends FtrService {
});
}
+ async isDeprecationLoggingEnabled(): Promise {
+ return await this.testSubjects.exists('externalLinksTitle');
+ }
+
async clickResetLastCheckpointButton() {
return await this.retry.try(async () => {
await this.testSubjects.click('resetLastStoredDate');
diff --git a/x-pack/test/saved_object_api_integration/common/fixtures/es_archiver/saved_objects/spaces/data.json b/x-pack/test/saved_object_api_integration/common/fixtures/es_archiver/saved_objects/spaces/data.json
index e7d2c630fc130d..671072999d90a6 100644
--- a/x-pack/test/saved_object_api_integration/common/fixtures/es_archiver/saved_objects/spaces/data.json
+++ b/x-pack/test/saved_object_api_integration/common/fixtures/es_archiver/saved_objects/spaces/data.json
@@ -710,3 +710,60 @@
"type": "doc"
}
}
+
+{
+ "type": "doc",
+ "value": {
+ "id": "index-pattern:inbound-reference-origin-match-1-newId",
+ "index": ".kibana",
+ "source": {
+ "originId": "inbound-reference-origin-match-1",
+ "index-pattern": {
+ "title": "This is used to test if an imported object with a reference to this originId will be remapped properly"
+ },
+ "namespaces": ["*"],
+ "type": "index-pattern",
+ "migrationVersion": { "index-pattern": "8.0.0" },
+ "updated_at": "2017-09-21T18:49:16.270Z"
+ },
+ "type": "doc"
+ }
+}
+
+{
+ "type": "doc",
+ "value": {
+ "id": "index-pattern:inbound-reference-origin-match-2a",
+ "index": ".kibana",
+ "source": {
+ "originId": "inbound-reference-origin-match-2",
+ "index-pattern": {
+ "title": "This is used to test if an imported object with a reference to this originId will *not* be remapped"
+ },
+ "namespaces": ["*"],
+ "type": "index-pattern",
+ "migrationVersion": { "index-pattern": "8.0.0" },
+ "updated_at": "2017-09-21T18:49:16.270Z"
+ },
+ "type": "doc"
+ }
+}
+
+{
+ "type": "doc",
+ "value": {
+ "id": "index-pattern:inbound-reference-origin-match-2b",
+ "index": ".kibana",
+ "source": {
+ "originId": "inbound-reference-origin-match-2",
+ "index-pattern": {
+ "title": "This is used to test if an imported object with a reference to this originId will *not* be remapped"
+ },
+ "namespaces": ["*"],
+ "type": "index-pattern",
+ "migrationVersion": { "index-pattern": "8.0.0" },
+ "updated_at": "2017-09-21T18:49:16.270Z"
+ },
+ "type": "doc"
+ }
+}
diff --git a/x-pack/test/saved_object_api_integration/common/suites/import.ts b/x-pack/test/saved_object_api_integration/common/suites/import.ts
index 04e0f3c41ed872..58b323e86d607b 100644
--- a/x-pack/test/saved_object_api_integration/common/suites/import.ts
+++ b/x-pack/test/saved_object_api_integration/common/suites/import.ts
@@ -8,23 +8,34 @@
import expect from '@kbn/expect';
import { SuperTest } from 'supertest';
import type { Client } from '@elastic/elasticsearch';
+import type { SavedObjectReference } from 'src/core/server';
import { SAVED_OBJECT_TEST_CASES as CASES } from '../lib/saved_object_test_cases';
import { SPACES } from '../lib/spaces';
import { expectResponses, getUrlPrefix, getTestTitle } from '../lib/saved_object_test_utils';
import { ExpectResponseBody, TestCase, TestDefinition, TestSuite } from '../lib/types';
export interface ImportTestDefinition extends TestDefinition {
- request: Array<{ type: string; id: string; originId?: string }>;
+ request: Array<{
+ type: string;
+ id: string;
+ originId?: string;
+ references?: SavedObjectReference[];
+ }>;
overwrite: boolean;
createNewCopies: boolean;
}
export type ImportTestSuite = TestSuite;
-export interface ImportTestCase extends TestCase {
+export type FailureType =
+ | 'unsupported_type'
+ | 'conflict'
+ | 'ambiguous_conflict'
+ | 'missing_references';
+export interface ImportTestCase extends Omit {
originId?: string;
expectedNewId?: string;
+ references?: SavedObjectReference[];
successParam?: string;
- failure?: 400 | 409; // only used for permitted response case
- fail409Param?: string;
+ failureType?: FailureType; // only used for permitted response case
}
const NEW_ATTRIBUTE_KEY = 'title'; // all type mappings include this attribute, for simplicity's sake
@@ -37,33 +48,60 @@ const NEW_ATTRIBUTE_VAL = `New attribute value ${Date.now()}`;
// * id: conflict_3
// * id: conflict_4a, originId: conflict_4
// using the seven conflict test case objects below, we can exercise various permutations of exact/inexact/ambiguous conflict scenarios
-const CID = 'conflict_';
+const { HIDDEN, ...REMAINING_CASES } = CASES;
export const TEST_CASES: Record = Object.freeze({
- ...CASES,
- CONFLICT_1_OBJ: Object.freeze({ type: 'sharedtype', id: `${CID}1` }),
- CONFLICT_1A_OBJ: Object.freeze({ type: 'sharedtype', id: `${CID}1a`, originId: `${CID}1` }),
- CONFLICT_1B_OBJ: Object.freeze({ type: 'sharedtype', id: `${CID}1b`, originId: `${CID}1` }),
- CONFLICT_2C_OBJ: Object.freeze({ type: 'sharedtype', id: `${CID}2c`, originId: `${CID}2` }),
- CONFLICT_2D_OBJ: Object.freeze({ type: 'sharedtype', id: `${CID}2d`, originId: `${CID}2` }),
+ ...REMAINING_CASES,
+ CONFLICT_1_OBJ: Object.freeze({ type: 'sharedtype', id: `conflict_1` }),
+ CONFLICT_1A_OBJ: Object.freeze({ type: 'sharedtype', id: `conflict_1a`, originId: `conflict_1` }),
+ CONFLICT_1B_OBJ: Object.freeze({ type: 'sharedtype', id: `conflict_1b`, originId: `conflict_1` }),
+ CONFLICT_2A_OBJ: Object.freeze({ type: 'sharedtype', id: `conflict_2a`, originId: `conflict_2` }),
+ CONFLICT_2C_OBJ: Object.freeze({ type: 'sharedtype', id: `conflict_2c`, originId: `conflict_2` }),
+ CONFLICT_2D_OBJ: Object.freeze({ type: 'sharedtype', id: `conflict_2d`, originId: `conflict_2` }),
CONFLICT_3A_OBJ: Object.freeze({
type: 'sharedtype',
- id: `${CID}3a`,
- originId: `${CID}3`,
- expectedNewId: `${CID}3`,
+ id: `conflict_3a`,
+ originId: `conflict_3`,
+ expectedNewId: `conflict_3`,
+ }),
+ CONFLICT_4_OBJ: Object.freeze({
+ type: 'sharedtype',
+ id: `conflict_4`,
+ expectedNewId: `conflict_4a`,
}),
- CONFLICT_4_OBJ: Object.freeze({ type: 'sharedtype', id: `${CID}4`, expectedNewId: `${CID}4a` }),
NEW_SINGLE_NAMESPACE_OBJ: Object.freeze({ type: 'isolatedtype', id: 'new-isolatedtype-id' }),
NEW_MULTI_NAMESPACE_OBJ: Object.freeze({ type: 'sharedtype', id: 'new-sharedtype-id' }),
NEW_NAMESPACE_AGNOSTIC_OBJ: Object.freeze({ type: 'globaltype', id: 'new-globaltype-id' }),
});
+export const SPECIAL_TEST_CASES: Record = Object.freeze({
+ HIDDEN,
+ OUTBOUND_REFERENCE_ORIGIN_MATCH_1_OBJ: Object.freeze({
+ // This object does not already exist, but it has a reference to the originId of an index pattern that does exist.
+ // We use index patterns because they are one of the few reference types that are validated, so the import will fail if the reference
+ // is broken.
+ // This import is designed to succeed because there is exactly one origin match for its reference, and that reference will be changed to
+ // match the index pattern's new ID.
+ type: 'sharedtype',
+ id: 'outbound-reference-origin-match-1',
+ references: [{ name: '1', type: 'index-pattern', id: 'inbound-reference-origin-match-1' }],
+ }),
+ OUTBOUND_REFERENCE_ORIGIN_MATCH_2_OBJ: Object.freeze({
+ // This object does not already exist, but it has a reference to the originId of two index patterns that do exist.
+ // This import is designed to fail because there are two origin matches for its reference, and we can't currently handle ambiguous
+ // destinations for reference origin matches.
+ type: 'sharedtype',
+ id: 'outbound-reference-origin-match-2',
+ references: [{ name: '1', type: 'index-pattern', id: 'inbound-reference-origin-match-2' }],
+ }),
+});
/**
* Test cases have additional properties that we don't want to send in HTTP Requests
*/
-const createRequest = ({ type, id, originId }: ImportTestCase) => ({
+const createRequest = ({ type, id, originId, references }: ImportTestCase) => ({
type,
id,
...(originId && { originId }),
+ ...(references && { references }),
});
const getConflictDest = (id: string) => ({
@@ -72,8 +110,20 @@ const getConflictDest = (id: string) => ({
updatedAt: '2017-09-21T18:59:16.270Z',
});
+export const importTestCaseFailures = {
+ failUnsupportedType: (condition?: boolean): { failureType?: 'unsupported_type' } =>
+ condition !== false ? { failureType: 'unsupported_type' } : {},
+ failConflict: (condition?: boolean): { failureType?: 'conflict' } =>
+ condition !== false ? { failureType: 'conflict' } : {},
+ failAmbiguousConflict: (condition?: boolean): { failureType?: 'ambiguous_conflict' } =>
+ condition !== false ? { failureType: 'ambiguous_conflict' } : {},
+ failMissingReferences: (condition?: boolean): { failureType?: 'missing_references' } =>
+ condition !== false ? { failureType: 'missing_references' } : {},
+};
+
export function importTestSuiteFactory(es: Client, esArchiver: any, supertest: SuperTest) {
- const expectSavedObjectForbidden = expectResponses.forbiddenTypes('bulk_create');
+ const expectSavedObjectForbidden = (action: string, typeOrTypes: string | string[]) =>
+ expectResponses.forbiddenTypes(action)(typeOrTypes);
const expectResponseBody =
(
testCases: ImportTestCase | ImportTestCase[],
@@ -87,12 +137,12 @@ export function importTestSuiteFactory(es: Client, esArchiver: any, supertest: S
const testCaseArray = Array.isArray(testCases) ? testCases : [testCases];
if (statusCode === 403) {
const types = testCaseArray.map((x) => x.type);
- await expectSavedObjectForbidden(types)(response);
+ await expectSavedObjectForbidden('bulk_create', types)(response);
} else {
// permitted
const { success, successCount, successResults, errors } = response.body;
- const expectedSuccesses = testCaseArray.filter((x) => !x.failure);
- const expectedFailures = testCaseArray.filter((x) => x.failure);
+ const expectedSuccesses = testCaseArray.filter((x) => !x.failureType);
+ const expectedFailures = testCaseArray.filter((x) => x.failureType);
expect(success).to.eql(expectedFailures.length === 0);
expect(successCount).to.eql(expectedSuccesses.length);
if (expectedFailures.length) {
@@ -147,30 +197,37 @@ export function importTestSuiteFactory(es: Client, esArchiver: any, supertest: S
}
}
for (let i = 0; i < expectedFailures.length; i++) {
- const { type, id, failure, fail409Param, expectedNewId } = expectedFailures[i];
+ const { type, id, failureType, expectedNewId } = expectedFailures[i];
// we don't know the order of the returned errors; search for each one
const object = (errors as Array>).find(
(x) => x.type === type && x.id === id
);
expect(object).not.to.be(undefined);
- if (failure === 400) {
- expect(object!.error).to.eql({ type: 'unsupported_type' });
- } else {
- // 409
- let error: Record = {
- type: 'conflict',
- ...(expectedNewId && { destinationId: expectedNewId }),
- };
- if (fail409Param === 'ambiguous_conflict_2c') {
- // "ambiguous destination" conflict
- error = {
- type: 'ambiguous_conflict',
- // response destinations should be sorted by updatedAt in descending order, then ID in ascending order
- destinations: [getConflictDest(`${CID}2a`), getConflictDest(`${CID}2b`)],
- };
- }
- expect(object!.error).to.eql(error);
+ const expectedError: Record = { type: failureType };
+ switch (failureType!) {
+ case 'unsupported_type':
+ break;
+ case 'conflict':
+ if (expectedNewId) {
+ expectedError.destinationId = expectedNewId;
+ }
+ break;
+ case 'ambiguous_conflict':
+ // We only have one test case for ambiguous conflicts, so these destination IDs are hardcoded below for simplicity.
+ // Response destinations should be sorted by updatedAt in descending order, then ID in ascending order.
+ expectedError.destinations = [
+ getConflictDest(`conflict_2a`),
+ getConflictDest(`conflict_2b`),
+ ];
+ break;
+ case 'missing_references':
+ // We only have one test case for missing references, so this reference is hardcoded below for simplicity.
+ expectedError.references = [
+ { type: 'index-pattern', id: 'inbound-reference-origin-match-2' },
+ ];
+ break;
}
+ expect(object!.error).to.eql(expectedError);
}
}
};
diff --git a/x-pack/test/saved_object_api_integration/common/suites/resolve_import_errors.ts b/x-pack/test/saved_object_api_integration/common/suites/resolve_import_errors.ts
index 6de4e6dfbdcfad..cd4123433cb8ba 100644
--- a/x-pack/test/saved_object_api_integration/common/suites/resolve_import_errors.ts
+++ b/x-pack/test/saved_object_api_integration/common/suites/resolve_import_errors.ts
@@ -8,6 +8,7 @@
import expect from '@kbn/expect';
import { SuperTest } from 'supertest';
import type { Client } from '@elastic/elasticsearch';
+import type { SavedObjectReference, SavedObjectsImportRetry } from 'src/core/server';
import { SAVED_OBJECT_TEST_CASES as CASES } from '../lib/saved_object_test_cases';
import { SPACES } from '../lib/spaces';
import { expectResponses, getUrlPrefix, getTestTitle } from '../lib/saved_object_test_utils';
@@ -15,18 +16,32 @@ import { ExpectResponseBody, TestCase, TestDefinition, TestSuite } from '../lib/
export interface ResolveImportErrorsTestDefinition extends TestDefinition {
request: {
- objects: Array<{ type: string; id: string; originId?: string }>;
- retries: Array<{ type: string; id: string; overwrite: boolean; destinationId?: string }>;
+ objects: Array<{
+ type: string;
+ id: string;
+ originId?: string;
+ references?: SavedObjectReference[];
+ }>;
+ retries: Array<{
+ type: string;
+ id: string;
+ overwrite: boolean;
+ destinationId?: string;
+ replaceReferences?: SavedObjectsImportRetry['replaceReferences'];
+ }>;
};
overwrite: boolean;
createNewCopies: boolean;
}
export type ResolveImportErrorsTestSuite = TestSuite;
-export interface ResolveImportErrorsTestCase extends TestCase {
+export type FailureType = 'unsupported_type' | 'conflict';
+export interface ResolveImportErrorsTestCase extends Omit {
originId?: string;
expectedNewId?: string;
+ references?: SavedObjectReference[];
+ replaceReferences?: SavedObjectsImportRetry['replaceReferences'];
successParam?: string;
- failure?: 400 | 409; // only used for permitted response case
+ failureType?: FailureType; // only used for permitted response case
}
const NEW_ATTRIBUTE_KEY = 'title'; // all type mappings include this attribute, for simplicity's sake
@@ -39,8 +54,9 @@ const NEW_ATTRIBUTE_VAL = `New attribute value ${Date.now()}`;
// * id: conflict_3
// * id: conflict_4a, originId: conflict_4
// using the five conflict test case objects below, we can exercise various permutations of exact/inexact/ambiguous conflict scenarios
+const { HIDDEN, ...REMAINING_CASES } = CASES;
export const TEST_CASES: Record = Object.freeze({
- ...CASES,
+ ...REMAINING_CASES,
CONFLICT_1A_OBJ: Object.freeze({
type: 'sharedtype',
id: `conflict_1a`,
@@ -71,32 +87,78 @@ export const TEST_CASES: Record = Object.fr
expectedNewId: `conflict_4a`,
}),
});
+export const SPECIAL_TEST_CASES: Record = Object.freeze({
+ HIDDEN,
+ OUTBOUND_REFERENCE_ORIGIN_MATCH_1_OBJ: Object.freeze({
+ // This object does not already exist, but it has a reference to the originId of an index pattern that does exist.
+ // We use index patterns because they are one of the few reference types that are validated, so the import will fail if the reference
+ // is broken.
+ // This import is designed to succeed because there is exactly one origin match for its reference, and that reference will be changed to
+ // match the index pattern's new ID.
+ type: 'sharedtype',
+ id: 'outbound-reference-origin-match-1',
+ references: [{ name: '1', type: 'index-pattern', id: 'inbound-reference-origin-match-1' }],
+ }),
+ OUTBOUND_REFERENCE_ORIGIN_MATCH_2_OBJ: Object.freeze({
+ // This object does not already exist, but it has a reference to the originId of two index patterns that do exist.
+ // This import would normally fail because there are two origin matches for its reference, and we can't currently handle ambiguous
+ // destinations for reference origin matches.
+ // However, when retrying we can specify which reference(s) should be replaced.
+ type: 'sharedtype',
+ id: 'outbound-reference-origin-match-2',
+ references: [{ name: '1', type: 'index-pattern', id: 'inbound-reference-origin-match-2' }],
+ replaceReferences: [
+ {
+ type: 'index-pattern',
+ from: 'inbound-reference-origin-match-2',
+ to: 'inbound-reference-origin-match-2a',
+ },
+ ],
+ }),
+});
/**
* Test cases have additional properties that we don't want to send in HTTP Requests
*/
const createRequest = (
- { type, id, originId, expectedNewId, successParam }: ResolveImportErrorsTestCase,
+ {
+ type,
+ id,
+ originId,
+ expectedNewId,
+ references,
+ replaceReferences,
+ successParam,
+ }: ResolveImportErrorsTestCase,
overwrite: boolean
): ResolveImportErrorsTestDefinition['request'] => ({
- objects: [{ type, id, ...(originId && { originId }) }],
+ objects: [{ type, id, ...(originId && { originId }), ...(references && { references }) }],
retries: [
{
type,
id,
overwrite,
...(expectedNewId && { destinationId: expectedNewId }),
+ ...(replaceReferences && { replaceReferences }),
...(successParam === 'createNewCopy' && { createNewCopy: true }),
},
],
});
+export const resolveImportErrorsTestCaseFailures = {
+ failUnsupportedType: (condition?: boolean): { failureType?: 'unsupported_type' } =>
+ condition !== false ? { failureType: 'unsupported_type' } : {},
+ failConflict: (condition?: boolean): { failureType?: 'conflict' } =>
+ condition !== false ? { failureType: 'conflict' } : {},
+};
+
export function resolveImportErrorsTestSuiteFactory(
es: Client,
esArchiver: any,
supertest: SuperTest
) {
- const expectSavedObjectForbidden = expectResponses.forbiddenTypes('bulk_create');
+ const expectSavedObjectForbidden = (action: string, typeOrTypes: string | string[]) =>
+ expectResponses.forbiddenTypes(action)(typeOrTypes);
const expectResponseBody =
(
testCases: ResolveImportErrorsTestCase | ResolveImportErrorsTestCase[],
@@ -110,12 +172,12 @@ export function resolveImportErrorsTestSuiteFactory(
const testCaseArray = Array.isArray(testCases) ? testCases : [testCases];
if (statusCode === 403) {
const types = testCaseArray.map((x) => x.type);
- await expectSavedObjectForbidden(types)(response);
+ await expectSavedObjectForbidden('bulk_create', types)(response);
} else {
// permitted
const { success, successCount, successResults, errors } = response.body;
- const expectedSuccesses = testCaseArray.filter((x) => !x.failure);
- const expectedFailures = testCaseArray.filter((x) => x.failure);
+ const expectedSuccesses = testCaseArray.filter((x) => !x.failureType);
+ const expectedFailures = testCaseArray.filter((x) => x.failureType);
expect(success).to.eql(expectedFailures.length === 0);
expect(successCount).to.eql(expectedSuccesses.length);
if (expectedFailures.length) {
@@ -168,21 +230,23 @@ export function resolveImportErrorsTestSuiteFactory(
}
}
for (let i = 0; i < expectedFailures.length; i++) {
- const { type, id, failure, expectedNewId } = expectedFailures[i];
+ const { type, id, failureType, expectedNewId } = expectedFailures[i];
// we don't know the order of the returned errors; search for each one
const object = (errors as Array>).find(
(x) => x.type === type && x.id === id
);
expect(object).not.to.be(undefined);
- if (failure === 400) {
- expect(object!.error).to.eql({ type: 'unsupported_type' });
- } else {
- // 409
- expect(object!.error).to.eql({
- type: 'conflict',
- ...(expectedNewId && { destinationId: expectedNewId }),
- });
+ const expectedError: Record = { type: failureType };
+ switch (failureType!) {
+ case 'unsupported_type':
+ break;
+ case 'conflict':
+ if (expectedNewId) {
+ expectedError.destinationId = expectedNewId;
+ }
+ break;
}
+ expect(object!.error).to.eql(expectedError);
}
}
};
diff --git a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/import.ts b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/import.ts
index 1992dd6fea2248..b1f1776a7c2f17 100644
--- a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/import.ts
+++ b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/import.ts
@@ -6,12 +6,14 @@
*/
import { SPACES } from '../../common/lib/spaces';
-import { testCaseFailures, getTestScenarios } from '../../common/lib/saved_object_test_utils';
+import { getTestScenarios } from '../../common/lib/saved_object_test_utils';
import { TestUser } from '../../common/lib/types';
import { FtrProviderContext } from '../../common/ftr_provider_context';
import {
importTestSuiteFactory,
+ importTestCaseFailures,
TEST_CASES as CASES,
+ SPECIAL_TEST_CASES,
ImportTestDefinition,
} from '../../common/suites/import';
@@ -20,21 +22,23 @@ const {
SPACE_1: { spaceId: SPACE_1_ID },
SPACE_2: { spaceId: SPACE_2_ID },
} = SPACES;
-const { fail400, fail409 } = testCaseFailures;
+const { failUnsupportedType, failConflict, failAmbiguousConflict, failMissingReferences } =
+ importTestCaseFailures;
const destinationId = (condition?: boolean) =>
condition !== false ? { successParam: 'destinationId' } : {};
const newCopy = () => ({ successParam: 'createNewCopy' });
-const ambiguousConflict = (suffix: string) => ({
- failure: 409 as 409,
- fail409Param: `ambiguous_conflict_${suffix}`,
-});
const createNewCopiesTestCases = () => {
// for each outcome, if failure !== undefined then we expect to receive
// an error; otherwise, we expect to receive a success result
- const cases = Object.entries(CASES).filter(([key]) => key !== 'HIDDEN');
- const importable = cases.map(([, val]) => ({ ...val, successParam: 'createNewCopies' }));
- const nonImportable = [{ ...CASES.HIDDEN, ...fail400() }];
+ const importable = Object.entries(CASES).map(([, val]) => ({
+ ...val,
+ successParam: 'createNewCopies',
+ }));
+ const nonImportable = [{ ...CASES.HIDDEN, ...failUnsupportedType() }]; // unsupported_type is an "unresolvable" error
+ // Other special test cases are excluded because they can result in "resolvable" errors that will prevent the rest of the objects from
+ // being created. The test suite assumes that when the createNewCopies option is enabled, all non-error results are actually created,
+ // and it makes assertions based on that.
const all = [...importable, ...nonImportable];
return { importable, nonImportable, all };
};
@@ -46,64 +50,92 @@ const createTestCases = (overwrite: boolean, spaceId: string) => {
// when overwrite=true, all of the objects in this group are created successfully, so we can check the created object attributes
{
...CASES.SINGLE_NAMESPACE_DEFAULT_SPACE,
- ...fail409(!overwrite && spaceId === DEFAULT_SPACE_ID),
+ ...failConflict(!overwrite && spaceId === DEFAULT_SPACE_ID),
},
- { ...CASES.SINGLE_NAMESPACE_SPACE_1, ...fail409(!overwrite && spaceId === SPACE_1_ID) },
- { ...CASES.SINGLE_NAMESPACE_SPACE_2, ...fail409(!overwrite && spaceId === SPACE_2_ID) },
- { ...CASES.NAMESPACE_AGNOSTIC, ...fail409(!overwrite) },
+ { ...CASES.SINGLE_NAMESPACE_SPACE_1, ...failConflict(!overwrite && spaceId === SPACE_1_ID) },
+ { ...CASES.SINGLE_NAMESPACE_SPACE_2, ...failConflict(!overwrite && spaceId === SPACE_2_ID) },
+ { ...CASES.NAMESPACE_AGNOSTIC, ...failConflict(!overwrite) },
CASES.NEW_SINGLE_NAMESPACE_OBJ,
CASES.NEW_NAMESPACE_AGNOSTIC_OBJ,
];
- const group1NonImportable = [{ ...CASES.HIDDEN, ...fail400() }];
+ const group1NonImportable = [{ ...CASES.HIDDEN, ...failUnsupportedType() }];
const group1All = group1Importable.concat(group1NonImportable);
const group2 = [
// when overwrite=true, all of the objects in this group are created successfully, so we can check the created object attributes
CASES.NEW_MULTI_NAMESPACE_OBJ,
- { ...CASES.MULTI_NAMESPACE_ALL_SPACES, ...fail409(!overwrite) },
+ { ...CASES.MULTI_NAMESPACE_ALL_SPACES, ...failConflict(!overwrite) },
{
...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1,
- ...fail409(!overwrite && (spaceId === DEFAULT_SPACE_ID || spaceId === SPACE_1_ID)),
+ ...failConflict(!overwrite && (spaceId === DEFAULT_SPACE_ID || spaceId === SPACE_1_ID)),
...destinationId(spaceId !== DEFAULT_SPACE_ID && spaceId !== SPACE_1_ID),
},
{
...CASES.MULTI_NAMESPACE_ONLY_SPACE_1,
- ...fail409(!overwrite && spaceId === SPACE_1_ID),
+ ...failConflict(!overwrite && spaceId === SPACE_1_ID),
...destinationId(spaceId !== SPACE_1_ID),
},
{
...CASES.MULTI_NAMESPACE_ONLY_SPACE_2,
- ...fail409(!overwrite && spaceId === SPACE_2_ID),
+ ...failConflict(!overwrite && spaceId === SPACE_2_ID),
...destinationId(spaceId !== SPACE_2_ID),
},
{
...CASES.MULTI_NAMESPACE_ISOLATED_ONLY_DEFAULT_SPACE,
- ...fail409(!overwrite && spaceId === DEFAULT_SPACE_ID),
+ ...failConflict(!overwrite && spaceId === DEFAULT_SPACE_ID),
...destinationId(spaceId !== DEFAULT_SPACE_ID),
},
{
...CASES.MULTI_NAMESPACE_ISOLATED_ONLY_SPACE_1,
- ...fail409(!overwrite && spaceId === SPACE_1_ID),
+ ...failConflict(!overwrite && spaceId === SPACE_1_ID),
...destinationId(spaceId !== SPACE_1_ID),
},
{ ...CASES.CONFLICT_1A_OBJ, ...newCopy() }, // "ambiguous source" conflict which results in a new destination ID and empty origin ID
{ ...CASES.CONFLICT_1B_OBJ, ...newCopy() }, // "ambiguous source" conflict which results in a new destination ID and empty origin ID
- { ...CASES.CONFLICT_3A_OBJ, ...fail409(!overwrite), ...destinationId() }, // "inexact match" conflict
- { ...CASES.CONFLICT_4_OBJ, ...fail409(!overwrite), ...destinationId() }, // "inexact match" conflict
+ { ...CASES.CONFLICT_3A_OBJ, ...failConflict(!overwrite), ...destinationId() }, // "inexact match" conflict
+ { ...CASES.CONFLICT_4_OBJ, ...failConflict(!overwrite), ...destinationId() }, // "inexact match" conflict
];
const group3 = [
// when overwrite=true, all of the objects in this group are errors, so we cannot check the created object attributes
// grouping errors together simplifies the test suite code
- { ...CASES.CONFLICT_2C_OBJ, ...ambiguousConflict('2c') }, // "ambiguous destination" conflict
+ { ...CASES.CONFLICT_2C_OBJ, ...failAmbiguousConflict() }, // "ambiguous destination" conflict
];
const group4 = [
+ // This group needs to be executed *after* the previous test case, because those error assertions include metadata of the destinations,
+ // and *these* test cases would change that metadata.
+ { ...CASES.CONFLICT_2A_OBJ, ...failConflict(!overwrite) }, // "exact match" conflict with 2a
+ {
+ // "inexact match" conflict with 2b (since 2a already has a conflict source, this is not an ambiguous destination conflict)
+ ...CASES.CONFLICT_2C_OBJ,
+ ...failConflict(!overwrite),
+ ...destinationId(),
+ expectedNewId: 'conflict_2b',
+ },
+ ];
+ const group5 = [
// when overwrite=true, all of the objects in this group are created successfully, so we can check the created object attributes
- { ...CASES.CONFLICT_1_OBJ, ...fail409(!overwrite) }, // "exact match" conflict
+ { ...CASES.CONFLICT_1_OBJ, ...failConflict(!overwrite) }, // "exact match" conflict
CASES.CONFLICT_1A_OBJ, // no conflict because CONFLICT_1_OBJ is an exact match
CASES.CONFLICT_1B_OBJ, // no conflict because CONFLICT_1_OBJ is an exact match
{ ...CASES.CONFLICT_2C_OBJ, ...newCopy() }, // "ambiguous source and destination" conflict which results in a new destination ID and empty origin ID
{ ...CASES.CONFLICT_2D_OBJ, ...newCopy() }, // "ambiguous source and destination" conflict which results in a new destination ID and empty origin ID
];
- return { group1Importable, group1NonImportable, group1All, group2, group3, group4 };
+ const refOrigins = [
+ // One of these cases will always generate a missing_references error, which is an "unresolvable" error that stops any other objects
+ // from being created in the import. Other test cases can have assertions based on the created objects' attributes when the overwrite
+ // option is enabled, but these test cases are simply asserting pass/fail, so this group needs to be tested separately.
+ { ...SPECIAL_TEST_CASES.OUTBOUND_REFERENCE_ORIGIN_MATCH_1_OBJ },
+ { ...SPECIAL_TEST_CASES.OUTBOUND_REFERENCE_ORIGIN_MATCH_2_OBJ, ...failMissingReferences() },
+ ];
+ return {
+ group1Importable,
+ group1NonImportable,
+ group1All,
+ group2,
+ group3,
+ group4,
+ group5,
+ refOrigins,
+ };
};
export default function ({ getService }: FtrProviderContext) {
@@ -121,48 +153,89 @@ export default function ({ getService }: FtrProviderContext) {
if (createNewCopies) {
const { importable, nonImportable, all } = createNewCopiesTestCases();
+ const unauthorizedCommonTestDefinitions = [
+ createTestDefinitions(importable, true, { createNewCopies, spaceId }),
+ createTestDefinitions(nonImportable, false, { createNewCopies, spaceId, singleRequest }),
+ createTestDefinitions(all, true, {
+ createNewCopies,
+ spaceId,
+ singleRequest,
+ responseBodyOverride: expectSavedObjectForbidden('bulk_create', [
+ 'globaltype',
+ 'isolatedtype',
+ 'sharedtype',
+ 'sharecapabletype',
+ ]),
+ }),
+ ];
return {
- unauthorized: [
- createTestDefinitions(importable, true, { createNewCopies, spaceId }),
- createTestDefinitions(nonImportable, false, { createNewCopies, spaceId, singleRequest }),
- createTestDefinitions(all, true, {
- createNewCopies,
- spaceId,
- singleRequest,
- responseBodyOverride: expectSavedObjectForbidden([
- 'globaltype',
- 'isolatedtype',
- 'sharedtype',
- 'sharecapabletype',
- ]),
- }),
- ].flat(),
+ unauthorizedRead: unauthorizedCommonTestDefinitions.flat(),
+ unauthorizedWrite: unauthorizedCommonTestDefinitions.flat(),
authorized: createTestDefinitions(all, false, { createNewCopies, spaceId, singleRequest }),
};
}
- const { group1Importable, group1NonImportable, group1All, group2, group3, group4 } =
- createTestCases(overwrite, spaceId);
- return {
- unauthorized: [
- createTestDefinitions(group1Importable, true, { overwrite, spaceId }),
- createTestDefinitions(group1NonImportable, false, { overwrite, spaceId, singleRequest }),
- createTestDefinitions(group1All, true, {
+ const {
+ group1Importable,
+ group1NonImportable,
+ group1All,
+ group2,
+ group3,
+ group4,
+ group5,
+ refOrigins,
+ } = createTestCases(overwrite, spaceId);
+ const unauthorizedCommonTestDefinitions = [
+ createTestDefinitions(group1Importable, true, { overwrite, spaceId }),
+ createTestDefinitions(group1NonImportable, false, { overwrite, spaceId, singleRequest }),
+ createTestDefinitions(group1All, true, {
+ overwrite,
+ spaceId,
+ singleRequest,
+ responseBodyOverride: expectSavedObjectForbidden('bulk_create', [
+ 'globaltype',
+ 'isolatedtype',
+ ]),
+ }),
+ createTestDefinitions(group2, true, { overwrite, spaceId, singleRequest }),
+ createTestDefinitions(group3, true, { overwrite, spaceId, singleRequest }),
+ createTestDefinitions(group4, true, { overwrite, spaceId, singleRequest }),
+ createTestDefinitions(group5, true, { overwrite, spaceId, singleRequest }),
+ ];
+ const unauthorizedReadTestDefinitions = [...unauthorizedCommonTestDefinitions];
+ const unauthorizedWriteTestDefinitions = [...unauthorizedCommonTestDefinitions];
+ const authorizedTestDefinitions = [
+ createTestDefinitions(group1All, false, { overwrite, spaceId, singleRequest }),
+ createTestDefinitions(group2, false, { overwrite, spaceId, singleRequest }),
+ createTestDefinitions(group3, false, { overwrite, spaceId, singleRequest }),
+ createTestDefinitions(group4, false, { overwrite, spaceId, singleRequest }),
+ createTestDefinitions(group5, false, { overwrite, spaceId, singleRequest }),
+ ];
+ if (!overwrite) {
+ // Only include this group of test cases if the overwrite option is not enabled
+ unauthorizedReadTestDefinitions.push(
+ createTestDefinitions(refOrigins, true, {
overwrite,
spaceId,
singleRequest,
- responseBodyOverride: expectSavedObjectForbidden(['globaltype', 'isolatedtype']),
- }),
- createTestDefinitions(group2, true, { overwrite, spaceId, singleRequest }),
- createTestDefinitions(group3, true, { overwrite, spaceId, singleRequest }),
- createTestDefinitions(group4, true, { overwrite, spaceId, singleRequest }),
- ].flat(),
- authorized: [
- createTestDefinitions(group1All, false, { overwrite, spaceId, singleRequest }),
- createTestDefinitions(group2, false, { overwrite, spaceId, singleRequest }),
- createTestDefinitions(group3, false, { overwrite, spaceId, singleRequest }),
- createTestDefinitions(group4, false, { overwrite, spaceId, singleRequest }),
- ].flat(),
+ responseBodyOverride: expectSavedObjectForbidden('bulk_get', ['index-pattern']),
+ })
+ );
+ unauthorizedWriteTestDefinitions.push(
+ createTestDefinitions(refOrigins, true, {
+ overwrite,
+ spaceId,
+ singleRequest,
+ })
+ );
+ authorizedTestDefinitions.push(
+ createTestDefinitions(refOrigins, false, { overwrite, spaceId, singleRequest })
+ );
+ }
+ return {
+ unauthorizedRead: unauthorizedReadTestDefinitions.flat(),
+ unauthorizedWrite: unauthorizedWriteTestDefinitions.flat(),
+ authorized: authorizedTestDefinitions.flat(),
};
};
@@ -180,20 +253,20 @@ export default function ({ getService }: FtrProviderContext) {
? ' with createNewCopies enabled'
: ''
}`;
- const { unauthorized, authorized } = createTests(overwrite, createNewCopies, spaceId);
+ const { unauthorizedRead, unauthorizedWrite, authorized } = createTests(
+ overwrite,
+ createNewCopies,
+ spaceId
+ );
const _addTests = (user: TestUser, tests: ImportTestDefinition[]) => {
addTests(`${user.description}${suffix}`, { user, spaceId, tests });
};
- [
- users.noAccess,
- users.legacyAll,
- users.dualRead,
- users.readGlobally,
- users.readAtSpace,
- users.allAtOtherSpace,
- ].forEach((user) => {
- _addTests(user, unauthorized);
+ [users.noAccess, users.legacyAll, users.allAtOtherSpace].forEach((user) => {
+ _addTests(user, unauthorizedRead);
+ });
+ [users.dualRead, users.readGlobally, users.readAtSpace].forEach((user) => {
+ _addTests(user, unauthorizedWrite);
});
[users.dualAll, users.allGlobally, users.allAtSpace, users.superuser].forEach((user) => {
_addTests(user, authorized);
diff --git a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/resolve_import_errors.ts b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/resolve_import_errors.ts
index b59ae923250403..153c756ee64610 100644
--- a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/resolve_import_errors.ts
+++ b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/resolve_import_errors.ts
@@ -7,12 +7,14 @@
import { v4 as uuidv4 } from 'uuid';
import { SPACES } from '../../common/lib/spaces';
-import { testCaseFailures, getTestScenarios } from '../../common/lib/saved_object_test_utils';
+import { getTestScenarios } from '../../common/lib/saved_object_test_utils';
import { TestUser } from '../../common/lib/types';
import { FtrProviderContext } from '../../common/ftr_provider_context';
import {
resolveImportErrorsTestSuiteFactory,
+ resolveImportErrorsTestCaseFailures,
TEST_CASES as CASES,
+ SPECIAL_TEST_CASES,
ResolveImportErrorsTestDefinition,
} from '../../common/suites/resolve_import_errors';
@@ -21,7 +23,7 @@ const {
SPACE_1: { spaceId: SPACE_1_ID },
SPACE_2: { spaceId: SPACE_2_ID },
} = SPACES;
-const { fail400, fail409 } = testCaseFailures;
+const { failUnsupportedType, failConflict } = resolveImportErrorsTestCaseFailures;
const destinationId = (condition?: boolean) =>
condition !== false ? { successParam: 'destinationId' } : {};
const newCopy = () => ({ successParam: 'createNewCopy' });
@@ -29,13 +31,12 @@ const newCopy = () => ({ successParam: 'createNewCopy' });
const createNewCopiesTestCases = () => {
// for each outcome, if failure !== undefined then we expect to receive
// an error; otherwise, we expect to receive a success result
- const cases = Object.entries(CASES).filter(([key]) => key !== 'HIDDEN');
- const importable = cases.map(([, val]) => ({
+ const importable = Object.entries(CASES).map(([, val]) => ({
...val,
successParam: 'createNewCopies',
expectedNewId: uuidv4(),
}));
- const nonImportable = [{ ...CASES.HIDDEN, ...fail400() }];
+ const nonImportable = [{ ...SPECIAL_TEST_CASES.HIDDEN, ...failUnsupportedType() }]; // unsupported_type is an "unresolvable" error
const all = [...importable, ...nonImportable];
return { importable, nonImportable, all };
};
@@ -50,36 +51,36 @@ const createTestCases = (overwrite: boolean, spaceId: string) => {
? CASES.SINGLE_NAMESPACE_SPACE_1
: CASES.SINGLE_NAMESPACE_SPACE_2;
const group1Importable = [
- { ...singleNamespaceObject, ...fail409(!overwrite) },
- { ...CASES.NAMESPACE_AGNOSTIC, ...fail409(!overwrite) },
+ { ...singleNamespaceObject, ...failConflict(!overwrite) },
+ { ...CASES.NAMESPACE_AGNOSTIC, ...failConflict(!overwrite) },
];
- const group1NonImportable = [{ ...CASES.HIDDEN, ...fail400() }];
+ const group1NonImportable = [{ ...SPECIAL_TEST_CASES.HIDDEN, ...failUnsupportedType() }];
const group1All = [...group1Importable, ...group1NonImportable];
const group2 = [
- { ...CASES.MULTI_NAMESPACE_ALL_SPACES, ...fail409(!overwrite) },
+ { ...CASES.MULTI_NAMESPACE_ALL_SPACES, ...failConflict(!overwrite) },
{
...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1,
- ...fail409(!overwrite && (spaceId === DEFAULT_SPACE_ID || spaceId === SPACE_1_ID)),
+ ...failConflict(!overwrite && (spaceId === DEFAULT_SPACE_ID || spaceId === SPACE_1_ID)),
...destinationId(spaceId !== DEFAULT_SPACE_ID && spaceId !== SPACE_1_ID),
},
{
...CASES.MULTI_NAMESPACE_ONLY_SPACE_1,
- ...fail409(!overwrite && spaceId === SPACE_1_ID),
+ ...failConflict(!overwrite && spaceId === SPACE_1_ID),
...destinationId(spaceId !== SPACE_1_ID),
},
{
...CASES.MULTI_NAMESPACE_ONLY_SPACE_2,
- ...fail409(!overwrite && spaceId === SPACE_2_ID),
+ ...failConflict(!overwrite && spaceId === SPACE_2_ID),
...destinationId(spaceId !== SPACE_2_ID),
},
{
...CASES.MULTI_NAMESPACE_ISOLATED_ONLY_DEFAULT_SPACE,
- ...fail409(!overwrite && spaceId === DEFAULT_SPACE_ID),
+ ...failConflict(!overwrite && spaceId === DEFAULT_SPACE_ID),
...destinationId(spaceId !== DEFAULT_SPACE_ID),
},
{
...CASES.MULTI_NAMESPACE_ISOLATED_ONLY_SPACE_1,
- ...fail409(!overwrite && spaceId === SPACE_1_ID),
+ ...failConflict(!overwrite && spaceId === SPACE_1_ID),
...destinationId(spaceId !== SPACE_1_ID),
},
{ ...CASES.CONFLICT_1A_OBJ, ...newCopy() }, // "ambiguous source" conflict which results in a new destination ID and empty origin ID
@@ -87,11 +88,16 @@ const createTestCases = (overwrite: boolean, spaceId: string) => {
// all of the cases below represent imports that had an inexact match conflict or an ambiguous conflict
// if we call _resolve_import_errors and don't specify overwrite, each of these will result in a conflict because an object with that
// `expectedDestinationId` already exists
- { ...CASES.CONFLICT_2C_OBJ, ...fail409(!overwrite), ...destinationId() }, // "ambiguous destination" conflict; if overwrite=true, will overwrite 'conflict_2a'
- { ...CASES.CONFLICT_3A_OBJ, ...fail409(!overwrite), ...destinationId() }, // "inexact match" conflict; if overwrite=true, will overwrite 'conflict_3'
- { ...CASES.CONFLICT_4_OBJ, ...fail409(!overwrite), ...destinationId() }, // "inexact match" conflict; if overwrite=true, will overwrite 'conflict_4a'
+ { ...CASES.CONFLICT_2C_OBJ, ...failConflict(!overwrite), ...destinationId() }, // "ambiguous destination" conflict; if overwrite=true, will overwrite 'conflict_2a'
+ { ...CASES.CONFLICT_3A_OBJ, ...failConflict(!overwrite), ...destinationId() }, // "inexact match" conflict; if overwrite=true, will overwrite 'conflict_3'
+ { ...CASES.CONFLICT_4_OBJ, ...failConflict(!overwrite), ...destinationId() }, // "inexact match" conflict; if overwrite=true, will overwrite 'conflict_4a'
];
- return { group1Importable, group1NonImportable, group1All, group2 };
+ const refOrigins = [
+ // These are in a separate group because they will result in a different 403 error for users who are unauthorized to read
+ { ...SPECIAL_TEST_CASES.OUTBOUND_REFERENCE_ORIGIN_MATCH_1_OBJ },
+ { ...SPECIAL_TEST_CASES.OUTBOUND_REFERENCE_ORIGIN_MATCH_2_OBJ },
+ ];
+ return { group1Importable, group1NonImportable, group1All, group2, refOrigins };
};
export default function ({ getService }: FtrProviderContext) {
@@ -107,45 +113,62 @@ export default function ({ getService }: FtrProviderContext) {
if (createNewCopies) {
const { importable, nonImportable, all } = createNewCopiesTestCases();
+ const unauthorizedCommonTestDefinitions = [
+ createTestDefinitions(importable, true, { createNewCopies, spaceId }),
+ createTestDefinitions(nonImportable, false, { createNewCopies, spaceId, singleRequest }),
+ createTestDefinitions(all, true, {
+ createNewCopies,
+ spaceId,
+ singleRequest,
+ responseBodyOverride: expectSavedObjectForbidden('bulk_create', [
+ 'globaltype',
+ 'isolatedtype',
+ 'sharedtype',
+ 'sharecapabletype',
+ ]),
+ }),
+ ];
return {
- unauthorized: [
- createTestDefinitions(importable, true, { createNewCopies, spaceId }),
- createTestDefinitions(nonImportable, false, { createNewCopies, spaceId, singleRequest }),
- createTestDefinitions(all, true, {
- createNewCopies,
- spaceId,
- singleRequest,
- responseBodyOverride: expectSavedObjectForbidden([
- 'globaltype',
- 'isolatedtype',
- 'sharedtype',
- 'sharecapabletype',
- ]),
- }),
- ].flat(),
+ unauthorizedRead: unauthorizedCommonTestDefinitions.flat(),
+ unauthorizedWrite: unauthorizedCommonTestDefinitions.flat(),
authorized: createTestDefinitions(all, false, { createNewCopies, spaceId, singleRequest }),
};
}
- const { group1Importable, group1NonImportable, group1All, group2 } = createTestCases(
- overwrite,
- spaceId
- );
+ const { group1Importable, group1NonImportable, group1All, group2, refOrigins } =
+ createTestCases(overwrite, spaceId);
+ const unauthorizedCommonTestDefinitions = [
+ createTestDefinitions(group1Importable, true, { overwrite, spaceId }),
+ createTestDefinitions(group1NonImportable, false, { overwrite, spaceId, singleRequest }),
+ createTestDefinitions(group1All, true, {
+ overwrite,
+ spaceId,
+ singleRequest,
+ responseBodyOverride: expectSavedObjectForbidden('bulk_create', [
+ 'globaltype',
+ 'isolatedtype',
+ ]),
+ }),
+ createTestDefinitions(group2, true, { overwrite, spaceId, singleRequest }),
+ ];
return {
- unauthorized: [
- createTestDefinitions(group1Importable, true, { overwrite, spaceId }),
- createTestDefinitions(group1NonImportable, false, { overwrite, spaceId, singleRequest }),
- createTestDefinitions(group1All, true, {
+ unauthorizedRead: [
+ ...unauthorizedCommonTestDefinitions,
+ createTestDefinitions(refOrigins, true, {
overwrite,
spaceId,
singleRequest,
- responseBodyOverride: expectSavedObjectForbidden(['globaltype', 'isolatedtype']),
+ responseBodyOverride: expectSavedObjectForbidden('bulk_get', ['index-pattern']),
}),
- createTestDefinitions(group2, true, { overwrite, spaceId, singleRequest }),
+ ].flat(),
+ unauthorizedWrite: [
+ ...unauthorizedCommonTestDefinitions,
+ createTestDefinitions(refOrigins, true, { overwrite, spaceId, singleRequest }),
].flat(),
authorized: [
createTestDefinitions(group1All, false, { overwrite, spaceId, singleRequest }),
createTestDefinitions(group2, false, { overwrite, spaceId, singleRequest }),
+ createTestDefinitions(refOrigins, false, { overwrite, spaceId, singleRequest }),
].flat(),
};
};
@@ -164,20 +187,20 @@ export default function ({ getService }: FtrProviderContext) {
? ' with createNewCopies enabled'
: ''
}`;
- const { unauthorized, authorized } = createTests(overwrite, createNewCopies, spaceId);
+ const { unauthorizedRead, unauthorizedWrite, authorized } = createTests(
+ overwrite,
+ createNewCopies,
+ spaceId
+ );
const _addTests = (user: TestUser, tests: ResolveImportErrorsTestDefinition[]) => {
addTests(`${user.description}${suffix}`, { user, spaceId, tests });
};
- [
- users.noAccess,
- users.legacyAll,
- users.dualRead,
- users.readGlobally,
- users.readAtSpace,
- users.allAtOtherSpace,
- ].forEach((user) => {
- _addTests(user, unauthorized);
+ [users.noAccess, users.legacyAll, users.allAtOtherSpace].forEach((user) => {
+ _addTests(user, unauthorizedRead);
+ });
+ [users.dualRead, users.readGlobally, users.readAtSpace].forEach((user) => {
+ _addTests(user, unauthorizedWrite);
});
[users.dualAll, users.allGlobally, users.allAtSpace, users.superuser].forEach((user) => {
_addTests(user, authorized);
diff --git a/x-pack/test/saved_object_api_integration/spaces_only/apis/import.ts b/x-pack/test/saved_object_api_integration/spaces_only/apis/import.ts
index 910b51a92ed816..04631641904a02 100644
--- a/x-pack/test/saved_object_api_integration/spaces_only/apis/import.ts
+++ b/x-pack/test/saved_object_api_integration/spaces_only/apis/import.ts
@@ -6,77 +6,81 @@
*/
import { SPACES } from '../../common/lib/spaces';
-import { testCaseFailures, getTestScenarios } from '../../common/lib/saved_object_test_utils';
+import { getTestScenarios } from '../../common/lib/saved_object_test_utils';
import { FtrProviderContext } from '../../common/ftr_provider_context';
-import { importTestSuiteFactory, TEST_CASES as CASES } from '../../common/suites/import';
+import {
+ importTestSuiteFactory,
+ importTestCaseFailures,
+ TEST_CASES as CASES,
+ SPECIAL_TEST_CASES,
+} from '../../common/suites/import';
const {
DEFAULT: { spaceId: DEFAULT_SPACE_ID },
SPACE_1: { spaceId: SPACE_1_ID },
SPACE_2: { spaceId: SPACE_2_ID },
} = SPACES;
-const { fail400, fail409 } = testCaseFailures;
+const { failUnsupportedType, failConflict, failAmbiguousConflict, failMissingReferences } =
+ importTestCaseFailures;
const destinationId = (condition?: boolean) =>
condition !== false ? { successParam: 'destinationId' } : {};
const newCopy = () => ({ successParam: 'createNewCopy' });
-const ambiguousConflict = (suffix: string) => ({
- failure: 409 as 409,
- fail409Param: `ambiguous_conflict_${suffix}`,
-});
const createNewCopiesTestCases = () => {
- // for each outcome, if failure !== undefined then we expect to receive
+ // for each outcome, if failureType !== undefined then we expect to receive
// an error; otherwise, we expect to receive a success result
- const cases = Object.entries(CASES).filter(([key]) => key !== 'HIDDEN');
return [
- ...cases.map(([, val]) => ({ ...val, successParam: 'createNewCopies' })),
- { ...CASES.HIDDEN, ...fail400() },
+ ...Object.entries(CASES).map(([, val]) => ({ ...val, successParam: 'createNewCopies' })),
+ { ...SPECIAL_TEST_CASES.HIDDEN, ...failUnsupportedType() }, // unsupported_type is an "unresolvable" error
+ // Other special test cases are excluded because they can result in "resolvable" errors that will prevent the rest of the objects from
+ // being created. The test suite assumes that when the createNewCopies option is enabled, all non-error results are actually created,
+ // and it makes assertions based on that.
];
};
const createTestCases = (overwrite: boolean, spaceId: string) => {
- // for each outcome, if failure !== undefined then we expect to receive
+ // for each outcome, if failureType !== undefined then we expect to receive
// an error; otherwise, we expect to receive a success result
const group1 = [
// when overwrite=true, all of the objects in this group are created successfully, so we can check the created object attributes
{
...CASES.SINGLE_NAMESPACE_DEFAULT_SPACE,
- ...fail409(!overwrite && spaceId === DEFAULT_SPACE_ID),
+ ...failConflict(!overwrite && spaceId === DEFAULT_SPACE_ID),
},
- { ...CASES.SINGLE_NAMESPACE_SPACE_1, ...fail409(!overwrite && spaceId === SPACE_1_ID) },
- { ...CASES.SINGLE_NAMESPACE_SPACE_2, ...fail409(!overwrite && spaceId === SPACE_2_ID) },
- { ...CASES.MULTI_NAMESPACE_ALL_SPACES, ...fail409(!overwrite) },
+ { ...CASES.SINGLE_NAMESPACE_SPACE_1, ...failConflict(!overwrite && spaceId === SPACE_1_ID) },
+ { ...CASES.SINGLE_NAMESPACE_SPACE_2, ...failConflict(!overwrite && spaceId === SPACE_2_ID) },
+ { ...CASES.MULTI_NAMESPACE_ALL_SPACES, ...failConflict(!overwrite) },
{
...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1,
- ...fail409(!overwrite && (spaceId === DEFAULT_SPACE_ID || spaceId === SPACE_1_ID)),
+ ...failConflict(!overwrite && (spaceId === DEFAULT_SPACE_ID || spaceId === SPACE_1_ID)),
...destinationId(spaceId !== DEFAULT_SPACE_ID && spaceId !== SPACE_1_ID),
},
{
...CASES.MULTI_NAMESPACE_ONLY_SPACE_1,
- ...fail409(!overwrite && spaceId === SPACE_1_ID),
+ ...failConflict(!overwrite && spaceId === SPACE_1_ID),
...destinationId(spaceId !== SPACE_1_ID),
},
{
...CASES.MULTI_NAMESPACE_ONLY_SPACE_2,
- ...fail409(!overwrite && spaceId === SPACE_2_ID),
+ ...failConflict(!overwrite && spaceId === SPACE_2_ID),
...destinationId(spaceId !== SPACE_2_ID),
},
{
...CASES.MULTI_NAMESPACE_ISOLATED_ONLY_DEFAULT_SPACE,
- ...fail409(!overwrite && spaceId === DEFAULT_SPACE_ID),
+ ...failConflict(!overwrite && spaceId === DEFAULT_SPACE_ID),
...destinationId(spaceId !== DEFAULT_SPACE_ID),
},
{
...CASES.MULTI_NAMESPACE_ISOLATED_ONLY_SPACE_1,
- ...fail409(!overwrite && spaceId === SPACE_1_ID),
+ ...failConflict(!overwrite && spaceId === SPACE_1_ID),
...destinationId(spaceId !== SPACE_1_ID),
},
- { ...CASES.NAMESPACE_AGNOSTIC, ...fail409(!overwrite) },
- { ...CASES.HIDDEN, ...fail400() },
+ { ...CASES.NAMESPACE_AGNOSTIC, ...failConflict(!overwrite) },
+ { ...SPECIAL_TEST_CASES.HIDDEN, ...failUnsupportedType() },
{ ...CASES.CONFLICT_1A_OBJ, ...newCopy() }, // "ambiguous source" conflict which results in a new destination ID and empty origin ID
{ ...CASES.CONFLICT_1B_OBJ, ...newCopy() }, // "ambiguous source" conflict which results in a new destination ID and empty origin ID
- { ...CASES.CONFLICT_3A_OBJ, ...fail409(!overwrite), ...destinationId() }, // "inexact match" conflict
- { ...CASES.CONFLICT_4_OBJ, ...fail409(!overwrite), ...destinationId() }, // "inexact match" conflict
+ { ...CASES.CONFLICT_3A_OBJ, ...failConflict(!overwrite), ...destinationId() }, // "inexact match" conflict
+ { ...CASES.CONFLICT_4_OBJ, ...failConflict(!overwrite), ...destinationId() }, // "inexact match" conflict
CASES.NEW_SINGLE_NAMESPACE_OBJ,
CASES.NEW_MULTI_NAMESPACE_OBJ,
CASES.NEW_NAMESPACE_AGNOSTIC_OBJ,
@@ -84,17 +88,36 @@ const createTestCases = (overwrite: boolean, spaceId: string) => {
const group2 = [
// when overwrite=true, all of the objects in this group are errors, so we cannot check the created object attributes
// grouping errors together simplifies the test suite code
- { ...CASES.CONFLICT_2C_OBJ, ...ambiguousConflict('2c') }, // "ambiguous destination" conflict
+ { ...CASES.CONFLICT_2C_OBJ, ...failAmbiguousConflict() }, // "ambiguous destination" conflict
];
const group3 = [
+ // This group needs to be executed *after* the previous test case, because those error assertions include metadata of the destinations,
+ // and *these* test cases would change that metadata.
+ { ...CASES.CONFLICT_2A_OBJ, ...failConflict(!overwrite) }, // "exact match" conflict with 2a
+ {
+ // "inexact match" conflict with 2b (since 2a already has a conflict source, this is not an ambiguous destination conflict)
+ ...CASES.CONFLICT_2C_OBJ,
+ ...failConflict(!overwrite),
+ ...destinationId(),
+ expectedNewId: 'conflict_2b',
+ },
+ ];
+ const group4 = [
// when overwrite=true, all of the objects in this group are created successfully, so we can check the created object attributes
- { ...CASES.CONFLICT_1_OBJ, ...fail409(!overwrite) }, // "exact match" conflict
+ { ...CASES.CONFLICT_1_OBJ, ...failConflict(!overwrite) }, // "exact match" conflict
CASES.CONFLICT_1A_OBJ, // no conflict because CONFLICT_1_OBJ is an exact match
CASES.CONFLICT_1B_OBJ, // no conflict because CONFLICT_1_OBJ is an exact match
{ ...CASES.CONFLICT_2C_OBJ, ...newCopy() }, // "ambiguous source and destination" conflict which results in a new destination ID and empty origin ID
{ ...CASES.CONFLICT_2D_OBJ, ...newCopy() }, // "ambiguous source and destination" conflict which results in a new destination ID and empty origin ID
];
- return { group1, group2, group3 };
+ const refOrigins = [
+ // One of these cases will always generate a missing_references error, which is an "unresolvable" error that stops any other objects
+ // from being created in the import. Other test cases can have assertions based on the created objects' attributes when the overwrite
+ // option is enabled, but these test cases are simply asserting pass/fail, so this group needs to be tested separately.
+ { ...SPECIAL_TEST_CASES.OUTBOUND_REFERENCE_ORIGIN_MATCH_1_OBJ },
+ { ...SPECIAL_TEST_CASES.OUTBOUND_REFERENCE_ORIGIN_MATCH_2_OBJ, ...failMissingReferences() },
+ ];
+ return { group1, group2, group3, group4, refOrigins };
};
export default function ({ getService }: FtrProviderContext) {
@@ -110,12 +133,18 @@ export default function ({ getService }: FtrProviderContext) {
return createTestDefinitions(cases, false, { createNewCopies, spaceId, singleRequest });
}
- const { group1, group2, group3 } = createTestCases(overwrite, spaceId);
- return [
+ const { group1, group2, group3, group4, refOrigins } = createTestCases(overwrite, spaceId);
+ const tests = [
createTestDefinitions(group1, false, { overwrite, spaceId, singleRequest }),
createTestDefinitions(group2, false, { overwrite, spaceId, singleRequest }),
createTestDefinitions(group3, false, { overwrite, spaceId, singleRequest }),
- ].flat();
+ createTestDefinitions(group4, false, { overwrite, spaceId, singleRequest }),
+ ];
+ if (!overwrite) {
+ // Only include this group of test cases if the overwrite option is not enabled
+ tests.push(createTestDefinitions(refOrigins, false, { overwrite, spaceId, singleRequest }));
+ }
+ return tests.flat();
};
describe('_import', () => {
diff --git a/x-pack/test/saved_object_api_integration/spaces_only/apis/resolve_import_errors.ts b/x-pack/test/saved_object_api_integration/spaces_only/apis/resolve_import_errors.ts
index 131335c421f00f..862e53d6e4663f 100644
--- a/x-pack/test/saved_object_api_integration/spaces_only/apis/resolve_import_errors.ts
+++ b/x-pack/test/saved_object_api_integration/spaces_only/apis/resolve_import_errors.ts
@@ -7,11 +7,13 @@
import { v4 as uuidv4 } from 'uuid';
import { SPACES } from '../../common/lib/spaces';
-import { testCaseFailures, getTestScenarios } from '../../common/lib/saved_object_test_utils';
+import { getTestScenarios } from '../../common/lib/saved_object_test_utils';
import { FtrProviderContext } from '../../common/ftr_provider_context';
import {
resolveImportErrorsTestSuiteFactory,
+ resolveImportErrorsTestCaseFailures,
TEST_CASES as CASES,
+ SPECIAL_TEST_CASES,
} from '../../common/suites/resolve_import_errors';
const {
@@ -19,27 +21,28 @@ const {
SPACE_1: { spaceId: SPACE_1_ID },
SPACE_2: { spaceId: SPACE_2_ID },
} = SPACES;
-const { fail400, fail409 } = testCaseFailures;
+const { failUnsupportedType, failConflict } = resolveImportErrorsTestCaseFailures;
const destinationId = (condition?: boolean) =>
condition !== false ? { successParam: 'destinationId' } : {};
const newCopy = () => ({ successParam: 'createNewCopy' });
const createNewCopiesTestCases = () => {
- // for each outcome, if failure !== undefined then we expect to receive
+ // for each outcome, if failureType !== undefined then we expect to receive
// an error; otherwise, we expect to receive a success result
- const cases = Object.entries(CASES).filter(([key]) => key !== 'HIDDEN');
return [
- ...cases.map(([, val]) => ({
+ ...Object.entries(CASES).map(([, val]) => ({
...val,
successParam: 'createNewCopies',
expectedNewId: uuidv4(),
})),
- { ...CASES.HIDDEN, ...fail400() },
+ { ...SPECIAL_TEST_CASES.HIDDEN, ...failUnsupportedType() }, // unsupported_type is an "unresolvable" error
+ // Other special test cases are excluded here for simplicity and consistency with the resolveImportErrors "spaces_and_security" test
+ // suite and the import test suites.
];
};
const createTestCases = (overwrite: boolean, spaceId: string) => {
- // for each outcome, if failure !== undefined then we expect to receive
+ // for each outcome, if failureType !== undefined then we expect to receive
// an error; otherwise, we expect to receive a success result
const singleNamespaceObject =
spaceId === DEFAULT_SPACE_ID
@@ -48,43 +51,45 @@ const createTestCases = (overwrite: boolean, spaceId: string) => {
? CASES.SINGLE_NAMESPACE_SPACE_1
: CASES.SINGLE_NAMESPACE_SPACE_2;
return [
- { ...singleNamespaceObject, ...fail409(!overwrite) },
- { ...CASES.MULTI_NAMESPACE_ALL_SPACES, ...fail409(!overwrite) },
+ { ...singleNamespaceObject, ...failConflict(!overwrite) },
+ { ...CASES.MULTI_NAMESPACE_ALL_SPACES, ...failConflict(!overwrite) },
{
...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1,
- ...fail409(!overwrite && (spaceId === DEFAULT_SPACE_ID || spaceId === SPACE_1_ID)),
+ ...failConflict(!overwrite && (spaceId === DEFAULT_SPACE_ID || spaceId === SPACE_1_ID)),
...destinationId(spaceId !== DEFAULT_SPACE_ID && spaceId !== SPACE_1_ID),
},
{
...CASES.MULTI_NAMESPACE_ONLY_SPACE_1,
- ...fail409(!overwrite && spaceId === SPACE_1_ID),
+ ...failConflict(!overwrite && spaceId === SPACE_1_ID),
...destinationId(spaceId !== SPACE_1_ID),
},
{
...CASES.MULTI_NAMESPACE_ONLY_SPACE_2,
- ...fail409(!overwrite && spaceId === SPACE_2_ID),
+ ...failConflict(!overwrite && spaceId === SPACE_2_ID),
...destinationId(spaceId !== SPACE_2_ID),
},
{
...CASES.MULTI_NAMESPACE_ISOLATED_ONLY_DEFAULT_SPACE,
- ...fail409(!overwrite && spaceId === DEFAULT_SPACE_ID),
+ ...failConflict(!overwrite && spaceId === DEFAULT_SPACE_ID),
...destinationId(spaceId !== DEFAULT_SPACE_ID),
},
{
...CASES.MULTI_NAMESPACE_ISOLATED_ONLY_SPACE_1,
- ...fail409(!overwrite && spaceId === SPACE_1_ID),
+ ...failConflict(!overwrite && spaceId === SPACE_1_ID),
...destinationId(spaceId !== SPACE_1_ID),
},
- { ...CASES.NAMESPACE_AGNOSTIC, ...fail409(!overwrite) },
- { ...CASES.HIDDEN, ...fail400() },
+ { ...CASES.NAMESPACE_AGNOSTIC, ...failConflict(!overwrite) },
+ { ...SPECIAL_TEST_CASES.HIDDEN, ...failUnsupportedType() },
{ ...CASES.CONFLICT_1A_OBJ, ...newCopy() }, // "ambiguous source" conflict which results in a new destination ID and empty origin ID
{ ...CASES.CONFLICT_1B_OBJ, ...newCopy() }, // "ambiguous source" conflict which results in a new destination ID and empty origin ID
// all of the cases below represent imports that had an inexact match conflict or an ambiguous conflict
// if we call _resolve_import_errors and don't specify overwrite, each of these will result in a conflict because an object with that
// `expectedDestinationId` already exists
- { ...CASES.CONFLICT_2C_OBJ, ...fail409(!overwrite), ...destinationId() }, // "ambiguous destination" conflict; if overwrite=true, will overwrite 'conflict_2a'
- { ...CASES.CONFLICT_3A_OBJ, ...fail409(!overwrite), ...destinationId() }, // "inexact match" conflict; if overwrite=true, will overwrite 'conflict_3'
- { ...CASES.CONFLICT_4_OBJ, ...fail409(!overwrite), ...destinationId() }, // "inexact match" conflict; if overwrite=true, will overwrite 'conflict_4a'
+ { ...CASES.CONFLICT_2C_OBJ, ...failConflict(!overwrite), ...destinationId() }, // "ambiguous destination" conflict; if overwrite=true, will overwrite 'conflict_2a'
+ { ...CASES.CONFLICT_3A_OBJ, ...failConflict(!overwrite), ...destinationId() }, // "inexact match" conflict; if overwrite=true, will overwrite 'conflict_3'
+ { ...CASES.CONFLICT_4_OBJ, ...failConflict(!overwrite), ...destinationId() }, // "inexact match" conflict; if overwrite=true, will overwrite 'conflict_4a'
+ { ...SPECIAL_TEST_CASES.OUTBOUND_REFERENCE_ORIGIN_MATCH_1_OBJ },
+ { ...SPECIAL_TEST_CASES.OUTBOUND_REFERENCE_ORIGIN_MATCH_2_OBJ },
];
};
diff --git a/x-pack/test/spaces_api_integration/common/fixtures/es_archiver/saved_objects/spaces/data.json b/x-pack/test/spaces_api_integration/common/fixtures/es_archiver/saved_objects/spaces/data.json
index c5dc147b45123c..ab7118c132f1b3 100644
--- a/x-pack/test/spaces_api_integration/common/fixtures/es_archiver/saved_objects/spaces/data.json
+++ b/x-pack/test/spaces_api_integration/common/fixtures/es_archiver/saved_objects/spaces/data.json
@@ -673,12 +673,12 @@
{
"type": "doc",
"value": {
- "id": "sharedtype:conflict_1_default",
+ "id": "sharedtype:conflict_1a_default",
"index": ".kibana",
"source": {
- "originId": "conflict_1",
+ "originId": "conflict_1a",
"sharedtype": {
- "title": "A shared saved-object in one space"
+ "title": "This is used to test an inexact match conflict for an originId -> originId match"
},
"type": "sharedtype",
"namespaces": ["default"],
@@ -691,12 +691,12 @@
{
"type": "doc",
"value": {
- "id": "sharedtype:conflict_1_space_1",
+ "id": "sharedtype:conflict_1a_space_1",
"index": ".kibana",
"source": {
- "originId": "conflict_1",
+ "originId": "conflict_1a",
"sharedtype": {
- "title": "A shared saved-object in one space"
+ "title": "This is used to test an inexact match conflict for an originId -> originId match"
},
"type": "sharedtype",
"namespaces": ["space_1"],
@@ -709,12 +709,100 @@
{
"type": "doc",
"value": {
- "id": "sharedtype:conflict_1_space_2",
+ "id": "sharedtype:conflict_1a_space_2",
"index": ".kibana",
"source": {
- "originId": "conflict_1",
+ "originId": "conflict_1a",
"sharedtype": {
- "title": "A shared saved-object in one space"
+ "title": "This is used to test an inexact match conflict for an originId -> originId match"
+ },
+ "type": "sharedtype",
+ "namespaces": ["space_2"],
+ "updated_at": "2017-09-21T18:59:16.270Z"
+ },
+ "type": "doc"
+ }
+}
+
+{
+ "type": "doc",
+ "value": {
+ "id": "sharedtype:conflict_1b_default",
+ "index": ".kibana",
+ "source": {
+ "originId": "conflict_1b_space_2",
+ "sharedtype": {
+ "title": "This is used to test an inexact match conflict for an originId -> id match"
+ },
+ "type": "sharedtype",
+ "namespaces": ["default"],
+ "updated_at": "2017-09-21T18:59:16.270Z"
+ },
+ "type": "doc"
+ }
+}
+
+{
+ "type": "doc",
+ "value": {
+ "id": "sharedtype:conflict_1b_space_1",
+ "index": ".kibana",
+ "source": {
+ "originId": "conflict_1b_space_2",
+ "sharedtype": {
+ "title": "This is used to test an inexact match conflict for an originId -> id match"
+ },
+ "type": "sharedtype",
+ "namespaces": ["space_1"],
+ "updated_at": "2017-09-21T18:59:16.270Z"
+ },
+ "type": "doc"
+ }
+}
+
+{
+ "type": "doc",
+ "value": {
+ "id": "sharedtype:conflict_1b_space_2",
+ "index": ".kibana",
+ "source": {
+ "sharedtype": {
+ "title": "This is used to test an inexact match conflict for an originId -> id match"
+ },
+ "type": "sharedtype",
+ "namespaces": ["space_2"],
+ "updated_at": "2017-09-21T18:59:16.270Z"
+ },
+ "type": "doc"
+ }
+}
+
+{
+ "type": "doc",
+ "value": {
+ "id": "sharedtype:conflict_1c_default_and_space_1",
+ "index": ".kibana",
+ "source": {
+ "sharedtype": {
+ "title": "This is used to test an inexact match conflict for an id -> originId match"
+ },
+ "type": "sharedtype",
+ "namespaces": ["default", "space_1"],
+ "updated_at": "2017-09-21T18:59:16.270Z"
+ },
+ "type": "doc"
+ }
+}
+
+{
+ "type": "doc",
+ "value": {
+ "id": "sharedtype:conflict_1c_space_2",
+ "index": ".kibana",
+ "source": {
+ "originId": "conflict_1c_default_and_space_1",
+ "sharedtype": {
+ "title": "This is used to test an inexact match conflict for an id -> originId match"
},
"type": "sharedtype",
"namespaces": ["space_2"],
diff --git a/x-pack/test/spaces_api_integration/common/suites/copy_to_space.ts b/x-pack/test/spaces_api_integration/common/suites/copy_to_space.ts
index d0f83bb6574ef4..8ade75e90b541a 100644
--- a/x-pack/test/spaces_api_integration/common/suites/copy_to_space.ts
+++ b/x-pack/test/spaces_api_integration/common/suites/copy_to_space.ts
@@ -482,7 +482,9 @@ export function copyToSpaceTestSuiteFactory(
const type = 'sharedtype';
const noConflictId = `${spaceId}_only`;
const exactMatchId = 'each_space';
- const inexactMatchId = `conflict_1_${spaceId}`;
+ const inexactMatchIdA = `conflict_1a_${spaceId}`;
+ const inexactMatchIdB = `conflict_1b_${spaceId}`;
+ const inexactMatchIdC = `conflict_1c_default_and_space_1`;
const ambiguousConflictId = `conflict_2_${spaceId}`;
const getResult = (response: TestResponse) => (response.body as CopyResponse).space_2;
@@ -560,22 +562,108 @@ export function copyToSpaceTestSuiteFactory(
},
},
{
- testTitle: 'copying with an inexact match conflict',
- objects: [{ type, id: inexactMatchId }],
+ testTitle:
+ 'copying with an inexact match conflict (a) - originId matches existing originId',
+ objects: [{ type, id: inexactMatchIdA }],
statusCode,
response: async (response: TestResponse) => {
if (outcome === 'authorized') {
const { success, successCount, successResults, errors } = getResult(response);
- const title = 'A shared saved-object in one space';
+ const title =
+ 'This is used to test an inexact match conflict for an originId -> originId match';
+ const meta = { title, icon: 'beaker' };
+ const destinationId = 'conflict_1a_space_2';
+ if (createNewCopies) {
+ expectNewCopyResponse(response, inexactMatchIdA, title);
+ } else if (overwrite) {
+ expect(success).to.eql(true);
+ expect(successCount).to.eql(1);
+ expect(successResults).to.eql([
+ { type, id: inexactMatchIdA, meta, overwrite: true, destinationId },
+ ]);
+ expect(errors).to.be(undefined);
+ } else {
+ expect(success).to.eql(false);
+ expect(successCount).to.eql(0);
+ expect(successResults).to.be(undefined);
+ expect(errors).to.eql([
+ {
+ error: { type: 'conflict', destinationId },
+ type,
+ id: inexactMatchIdA,
+ title,
+ meta,
+ },
+ ]);
+ }
+ } else if (outcome === 'noAccess') {
+ expectRouteForbiddenResponse(response);
+ } else {
+ // unauthorized read/write
+ expectSavedObjectForbiddenResponse(response);
+ }
+ },
+ },
+ {
+ testTitle: 'copying with an inexact match conflict (b) - originId matches existing id',
+ objects: [{ type, id: inexactMatchIdB }],
+ statusCode,
+ response: async (response: TestResponse) => {
+ if (outcome === 'authorized') {
+ const { success, successCount, successResults, errors } = getResult(response);
+ const title =
+ 'This is used to test an inexact match conflict for an originId -> id match';
+ const meta = { title, icon: 'beaker' };
+ const destinationId = 'conflict_1b_space_2';
+ if (createNewCopies) {
+ expectNewCopyResponse(response, inexactMatchIdB, title);
+ } else if (overwrite) {
+ expect(success).to.eql(true);
+ expect(successCount).to.eql(1);
+ expect(successResults).to.eql([
+ { type, id: inexactMatchIdB, meta, overwrite: true, destinationId },
+ ]);
+ expect(errors).to.be(undefined);
+ } else {
+ expect(success).to.eql(false);
+ expect(successCount).to.eql(0);
+ expect(successResults).to.be(undefined);
+ expect(errors).to.eql([
+ {
+ error: { type: 'conflict', destinationId },
+ type,
+ id: inexactMatchIdB,
+ title,
+ meta,
+ },
+ ]);
+ }
+ } else if (outcome === 'noAccess') {
+ expectRouteForbiddenResponse(response);
+ } else {
+ // unauthorized read/write
+ expectSavedObjectForbiddenResponse(response);
+ }
+ },
+ },
+ {
+ testTitle: 'copying with an inexact match conflict (c) - id matches existing originId',
+ objects: [{ type, id: inexactMatchIdC }],
+ statusCode,
+ response: async (response: TestResponse) => {
+ if (outcome === 'authorized') {
+ const { success, successCount, successResults, errors } = getResult(response);
+ const title =
+ 'This is used to test an inexact match conflict for an id -> originId match';
const meta = { title, icon: 'beaker' };
- const destinationId = 'conflict_1_space_2';
+ const destinationId = 'conflict_1c_space_2';
if (createNewCopies) {
- expectNewCopyResponse(response, inexactMatchId, title);
+ expectNewCopyResponse(response, inexactMatchIdC, title);
} else if (overwrite) {
expect(success).to.eql(true);
expect(successCount).to.eql(1);
expect(successResults).to.eql([
- { type, id: inexactMatchId, meta, overwrite: true, destinationId },
+ { type, id: inexactMatchIdC, meta, overwrite: true, destinationId },
]);
expect(errors).to.be(undefined);
} else {
@@ -586,7 +674,7 @@ export function copyToSpaceTestSuiteFactory(
{
error: { type: 'conflict', destinationId },
type,
- id: inexactMatchId,
+ id: inexactMatchIdC,
title,
meta,
},
diff --git a/x-pack/test/spaces_api_integration/common/suites/delete.ts b/x-pack/test/spaces_api_integration/common/suites/delete.ts
index ae8b73535c2c65..84f899bb911e55 100644
--- a/x-pack/test/spaces_api_integration/common/suites/delete.ts
+++ b/x-pack/test/spaces_api_integration/common/suites/delete.ts
@@ -101,7 +101,7 @@ export function deleteTestSuiteFactory(es: Client, esArchiver: any, supertest: S
expect(buckets).to.eql(expectedBuckets);
- // There were 15 multi-namespace objects.
+ // There were 22 multi-namespace objects.
// Since Space 2 was deleted, any multi-namespace objects that existed in that space
// are updated to remove it, and of those, any that don't exist in any space are deleted.
const multiNamespaceResponse = await es.search>({
@@ -110,8 +110,8 @@ export function deleteTestSuiteFactory(es: Client, esArchiver: any, supertest: S
body: { query: { terms: { type: ['sharedtype'] } } },
});
const docs = multiNamespaceResponse.hits.hits;
- // Just 14 results, since spaces_2_only, conflict_1_space_2 and conflict_2_space_2 got deleted.
- expect(docs).length(14);
+ // Just 17 results, since spaces_2_only, conflict_1a_space_2, conflict_1b_space_2, conflict_1c_space_2, and conflict_2_space_2 got deleted.
+ expect(docs).length(17);
docs.forEach((doc) => () => {
const containsSpace2 = doc?._source?.namespaces.includes('space_2');
expect(containsSpace2).to.eql(false);
diff --git a/x-pack/test/spaces_api_integration/common/suites/resolve_copy_to_space_conflicts.ts b/x-pack/test/spaces_api_integration/common/suites/resolve_copy_to_space_conflicts.ts
index 1d9d5325cbabf5..336b04832e2dc5 100644
--- a/x-pack/test/spaces_api_integration/common/suites/resolve_copy_to_space_conflicts.ts
+++ b/x-pack/test/spaces_api_integration/common/suites/resolve_copy_to_space_conflicts.ts
@@ -323,7 +323,9 @@ export function resolveCopyToSpaceConflictsSuite(
const statusCode = outcome === 'noAccess' ? 403 : 200;
const type = 'sharedtype';
const exactMatchId = 'each_space';
- const inexactMatchId = `conflict_1_${spaceId}`;
+ const inexactMatchIdA = `conflict_1a_${spaceId}`;
+ const inexactMatchIdB = `conflict_1b_${spaceId}`;
+ const inexactMatchIdC = `conflict_1c_default_and_space_1`;
const ambiguousConflictId = `conflict_2_${spaceId}`;
const createRetries = (overwriteRetry: Record) => ({
@@ -350,10 +352,20 @@ export function resolveCopyToSpaceConflictsSuite(
expect(success).to.eql(true);
expect(successCount).to.eql(1);
expect(errors).to.be(undefined);
- const title =
- id === exactMatchId
- ? 'A shared saved-object in the default, space_1, and space_2 spaces'
- : 'A shared saved-object in one space';
+ const title = (() => {
+ switch (id) {
+ case exactMatchId:
+ return 'A shared saved-object in the default, space_1, and space_2 spaces';
+ case inexactMatchIdA:
+ return 'This is used to test an inexact match conflict for an originId -> originId match';
+ case inexactMatchIdB:
+ return 'This is used to test an inexact match conflict for an originId -> id match';
+ case inexactMatchIdC:
+ return 'This is used to test an inexact match conflict for an id -> originId match';
+ default:
+ return 'A shared saved-object in one space';
+ }
+ })();
const meta = { title, icon: 'beaker' };
expect(successResults).to.eql([
{ type, id, meta, overwrite: true, ...(destinationId && { destinationId }) },
@@ -378,18 +390,61 @@ export function resolveCopyToSpaceConflictsSuite(
},
},
{
- testTitle: 'copying with an inexact match conflict',
- objects: [{ type, id: inexactMatchId }],
+ testTitle:
+ 'copying with an inexact match conflict (a) - originId matches existing originId',
+ objects: [{ type, id: inexactMatchIdA }],
retries: createRetries({
type,
- id: inexactMatchId,
+ id: inexactMatchIdA,
overwrite: true,
- destinationId: 'conflict_1_space_2',
+ destinationId: 'conflict_1a_space_2',
}),
statusCode,
response: async (response: TestResponse) => {
if (outcome === 'authorized') {
- expectSavedObjectSuccessResponse(response, inexactMatchId, 'conflict_1_space_2');
+ expectSavedObjectSuccessResponse(response, inexactMatchIdA, 'conflict_1a_space_2');
+ } else if (outcome === 'noAccess') {
+ expectRouteForbiddenResponse(response);
+ } else {
+ // unauthorized read/write
+ expectSavedObjectForbiddenResponse(response);
+ }
+ },
+ },
+ {
+ testTitle: 'copying with an inexact match conflict (b) - originId matches existing id',
+ objects: [{ type, id: inexactMatchIdB }],
+ retries: createRetries({
+ type,
+ id: inexactMatchIdB,
+ overwrite: true,
+ destinationId: 'conflict_1b_space_2',
+ }),
+ statusCode,
+ response: async (response: TestResponse) => {
+ if (outcome === 'authorized') {
+ expectSavedObjectSuccessResponse(response, inexactMatchIdB, 'conflict_1b_space_2');
+ } else if (outcome === 'noAccess') {
+ expectRouteForbiddenResponse(response);
+ } else {
+ // unauthorized read/write
+ expectSavedObjectForbiddenResponse(response);
+ }
+ },
+ },
+ {
+ testTitle: 'copying with an inexact match conflict (c) - id matches existing originId',
+ objects: [{ type, id: inexactMatchIdC }],
+ retries: createRetries({
+ type,
+ id: inexactMatchIdC,
+ overwrite: true,
+ destinationId: 'conflict_1c_space_2',
+ }),
+ statusCode,
+ response: async (response: TestResponse) => {
+ if (outcome === 'authorized') {
+ expectSavedObjectSuccessResponse(response, inexactMatchIdC, 'conflict_1c_space_2');
} else if (outcome === 'noAccess') {
expectRouteForbiddenResponse(response);
} else {
diff --git a/x-pack/test/upgrade_assistant_integration/upgrade_assistant/reindexing.js b/x-pack/test/upgrade_assistant_integration/upgrade_assistant/reindexing.js
index af2393d7b00d1f..6a326840bc5510 100644
--- a/x-pack/test/upgrade_assistant_integration/upgrade_assistant/reindexing.js
+++ b/x-pack/test/upgrade_assistant_integration/upgrade_assistant/reindexing.js
@@ -34,7 +34,7 @@ export default function ({ getService }) {
return lastState;
};
- describe('reindexing', () => {
+ describe.skip('reindexing', () => {
afterEach(() => {
// Cleanup saved objects
return es.deleteByQuery({
diff --git a/yarn.lock b/yarn.lock
index 61d331ca5cb9f8..cc181351a3d96d 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1396,29 +1396,29 @@
dependencies:
tslib "^2.0.0"
-"@elastic/apm-rum-core@^5.13.0":
- version "5.13.0"
- resolved "https://registry.yarnpkg.com/@elastic/apm-rum-core/-/apm-rum-core-5.13.0.tgz#dbada016fc73c0be4d7df1ba835a1556dc48d21e"
- integrity sha512-5kSTbdmyLfpCdLgoy387y+WMCVl4YuYHdkFgDWAGfOBR+aaOCQAcQoLc8KK6+FZoN/vqvVSFCN+GxxyBc9Kmdw==
+"@elastic/apm-rum-core@^5.14.0":
+ version "5.14.0"
+ resolved "https://registry.yarnpkg.com/@elastic/apm-rum-core/-/apm-rum-core-5.14.0.tgz#b3eb2569b3bb3dc706f92e6ec830f45efef5e76c"
+ integrity sha512-AhmbApgdvfJGcZZD1RUIGQsFtWe89LN5fzMpse7yVqrhytKoSxaG3r1ap6xhHQIvTwHVIE3Bg8t1vviak1f4DQ==
dependencies:
error-stack-parser "^1.3.5"
opentracing "^0.14.3"
promise-polyfill "^8.1.3"
-"@elastic/apm-rum-react@^1.3.2":
- version "1.3.2"
- resolved "https://registry.yarnpkg.com/@elastic/apm-rum-react/-/apm-rum-react-1.3.2.tgz#682dbf040aad4a6d7c423d0da81cc9e65a910693"
- integrity sha512-ohSgd8wXPziJQpaixvbNAHL6/sMLBW+iOrxCFvCKF9gRllowUTqYi+etery96Hq0X8yXC+/fJg18ZXx9wlJLEQ==
+"@elastic/apm-rum-react@^1.3.3":
+ version "1.3.3"
+ resolved "https://registry.yarnpkg.com/@elastic/apm-rum-react/-/apm-rum-react-1.3.3.tgz#99ba436ecbbc9332c98fba402c0f602de1bc747b"
+ integrity sha512-jrI29O1xRtiQov0OMuUSgM157RlcBDglxOhkkPxxzAXDW8NGZ/s3qAVwN/xoVNuhdIPRiRU2+ooODs3QPTfj/Q==
dependencies:
- "@elastic/apm-rum" "^5.10.0"
+ "@elastic/apm-rum" "^5.10.1"
hoist-non-react-statics "^3.3.0"
-"@elastic/apm-rum@^5.10.0":
- version "5.10.0"
- resolved "https://registry.yarnpkg.com/@elastic/apm-rum/-/apm-rum-5.10.0.tgz#ffadc50e53d7b7bc9f1c7851f7d70631c3e14eba"
- integrity sha512-Aw3UwiduxNfJ0/S3Uq0cO8O+60RmEMa9AJGq6v8fFQ8UdnTV1IgT73NpPyMLtn3qBcKyJNKpvx0jW28w5IVLcQ==
+"@elastic/apm-rum@^5.10.1":
+ version "5.10.1"
+ resolved "https://registry.yarnpkg.com/@elastic/apm-rum/-/apm-rum-5.10.1.tgz#d907f50799b7c5d61ef292558784d55eaafb109f"
+ integrity sha512-UtYjzmg6A2dpeU4mue/z75yvJSFhbBlngdz95rXJhPKLKXJWiUpKWParfNOaH52B7H/UzFDoHTaButVkdGC7UA==
dependencies:
- "@elastic/apm-rum-core" "^5.13.0"
+ "@elastic/apm-rum-core" "^5.14.0"
"@elastic/apm-synthtrace@link:bazel-bin/packages/elastic-apm-synthtrace":
version "0.0.0"
@@ -5956,6 +5956,10 @@
version "0.0.0"
uid ""
+"@types/kbn__ui-shared-deps-npm@link:bazel-bin/packages/kbn-ui-shared-deps-npm/npm_module_types":
+ version "0.0.0"
+ uid ""
+
"@types/kbn__utility-types@link:bazel-bin/packages/kbn-utility-types/npm_module_types":
version "0.0.0"
uid ""
@@ -10526,7 +10530,7 @@ core-js@^2.4.0, core-js@^2.5.0, core-js@^2.6.9:
resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.9.tgz#6b4b214620c834152e179323727fc19741b084f2"
integrity sha512-HOpZf6eXmnl7la+cUdMnLvUxKNqLUzJvgIziQ0DiF3JwSImNphIqdGqzj6hIKyX04MmV0poclQ7+wjWvxQyR2A==
-core-js@^3.0.4, core-js@^3.6.5, core-js@^3.8.2, core-js@^3.8.3, core-js@^3.20.2:
+core-js@^3.0.4, core-js@^3.20.2, core-js@^3.6.5, core-js@^3.8.2, core-js@^3.8.3:
version "3.20.2"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.20.2.tgz#46468d8601eafc8b266bd2dd6bf9dee622779581"
integrity sha512-nuqhq11DcOAbFBV4zCbKeGbKQsUDRqTX0oqx7AttUBuqe3h20ixsE039QHelbL6P4h+9kytVqyEtyZ6gsiwEYw==