diff --git a/docs/user/discover.asciidoc b/docs/user/discover.asciidoc
index d60ccb2ccd0de..ddd06b06c9cd8 100644
--- a/docs/user/discover.asciidoc
+++ b/docs/user/discover.asciidoc
@@ -334,6 +334,11 @@ For more about this and other rules provided in {alert-features}, go to <> to better meet your needs.
+[float]
+=== Troubleshooting
+
+* {blog-ref}troubleshooting-guide-common-issues-kibana-discover-load[Learn how to resolve common issues with Discover.]
+
--
include::{kibana-root}/docs/discover/document-explorer.asciidoc[]
diff --git a/packages/core/plugins/core-plugins-server-internal/src/plugins_system.test.ts b/packages/core/plugins/core-plugins-server-internal/src/plugins_system.test.ts
index 1ddc4c08f915c..e99ed47ca43f0 100644
--- a/packages/core/plugins/core-plugins-server-internal/src/plugins_system.test.ts
+++ b/packages/core/plugins/core-plugins-server-internal/src/plugins_system.test.ts
@@ -128,7 +128,7 @@ test('getPlugins returns the list of plugins', () => {
expect(pluginsSystem.getPlugins()).toEqual([pluginA, pluginB]);
});
-test('getPluginDependencies returns dependency tree of symbols', () => {
+test('getPluginDependencies returns dependency tree with keys topologically sorted', () => {
pluginsSystem.addPlugin(createPlugin('plugin-a', { required: ['no-dep'] }));
pluginsSystem.addPlugin(
createPlugin('plugin-b', { required: ['plugin-a'], optional: ['no-dep', 'other'] })
@@ -138,6 +138,7 @@ test('getPluginDependencies returns dependency tree of symbols', () => {
expect(pluginsSystem.getPluginDependencies()).toMatchInlineSnapshot(`
Object {
"asNames": Map {
+ "no-dep" => Array [],
"plugin-a" => Array [
"no-dep",
],
@@ -145,9 +146,9 @@ test('getPluginDependencies returns dependency tree of symbols', () => {
"plugin-a",
"no-dep",
],
- "no-dep" => Array [],
},
"asOpaqueIds": Map {
+ Symbol(no-dep) => Array [],
Symbol(plugin-a) => Array [
Symbol(no-dep),
],
@@ -155,7 +156,6 @@ test('getPluginDependencies returns dependency tree of symbols', () => {
Symbol(plugin-a),
Symbol(no-dep),
],
- Symbol(no-dep) => Array [],
},
}
`);
diff --git a/packages/core/plugins/core-plugins-server-internal/src/plugins_system.ts b/packages/core/plugins/core-plugins-server-internal/src/plugins_system.ts
index a47c82f73aca7..a929044cd6037 100644
--- a/packages/core/plugins/core-plugins-server-internal/src/plugins_system.ts
+++ b/packages/core/plugins/core-plugins-server-internal/src/plugins_system.ts
@@ -34,6 +34,7 @@ export class PluginsSystem {
private readonly log: Logger;
// `satup`, the past-tense version of the noun `setup`.
private readonly satupPlugins: PluginName[] = [];
+ private sortedPluginNames?: Set;
constructor(private readonly coreContext: CoreContext, public readonly type: T) {
this.log = coreContext.logger.get('plugins-system', this.type);
@@ -47,6 +48,9 @@ export class PluginsSystem {
}
this.plugins.set(plugin.name, plugin);
+
+ // clear sorted plugin name cache on addition
+ this.sortedPluginNames = undefined;
}
public getPlugins() {
@@ -54,32 +58,31 @@ export class PluginsSystem {
}
/**
- * @returns a ReadonlyMap of each plugin and an Array of its available dependencies
+ * @returns a Map of each plugin and an Array of its available dependencies
* @internal
*/
public getPluginDependencies(): PluginDependencies {
- const asNames = new Map(
- [...this.plugins].map(([name, plugin]) => [
+ const asNames = new Map();
+ const asOpaqueIds = new Map();
+
+ for (const pluginName of this.getTopologicallySortedPluginNames()) {
+ const plugin = this.plugins.get(pluginName)!;
+ const dependencies = [
+ ...new Set([
+ ...plugin.requiredPlugins,
+ ...plugin.optionalPlugins.filter((optPlugin) => this.plugins.has(optPlugin)),
+ ]),
+ ];
+
+ asNames.set(
plugin.name,
- [
- ...new Set([
- ...plugin.requiredPlugins,
- ...plugin.optionalPlugins.filter((optPlugin) => this.plugins.has(optPlugin)),
- ]),
- ].map((depId) => this.plugins.get(depId)!.name),
- ])
- );
- const asOpaqueIds = new Map(
- [...this.plugins].map(([name, plugin]) => [
+ dependencies.map((depId) => this.plugins.get(depId)!.name)
+ );
+ asOpaqueIds.set(
plugin.opaqueId,
- [
- ...new Set([
- ...plugin.requiredPlugins,
- ...plugin.optionalPlugins.filter((optPlugin) => this.plugins.has(optPlugin)),
- ]),
- ].map((depId) => this.plugins.get(depId)!.opaqueId),
- ])
- );
+ dependencies.map((depId) => this.plugins.get(depId)!.opaqueId)
+ );
+ }
return { asNames, asOpaqueIds };
}
@@ -298,67 +301,74 @@ export class PluginsSystem {
return publicPlugins;
}
- /**
- * Gets topologically sorted plugin names that are registered with the plugin system.
- * Ordering is possible if and only if the plugins graph has no directed cycles,
- * that is, if it is a directed acyclic graph (DAG). If plugins cannot be ordered
- * an error is thrown.
- *
- * Uses Kahn's Algorithm to sort the graph.
- */
private getTopologicallySortedPluginNames() {
- // We clone plugins so we can remove handled nodes while we perform the
- // topological ordering. If the cloned graph is _not_ empty at the end, we
- // know we were not able to topologically order the graph. We exclude optional
- // dependencies that are not present in the plugins graph.
- const pluginsDependenciesGraph = new Map(
- [...this.plugins.entries()].map(([pluginName, plugin]) => {
- return [
- pluginName,
- new Set([
- ...plugin.requiredPlugins,
- ...plugin.optionalPlugins.filter((dependency) => this.plugins.has(dependency)),
- ]),
- ] as [PluginName, Set];
- })
- );
-
- // First, find a list of "start nodes" which have no outgoing edges. At least
- // one such node must exist in a non-empty acyclic graph.
- const pluginsWithAllDependenciesSorted = [...pluginsDependenciesGraph.keys()].filter(
- (pluginName) => pluginsDependenciesGraph.get(pluginName)!.size === 0
- );
-
- const sortedPluginNames = new Set();
- while (pluginsWithAllDependenciesSorted.length > 0) {
- const sortedPluginName = pluginsWithAllDependenciesSorted.pop()!;
-
- // We know this plugin has all its dependencies sorted, so we can remove it
- // and include into the final result.
- pluginsDependenciesGraph.delete(sortedPluginName);
- sortedPluginNames.add(sortedPluginName);
-
- // Go through the rest of the plugins and remove `sortedPluginName` from their
- // unsorted dependencies.
- for (const [pluginName, dependencies] of pluginsDependenciesGraph) {
- // If we managed delete `sortedPluginName` from dependencies let's check
- // whether it was the last one and we can mark plugin as sorted.
- if (dependencies.delete(sortedPluginName) && dependencies.size === 0) {
- pluginsWithAllDependenciesSorted.push(pluginName);
- }
- }
+ if (!this.sortedPluginNames) {
+ this.sortedPluginNames = getTopologicallySortedPluginNames(this.plugins);
}
+ return this.sortedPluginNames;
+ }
+}
- if (pluginsDependenciesGraph.size > 0) {
- const edgesLeft = JSON.stringify([...pluginsDependenciesGraph.keys()]);
- throw new Error(
- `Topological ordering of plugins did not complete, these plugins have cyclic or missing dependencies: ${edgesLeft}`
- );
+/**
+ * Gets topologically sorted plugin names that are registered with the plugin system.
+ * Ordering is possible if and only if the plugins graph has no directed cycles,
+ * that is, if it is a directed acyclic graph (DAG). If plugins cannot be ordered
+ * an error is thrown.
+ *
+ * Uses Kahn's Algorithm to sort the graph.
+ */
+const getTopologicallySortedPluginNames = (plugins: Map) => {
+ // We clone plugins so we can remove handled nodes while we perform the
+ // topological ordering. If the cloned graph is _not_ empty at the end, we
+ // know we were not able to topologically order the graph. We exclude optional
+ // dependencies that are not present in the plugins graph.
+ const pluginsDependenciesGraph = new Map(
+ [...plugins.entries()].map(([pluginName, plugin]) => {
+ return [
+ pluginName,
+ new Set([
+ ...plugin.requiredPlugins,
+ ...plugin.optionalPlugins.filter((dependency) => plugins.has(dependency)),
+ ]),
+ ] as [PluginName, Set];
+ })
+ );
+
+ // First, find a list of "start nodes" which have no outgoing edges. At least
+ // one such node must exist in a non-empty acyclic graph.
+ const pluginsWithAllDependenciesSorted = [...pluginsDependenciesGraph.keys()].filter(
+ (pluginName) => pluginsDependenciesGraph.get(pluginName)!.size === 0
+ );
+
+ const sortedPluginNames = new Set();
+ while (pluginsWithAllDependenciesSorted.length > 0) {
+ const sortedPluginName = pluginsWithAllDependenciesSorted.pop()!;
+
+ // We know this plugin has all its dependencies sorted, so we can remove it
+ // and include into the final result.
+ pluginsDependenciesGraph.delete(sortedPluginName);
+ sortedPluginNames.add(sortedPluginName);
+
+ // Go through the rest of the plugins and remove `sortedPluginName` from their
+ // unsorted dependencies.
+ for (const [pluginName, dependencies] of pluginsDependenciesGraph) {
+ // If we managed delete `sortedPluginName` from dependencies let's check
+ // whether it was the last one and we can mark plugin as sorted.
+ if (dependencies.delete(sortedPluginName) && dependencies.size === 0) {
+ pluginsWithAllDependenciesSorted.push(pluginName);
+ }
}
+ }
- return sortedPluginNames;
+ if (pluginsDependenciesGraph.size > 0) {
+ const edgesLeft = JSON.stringify([...pluginsDependenciesGraph.keys()]);
+ throw new Error(
+ `Topological ordering of plugins did not complete, these plugins have cyclic or missing dependencies: ${edgesLeft}`
+ );
}
-}
+
+ return sortedPluginNames;
+};
const buildReverseDependencyMap = (
pluginMap: Map
diff --git a/packages/core/plugins/core-plugins-server-internal/src/types.ts b/packages/core/plugins/core-plugins-server-internal/src/types.ts
index def1a27a4c26f..195b4910f71dd 100644
--- a/packages/core/plugins/core-plugins-server-internal/src/types.ts
+++ b/packages/core/plugins/core-plugins-server-internal/src/types.ts
@@ -10,6 +10,16 @@ import type { PluginName, PluginOpaqueId } from '@kbn/core-base-common';
/** @internal */
export interface PluginDependencies {
+ /**
+ * Plugin to dependencies map with plugin names as key/values.
+ *
+ * Keys sorted by plugin topological order (root plugins first, leaf plugins last).
+ */
asNames: ReadonlyMap;
+ /**
+ * Plugin to dependencies map with plugin opaque ids as key/values.
+ *
+ * Keys sorted by plugin topological order (root plugins first, leaf plugins last).
+ */
asOpaqueIds: ReadonlyMap;
}
diff --git a/packages/core/status/core-status-server-internal/src/plugins_status.ts b/packages/core/status/core-status-server-internal/src/plugins_status.ts
index 7de22e23cc97e..a34fb8a98d794 100644
--- a/packages/core/status/core-status-server-internal/src/plugins_status.ts
+++ b/packages/core/status/core-status-server-internal/src/plugins_status.ts
@@ -23,7 +23,6 @@ import {
takeUntil,
delay,
} from 'rxjs/operators';
-import { sortBy } from 'lodash';
import { isDeepStrictEqual } from 'util';
import type { PluginName } from '@kbn/core-base-common';
import { ServiceStatusLevels, type CoreStatus, type ServiceStatus } from '@kbn/core-status-common';
@@ -45,7 +44,6 @@ export interface Deps {
interface PluginData {
[name: PluginName]: {
name: PluginName;
- depth: number; // depth of this plugin in the dependency tree (root plugins will have depth = 1)
dependencies: PluginName[];
reverseDependencies: PluginName[];
reportedStatus?: PluginStatus;
@@ -81,7 +79,8 @@ export class PluginsStatusService {
constructor(deps: Deps, private readonly statusTimeoutMs: number = STATUS_TIMEOUT_MS) {
this.pluginData = this.initPluginData(deps.pluginDependencies);
this.rootPlugins = this.getRootPlugins();
- this.orderedPluginNames = this.getOrderedPluginNames();
+ // plugin dependencies keys are already sorted
+ this.orderedPluginNames = [...deps.pluginDependencies.keys()];
this.coreSubscription = deps.core$
.pipe(
@@ -216,23 +215,20 @@ export class PluginsStatusService {
private initPluginData(pluginDependencies: ReadonlyMap): PluginData {
const pluginData: PluginData = {};
- if (pluginDependencies) {
- pluginDependencies.forEach((dependencies, name) => {
- pluginData[name] = {
- name,
- depth: 0,
- dependencies,
- reverseDependencies: [],
- derivedStatus: defaultStatus,
- };
- });
+ pluginDependencies.forEach((dependencies, name) => {
+ pluginData[name] = {
+ name,
+ dependencies,
+ reverseDependencies: [],
+ derivedStatus: defaultStatus,
+ };
+ });
- pluginDependencies.forEach((dependencies, name) => {
- dependencies.forEach((dependency) => {
- pluginData[dependency].reverseDependencies.push(name);
- });
+ pluginDependencies.forEach((dependencies, name) => {
+ dependencies.forEach((dependency) => {
+ pluginData[dependency].reverseDependencies.push(name);
});
- }
+ });
return pluginData;
}
@@ -248,36 +244,6 @@ export class PluginsStatusService {
);
}
- /**
- * Obtain a list of plugins names, ordered by depth.
- * @see {calculateDepthRecursive}
- * @returns {PluginName[]} a list of plugins, ordered by depth + name
- */
- private getOrderedPluginNames(): PluginName[] {
- this.rootPlugins.forEach((plugin) => {
- this.calculateDepthRecursive(plugin, 1);
- });
-
- return sortBy(Object.values(this.pluginData), ['depth', 'name']).map(({ name }) => name);
- }
-
- /**
- * Calculate the depth of the given plugin, knowing that it's has at least the specified depth
- * The depth of a plugin is determined by how many levels of dependencies the plugin has above it.
- * We define root plugins as depth = 1, plugins that only depend on root plugins will have depth = 2
- * and so on so forth
- * @param {PluginName} plugin the name of the plugin whose depth must be calculated
- * @param {number} depth the minimum depth that we know for sure this plugin has
- */
- private calculateDepthRecursive(plugin: PluginName, depth: number): void {
- const pluginData = this.pluginData[plugin];
- pluginData.depth = Math.max(pluginData.depth, depth);
- const newDepth = depth + 1;
- pluginData.reverseDependencies.forEach((revDep) =>
- this.calculateDepthRecursive(revDep, newDepth)
- );
- }
-
/**
* Updates the root plugins statuses according to the current core services status
*/
diff --git a/packages/kbn-apm-config-loader/src/config.test.ts b/packages/kbn-apm-config-loader/src/config.test.ts
index ac139b220768c..caf3b800b444b 100644
--- a/packages/kbn-apm-config-loader/src/config.test.ts
+++ b/packages/kbn-apm-config-loader/src/config.test.ts
@@ -7,9 +7,9 @@
*/
import type { AgentConfigOptions, Labels } from 'elastic-apm-node';
import {
- packageMock,
- mockedRootDir,
gitRevExecMock,
+ mockedRootDir,
+ packageMock,
readUuidFileMock,
resetAllMocks,
} from './config.test.mocks';
@@ -152,6 +152,7 @@ describe('ApmConfiguration', () => {
delete process.env.ELASTIC_APM_SECRET_TOKEN;
delete process.env.ELASTIC_APM_API_KEY;
delete process.env.ELASTIC_APM_SERVER_URL;
+ delete process.env.ELASTIC_APM_GLOBAL_LABELS;
delete process.env.NODE_ENV;
});
@@ -184,6 +185,21 @@ describe('ApmConfiguration', () => {
})
);
});
+
+ it('ELASTIC_APM_GLOBAL_LABELS', () => {
+ process.env.ELASTIC_APM_GLOBAL_LABELS = 'test1=1,test2=2';
+ const config = new ApmConfiguration(mockedRootDir, {}, true);
+
+ expect(config.getConfig('serviceName')).toEqual(
+ expect.objectContaining({
+ globalLabels: {
+ git_rev: 'sha',
+ test1: '1',
+ test2: '2',
+ },
+ })
+ );
+ });
});
it('does not override the environment from NODE_ENV if already set in the config file', () => {
diff --git a/packages/kbn-apm-config-loader/src/config.ts b/packages/kbn-apm-config-loader/src/config.ts
index f25feafe90eb7..efe228be8157e 100644
--- a/packages/kbn-apm-config-loader/src/config.ts
+++ b/packages/kbn-apm-config-loader/src/config.ts
@@ -7,6 +7,7 @@
*/
import { join } from 'path';
+import deepmerge from 'deepmerge';
import { merge } from 'lodash';
import { execSync } from 'child_process';
import { getDataPath } from '@kbn/utils';
@@ -295,7 +296,10 @@ export class ApmConfiguration {
const { servicesOverrides, redactUsers, ...configFromKibanaConfig } =
this.getConfigFromKibanaConfig();
const configFromEnv = this.getConfigFromEnv(configFromKibanaConfig);
- const config = merge({}, configFromKibanaConfig, configFromEnv);
+ const config = [configFromKibanaConfig, configFromEnv].reduce(
+ (acc, conf) => deepmerge(acc, conf),
+ {}
+ );
if (config.active === false && config.contextPropagationOnly !== false) {
throw new Error(
diff --git a/x-pack/plugins/actions/server/application/connector/methods/get_all/get_all.test.ts b/x-pack/plugins/actions/server/application/connector/methods/get_all/get_all.test.ts
index 2e264300490f8..2032653712a59 100644
--- a/x-pack/plugins/actions/server/application/connector/methods/get_all/get_all.test.ts
+++ b/x-pack/plugins/actions/server/application/connector/methods/get_all/get_all.test.ts
@@ -10,7 +10,10 @@ import { ActionsAuthorization } from '../../../../authorization/actions_authoriz
import { connectorTokenClientMock } from '../../../../lib/connector_token_client.mock';
import { getOAuthJwtAccessToken } from '../../../../lib/get_oauth_jwt_access_token';
import { getOAuthClientCredentialsAccessToken } from '../../../../lib/get_oauth_client_credentials_access_token';
-import { savedObjectsClientMock } from '@kbn/core-saved-objects-api-server-mocks';
+import {
+ savedObjectsClientMock,
+ savedObjectsRepositoryMock,
+} from '@kbn/core-saved-objects-api-server-mocks';
import { actionsAuthorizationMock } from '../../../../authorization/actions_authorization.mock';
import { elasticsearchServiceMock } from '@kbn/core-elasticsearch-server-mocks';
import { actionExecutorMock } from '../../../../lib/action_executor.mock';
@@ -21,6 +24,7 @@ import { loggingSystemMock } from '@kbn/core-logging-server-mocks';
import { Logger } from '@kbn/logging';
import { eventLogClientMock } from '@kbn/event-log-plugin/server/event_log_client.mock';
import { ActionTypeRegistry } from '../../../../action_type_registry';
+import { getAllUnsecured } from './get_all';
jest.mock('@kbn/core-saved-objects-utils-server', () => {
const actual = jest.requireActual('@kbn/core-saved-objects-utils-server');
@@ -77,6 +81,7 @@ const logger = loggingSystemMock.create().get() as jest.Mocked;
const eventLogClient = eventLogClientMock.create();
const getEventLogClient = jest.fn();
const connectorTokenClient = connectorTokenClientMock.create();
+const internalSavedObjectsRepository = savedObjectsRepositoryMock.create();
let actionsClient: ActionsClient;
let actionTypeRegistry: ActionTypeRegistry;
@@ -551,3 +556,447 @@ describe('getAll()', () => {
);
});
});
+
+describe('getAllUnsecured()', () => {
+ beforeEach(() => {
+ jest.resetAllMocks();
+ jest.clearAllMocks();
+ });
+
+ test('calls internalSavedObjectRepository with parameters and returns inMemoryConnectors correctly', async () => {
+ const expectedResult = {
+ total: 1,
+ per_page: 10,
+ page: 1,
+ saved_objects: [
+ {
+ id: '1',
+ type: 'type',
+ attributes: {
+ name: 'test',
+ isMissingSecrets: false,
+ config: {
+ foo: 'bar',
+ },
+ secrets: 'this should not be returned',
+ },
+ score: 1,
+ references: [],
+ },
+ ],
+ };
+ internalSavedObjectsRepository.find.mockResolvedValueOnce(expectedResult);
+ scopedClusterClient.asInternalUser.search.mockResponse(
+ // @ts-expect-error not full search response
+ {
+ aggregations: {
+ '1': { doc_count: 6 },
+ testPreconfigured: { doc_count: 2 },
+ 'system-connector-.cases': { doc_count: 2 },
+ },
+ }
+ );
+
+ const result = await getAllUnsecured({
+ esClient: scopedClusterClient.asInternalUser,
+ inMemoryConnectors: [
+ {
+ id: 'testPreconfigured',
+ actionTypeId: '.slack',
+ secrets: {},
+ isPreconfigured: true,
+ isDeprecated: false,
+ isSystemAction: false,
+ name: 'test',
+ config: {
+ foo: 'bar',
+ },
+ },
+ /**
+ * System actions will not
+ * be returned from getAllUnsecured
+ */
+ {
+ id: 'system-connector-.cases',
+ actionTypeId: '.cases',
+ name: 'System action: .cases',
+ config: {},
+ secrets: {},
+ isDeprecated: false,
+ isMissingSecrets: false,
+ isPreconfigured: false,
+ isSystemAction: true,
+ },
+ ],
+ internalSavedObjectsRepository,
+ kibanaIndices,
+ logger,
+ spaceId: 'default',
+ });
+
+ expect(result).toEqual([
+ {
+ id: '1',
+ name: 'test',
+ isMissingSecrets: false,
+ config: { foo: 'bar' },
+ isPreconfigured: false,
+ isDeprecated: false,
+ isSystemAction: false,
+ referencedByCount: 6,
+ },
+ {
+ id: 'testPreconfigured',
+ actionTypeId: '.slack',
+ name: 'test',
+ isPreconfigured: true,
+ isSystemAction: false,
+ isDeprecated: false,
+ referencedByCount: 2,
+ },
+ ]);
+
+ expect(internalSavedObjectsRepository.find).toHaveBeenCalledWith({
+ perPage: 10000,
+ type: 'action',
+ });
+
+ expect(scopedClusterClient.asInternalUser.search).toHaveBeenCalledWith({
+ index: kibanaIndices,
+ ignore_unavailable: true,
+ body: {
+ aggs: {
+ '1': {
+ filter: {
+ bool: {
+ must: {
+ nested: {
+ path: 'references',
+ query: {
+ bool: {
+ filter: {
+ bool: {
+ must: [
+ {
+ term: {
+ 'references.id': '1',
+ },
+ },
+ {
+ term: {
+ 'references.type': 'action',
+ },
+ },
+ ],
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ testPreconfigured: {
+ filter: {
+ bool: {
+ must: {
+ nested: {
+ path: 'references',
+ query: {
+ bool: {
+ filter: {
+ bool: {
+ must: [
+ {
+ term: {
+ 'references.id': 'testPreconfigured',
+ },
+ },
+ {
+ term: {
+ 'references.type': 'action',
+ },
+ },
+ ],
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ size: 0,
+ query: {
+ match_all: {},
+ },
+ },
+ });
+
+ expect(auditLogger.log).not.toHaveBeenCalled();
+ expect(authorization.ensureAuthorized).not.toHaveBeenCalled();
+ });
+
+ test('passed custom space id if defined', async () => {
+ const expectedResult = {
+ total: 1,
+ per_page: 10,
+ page: 1,
+ saved_objects: [
+ {
+ id: '1',
+ type: 'type',
+ attributes: {
+ name: 'test',
+ isMissingSecrets: false,
+ config: {
+ foo: 'bar',
+ },
+ secrets: 'this should not be returned',
+ },
+ score: 1,
+ references: [],
+ },
+ ],
+ };
+ internalSavedObjectsRepository.find.mockResolvedValueOnce(expectedResult);
+ scopedClusterClient.asInternalUser.search.mockResponse(
+ // @ts-expect-error not full search response
+ {
+ aggregations: {
+ '1': { doc_count: 6 },
+ testPreconfigured: { doc_count: 2 },
+ 'system-connector-.cases': { doc_count: 2 },
+ },
+ }
+ );
+
+ const result = await getAllUnsecured({
+ esClient: scopedClusterClient.asInternalUser,
+ inMemoryConnectors: [
+ {
+ id: 'testPreconfigured',
+ actionTypeId: '.slack',
+ secrets: {},
+ isPreconfigured: true,
+ isDeprecated: false,
+ isSystemAction: false,
+ name: 'test',
+ config: {
+ foo: 'bar',
+ },
+ },
+ /**
+ * System actions will not
+ * be returned from getAllUnsecured
+ */
+ {
+ id: 'system-connector-.cases',
+ actionTypeId: '.cases',
+ name: 'System action: .cases',
+ config: {},
+ secrets: {},
+ isDeprecated: false,
+ isMissingSecrets: false,
+ isPreconfigured: false,
+ isSystemAction: true,
+ },
+ ],
+ internalSavedObjectsRepository,
+ kibanaIndices,
+ logger,
+ spaceId: 'custom',
+ });
+
+ expect(result).toEqual([
+ {
+ id: '1',
+ name: 'test',
+ isMissingSecrets: false,
+ config: { foo: 'bar' },
+ isPreconfigured: false,
+ isDeprecated: false,
+ isSystemAction: false,
+ referencedByCount: 6,
+ },
+ {
+ id: 'testPreconfigured',
+ actionTypeId: '.slack',
+ name: 'test',
+ isPreconfigured: true,
+ isSystemAction: false,
+ isDeprecated: false,
+ referencedByCount: 2,
+ },
+ ]);
+
+ expect(internalSavedObjectsRepository.find).toHaveBeenCalledWith({
+ perPage: 10000,
+ type: 'action',
+ namespaces: ['custom'],
+ });
+
+ expect(scopedClusterClient.asInternalUser.search).toHaveBeenCalledWith({
+ index: kibanaIndices,
+ ignore_unavailable: true,
+ body: {
+ aggs: {
+ '1': {
+ filter: {
+ bool: {
+ must: {
+ nested: {
+ path: 'references',
+ query: {
+ bool: {
+ filter: {
+ bool: {
+ must: [
+ {
+ term: {
+ 'references.id': '1',
+ },
+ },
+ {
+ term: {
+ 'references.type': 'action',
+ },
+ },
+ ],
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ testPreconfigured: {
+ filter: {
+ bool: {
+ must: {
+ nested: {
+ path: 'references',
+ query: {
+ bool: {
+ filter: {
+ bool: {
+ must: [
+ {
+ term: {
+ 'references.id': 'testPreconfigured',
+ },
+ },
+ {
+ term: {
+ 'references.type': 'action',
+ },
+ },
+ ],
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ size: 0,
+ query: {
+ match_all: {},
+ },
+ },
+ });
+
+ expect(auditLogger.log).not.toHaveBeenCalled();
+ expect(authorization.ensureAuthorized).not.toHaveBeenCalled();
+ });
+
+ test('validates connectors before return', async () => {
+ internalSavedObjectsRepository.find.mockResolvedValueOnce({
+ total: 1,
+ per_page: 10,
+ page: 1,
+ saved_objects: [
+ {
+ id: '1',
+ type: 'type',
+ attributes: {
+ name: 'test',
+ isMissingSecrets: false,
+ config: {
+ foo: 'bar',
+ },
+ },
+ score: 1,
+ references: [],
+ },
+ ],
+ });
+ scopedClusterClient.asInternalUser.search.mockResponse(
+ // @ts-expect-error not full search response
+ {
+ aggregations: {
+ '1': { doc_count: 6 },
+ testPreconfigured: { doc_count: 2 },
+ },
+ }
+ );
+
+ const result = await getAllUnsecured({
+ esClient: scopedClusterClient.asInternalUser,
+ inMemoryConnectors: [
+ {
+ id: 'testPreconfigured',
+ actionTypeId: '.slack',
+ secrets: {},
+ isPreconfigured: true,
+ isDeprecated: false,
+ isSystemAction: false,
+ name: 'test',
+ config: {
+ foo: 'bar',
+ },
+ },
+ ],
+ internalSavedObjectsRepository,
+ kibanaIndices,
+ logger,
+ spaceId: 'default',
+ });
+
+ expect(result).toEqual([
+ {
+ config: {
+ foo: 'bar',
+ },
+ id: '1',
+ isDeprecated: false,
+ isMissingSecrets: false,
+ isPreconfigured: false,
+ isSystemAction: false,
+ name: 'test',
+ referencedByCount: 6,
+ },
+ {
+ actionTypeId: '.slack',
+ id: 'testPreconfigured',
+ isDeprecated: false,
+ isPreconfigured: true,
+ isSystemAction: false,
+ name: 'test',
+ referencedByCount: 2,
+ },
+ ]);
+
+ expect(logger.warn).toHaveBeenCalledWith(
+ 'Error validating connector: 1, Error: [actionTypeId]: expected value of type [string] but got [undefined]'
+ );
+ });
+});
diff --git a/x-pack/plugins/actions/server/application/connector/methods/get_all/get_all.ts b/x-pack/plugins/actions/server/application/connector/methods/get_all/get_all.ts
index 8d764a9c632e0..9c3b9c13924fd 100644
--- a/x-pack/plugins/actions/server/application/connector/methods/get_all/get_all.ts
+++ b/x-pack/plugins/actions/server/application/connector/methods/get_all/get_all.ts
@@ -9,12 +9,28 @@
* Get all actions with in-memory connectors
*/
import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
+import { AuditLogger } from '@kbn/security-plugin-types-server';
+import { ElasticsearchClient, Logger } from '@kbn/core/server';
+import { omit } from 'lodash';
+import { InMemoryConnector } from '../../../..';
+import { SavedObjectClientForFind } from '../../../../data/connector/types/params';
import { connectorWithExtraFindDataSchema } from '../../schemas';
import { findConnectorsSo, searchConnectorsSo } from '../../../../data/connector';
import { GetAllParams, InjectExtraFindDataParams } from './types';
import { ConnectorAuditAction, connectorAuditEvent } from '../../../../lib/audit_events';
import { connectorFromSavedObject, isConnectorDeprecated } from '../../lib';
import { ConnectorWithExtraFindData } from '../../types';
+import { GetAllUnsecuredParams } from './types/params';
+
+interface GetAllHelperOpts {
+ auditLogger?: AuditLogger;
+ esClient: ElasticsearchClient;
+ inMemoryConnectors: InMemoryConnector[];
+ kibanaIndices: string[];
+ logger: Logger;
+ namespace?: string;
+ savedObjectsClient: SavedObjectClientForFind;
+}
export async function getAll({
context,
@@ -32,28 +48,70 @@ export async function getAll({
throw error;
}
+ return await getAllHelper({
+ auditLogger: context.auditLogger,
+ esClient: context.scopedClusterClient.asInternalUser,
+ inMemoryConnectors: includeSystemActions
+ ? context.inMemoryConnectors
+ : context.inMemoryConnectors.filter((connector) => !connector.isSystemAction),
+ kibanaIndices: context.kibanaIndices,
+ logger: context.logger,
+ savedObjectsClient: context.unsecuredSavedObjectsClient,
+ });
+}
+
+export async function getAllUnsecured({
+ esClient,
+ inMemoryConnectors,
+ internalSavedObjectsRepository,
+ kibanaIndices,
+ logger,
+ spaceId,
+}: GetAllUnsecuredParams): Promise {
+ const namespace = spaceId && spaceId !== 'default' ? spaceId : undefined;
+
+ const connectors = await getAllHelper({
+ esClient,
+ // Unsecured execution does not currently support system actions so we filter them out
+ inMemoryConnectors: inMemoryConnectors.filter((connector) => !connector.isSystemAction),
+ kibanaIndices,
+ logger,
+ namespace,
+ savedObjectsClient: internalSavedObjectsRepository,
+ });
+
+ return connectors.map((connector) => omit(connector, 'secrets'));
+}
+
+async function getAllHelper({
+ auditLogger,
+ esClient,
+ inMemoryConnectors,
+ kibanaIndices,
+ logger,
+ namespace,
+ savedObjectsClient,
+}: GetAllHelperOpts): Promise {
const savedObjectsActions = (
- await findConnectorsSo({ unsecuredSavedObjectsClient: context.unsecuredSavedObjectsClient })
+ await findConnectorsSo({ savedObjectsClient, namespace })
).saved_objects.map((rawAction) =>
connectorFromSavedObject(rawAction, isConnectorDeprecated(rawAction.attributes))
);
- savedObjectsActions.forEach(({ id }) =>
- context.auditLogger?.log(
- connectorAuditEvent({
- action: ConnectorAuditAction.FIND,
- savedObject: { type: 'action', id },
- })
- )
- );
-
- const inMemoryConnectorsFiltered = includeSystemActions
- ? context.inMemoryConnectors
- : context.inMemoryConnectors.filter((connector) => !connector.isSystemAction);
+ if (auditLogger) {
+ savedObjectsActions.forEach(({ id }) =>
+ auditLogger.log(
+ connectorAuditEvent({
+ action: ConnectorAuditAction.FIND,
+ savedObject: { type: 'action', id },
+ })
+ )
+ );
+ }
const mergedResult = [
...savedObjectsActions,
- ...inMemoryConnectorsFiltered.map((inMemoryConnector) => ({
+ ...inMemoryConnectors.map((inMemoryConnector) => ({
id: inMemoryConnector.id,
actionTypeId: inMemoryConnector.actionTypeId,
name: inMemoryConnector.name,
@@ -64,8 +122,8 @@ export async function getAll({
].sort((a, b) => a.name.localeCompare(b.name));
const connectors = await injectExtraFindData({
- kibanaIndices: context.kibanaIndices,
- scopedClusterClient: context.scopedClusterClient,
+ kibanaIndices,
+ esClient,
connectors: mergedResult,
});
@@ -74,7 +132,7 @@ export async function getAll({
try {
connectorWithExtraFindDataSchema.validate(connector);
} catch (e) {
- context.logger.warn(`Error validating connector: ${connector.id}, ${e}`);
+ logger.warn(`Error validating connector: ${connector.id}, ${e}`);
}
});
@@ -83,7 +141,7 @@ export async function getAll({
async function injectExtraFindData({
kibanaIndices,
- scopedClusterClient,
+ esClient,
connectors,
}: InjectExtraFindDataParams): Promise {
const aggs: Record = {};
@@ -121,7 +179,7 @@ async function injectExtraFindData({
};
}
- const aggregationResult = await searchConnectorsSo({ scopedClusterClient, kibanaIndices, aggs });
+ const aggregationResult = await searchConnectorsSo({ esClient, kibanaIndices, aggs });
return connectors.map((connector) => ({
...connector,
diff --git a/x-pack/plugins/actions/server/application/connector/methods/get_all/index.ts b/x-pack/plugins/actions/server/application/connector/methods/get_all/index.ts
index dcbc4c6fbc957..5b3da65578d65 100644
--- a/x-pack/plugins/actions/server/application/connector/methods/get_all/index.ts
+++ b/x-pack/plugins/actions/server/application/connector/methods/get_all/index.ts
@@ -5,4 +5,4 @@
* 2.0.
*/
-export { getAll } from './get_all';
+export { getAll, getAllUnsecured } from './get_all';
diff --git a/x-pack/plugins/actions/server/application/connector/methods/get_all/types/params.ts b/x-pack/plugins/actions/server/application/connector/methods/get_all/types/params.ts
index 4e5157a1fdce0..ca0afdb782f7d 100644
--- a/x-pack/plugins/actions/server/application/connector/methods/get_all/types/params.ts
+++ b/x-pack/plugins/actions/server/application/connector/methods/get_all/types/params.ts
@@ -5,7 +5,10 @@
* 2.0.
*/
-import { IScopedClusterClient } from '@kbn/core-elasticsearch-server';
+import { ElasticsearchClient } from '@kbn/core-elasticsearch-server';
+import { ISavedObjectsRepository, Logger } from '@kbn/core/server';
+import { AuditLogger } from '@kbn/security-plugin/server';
+import { InMemoryConnector } from '../../../../..';
import { ActionsClientContext } from '../../../../../actions_client';
import { Connector } from '../../../types';
@@ -14,8 +17,18 @@ export interface GetAllParams {
context: ActionsClientContext;
}
+export interface GetAllUnsecuredParams {
+ auditLogger?: AuditLogger;
+ esClient: ElasticsearchClient;
+ inMemoryConnectors: InMemoryConnector[];
+ internalSavedObjectsRepository: ISavedObjectsRepository;
+ kibanaIndices: string[];
+ logger: Logger;
+ spaceId: string;
+}
+
export interface InjectExtraFindDataParams {
kibanaIndices: string[];
- scopedClusterClient: IScopedClusterClient;
+ esClient: ElasticsearchClient;
connectors: Connector[];
}
diff --git a/x-pack/plugins/actions/server/data/connector/find_connectors_so.ts b/x-pack/plugins/actions/server/data/connector/find_connectors_so.ts
index da232f5b2aa83..238ae18a1b62b 100644
--- a/x-pack/plugins/actions/server/data/connector/find_connectors_so.ts
+++ b/x-pack/plugins/actions/server/data/connector/find_connectors_so.ts
@@ -9,10 +9,12 @@ import { FindConnectorsSoResult, FindConnectorsSoParams } from './types';
import { MAX_ACTIONS_RETURNED } from './constants';
export const findConnectorsSo = async ({
- unsecuredSavedObjectsClient,
+ savedObjectsClient,
+ namespace,
}: FindConnectorsSoParams): Promise => {
- return unsecuredSavedObjectsClient.find({
+ return savedObjectsClient.find({
perPage: MAX_ACTIONS_RETURNED,
type: 'action',
+ ...(namespace ? { namespaces: [namespace] } : {}),
});
};
diff --git a/x-pack/plugins/actions/server/data/connector/search_connectors_so.ts b/x-pack/plugins/actions/server/data/connector/search_connectors_so.ts
index 09d3ae3b532d9..ab549899348ae 100644
--- a/x-pack/plugins/actions/server/data/connector/search_connectors_so.ts
+++ b/x-pack/plugins/actions/server/data/connector/search_connectors_so.ts
@@ -8,11 +8,11 @@
import { SearchConnectorsSoParams } from './types';
export const searchConnectorsSo = async ({
- scopedClusterClient,
+ esClient,
kibanaIndices,
aggs,
}: SearchConnectorsSoParams) => {
- return scopedClusterClient.asInternalUser.search({
+ return esClient.search({
index: kibanaIndices,
ignore_unavailable: true,
body: {
diff --git a/x-pack/plugins/actions/server/data/connector/types/params.ts b/x-pack/plugins/actions/server/data/connector/types/params.ts
index 73d8ea6dadd14..c23447fb37486 100644
--- a/x-pack/plugins/actions/server/data/connector/types/params.ts
+++ b/x-pack/plugins/actions/server/data/connector/types/params.ts
@@ -5,18 +5,21 @@
* 2.0.
*/
-import { IScopedClusterClient } from '@kbn/core-elasticsearch-server';
+import { ElasticsearchClient } from '@kbn/core-elasticsearch-server';
import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { SavedObjectsClientContract } from '@kbn/core-saved-objects-api-server';
+import { SavedObjectsClient } from '@kbn/core/server';
+export type SavedObjectClientForFind = Pick;
export interface SearchConnectorsSoParams {
kibanaIndices: string[];
- scopedClusterClient: IScopedClusterClient;
+ esClient: ElasticsearchClient;
aggs: Record;
}
export interface FindConnectorsSoParams {
- unsecuredSavedObjectsClient: SavedObjectsClientContract;
+ savedObjectsClient: SavedObjectClientForFind;
+ namespace?: string;
}
export interface GetConnectorSoParams {
diff --git a/x-pack/plugins/actions/server/plugin.ts b/x-pack/plugins/actions/server/plugin.ts
index 33383e526e36d..1ef28b10e6440 100644
--- a/x-pack/plugins/actions/server/plugin.ts
+++ b/x-pack/plugins/actions/server/plugin.ts
@@ -486,18 +486,22 @@ export class ActionsPlugin implements Plugin {
const internalSavedObjectsRepository = core.savedObjects.createInternalRepository([
+ ACTION_SAVED_OBJECT_TYPE,
ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE,
]);
return new UnsecuredActionsClient({
actionExecutor: actionExecutor!,
- internalSavedObjectsRepository,
+ clusterClient: core.elasticsearch.client,
executionEnqueuer: createBulkUnsecuredExecutionEnqueuerFunction({
taskManager: plugins.taskManager,
connectorTypeRegistry: actionTypeRegistry!,
inMemoryConnectors: this.inMemoryConnectors,
configurationUtilities: actionsConfigUtils,
}),
+ inMemoryConnectors: this.inMemoryConnectors,
+ internalSavedObjectsRepository,
+ kibanaIndices: core.savedObjects.getAllIndices(),
logger: this.logger,
});
};
diff --git a/x-pack/plugins/actions/server/unsecured_actions_client/unsecured_actions_client.mock.ts b/x-pack/plugins/actions/server/unsecured_actions_client/unsecured_actions_client.mock.ts
index 4cbbfa1604dc1..748847d579eeb 100644
--- a/x-pack/plugins/actions/server/unsecured_actions_client/unsecured_actions_client.mock.ts
+++ b/x-pack/plugins/actions/server/unsecured_actions_client/unsecured_actions_client.mock.ts
@@ -11,6 +11,7 @@ export type UnsecuredActionsClientMock = jest.Mocked;
const createUnsecuredActionsClientMock = () => {
const mocked: UnsecuredActionsClientMock = {
+ getAll: jest.fn(),
execute: jest.fn(),
bulkEnqueueExecution: jest.fn(),
};
diff --git a/x-pack/plugins/actions/server/unsecured_actions_client/unsecured_actions_client.test.ts b/x-pack/plugins/actions/server/unsecured_actions_client/unsecured_actions_client.test.ts
index 5df39e28fcbc1..89145d80eea19 100644
--- a/x-pack/plugins/actions/server/unsecured_actions_client/unsecured_actions_client.test.ts
+++ b/x-pack/plugins/actions/server/unsecured_actions_client/unsecured_actions_client.test.ts
@@ -6,28 +6,125 @@
*/
import { v4 as uuidv4 } from 'uuid';
-import { loggingSystemMock, savedObjectsRepositoryMock } from '@kbn/core/server/mocks';
+import {
+ elasticsearchServiceMock,
+ loggingSystemMock,
+ savedObjectsRepositoryMock,
+} from '@kbn/core/server/mocks';
import { asNotificationExecutionSource } from '../lib';
import { actionExecutorMock } from '../lib/action_executor.mock';
import { UnsecuredActionsClient } from './unsecured_actions_client';
+import { Logger } from '@kbn/core/server';
+import { getAllUnsecured } from '../application/connector/methods/get_all/get_all';
+
+jest.mock('../application/connector/methods/get_all/get_all');
+
+const mockGetAllUnsecured = getAllUnsecured as jest.MockedFunction;
const internalSavedObjectsRepository = savedObjectsRepositoryMock.create();
const actionExecutor = actionExecutorMock.create();
const executionEnqueuer = jest.fn();
-const logger = loggingSystemMock.create().get();
-
+const logger = loggingSystemMock.create().get() as jest.Mocked;
+const clusterClient = elasticsearchServiceMock.createClusterClient();
+const inMemoryConnectors = [
+ {
+ id: 'testPreconfigured',
+ actionTypeId: '.slack',
+ secrets: {},
+ isPreconfigured: true,
+ isDeprecated: false,
+ isSystemAction: false,
+ name: 'test',
+ config: {
+ foo: 'bar',
+ },
+ },
+ /**
+ * System actions will not
+ * be returned from getAllUnsecured
+ */
+ {
+ id: 'system-connector-.cases',
+ actionTypeId: '.cases',
+ name: 'System action: .cases',
+ config: {},
+ secrets: {},
+ isDeprecated: false,
+ isMissingSecrets: false,
+ isPreconfigured: false,
+ isSystemAction: true,
+ },
+];
let unsecuredActionsClient: UnsecuredActionsClient;
beforeEach(() => {
jest.resetAllMocks();
unsecuredActionsClient = new UnsecuredActionsClient({
actionExecutor,
- internalSavedObjectsRepository,
+ clusterClient,
executionEnqueuer,
+ inMemoryConnectors,
+ internalSavedObjectsRepository,
+ kibanaIndices: ['.kibana'],
logger,
});
});
+describe('getAll()', () => {
+ test('calls getAllUnsecured library method with appropriate parameters', async () => {
+ const expectedResult = [
+ {
+ actionTypeId: 'test',
+ id: '1',
+ name: 'test',
+ isMissingSecrets: false,
+ config: { foo: 'bar' },
+ isPreconfigured: false,
+ isDeprecated: false,
+ isSystemAction: false,
+ referencedByCount: 6,
+ },
+ {
+ id: 'testPreconfigured',
+ actionTypeId: '.slack',
+ name: 'test',
+ isPreconfigured: true,
+ isSystemAction: false,
+ isDeprecated: false,
+ referencedByCount: 2,
+ },
+ ];
+ mockGetAllUnsecured.mockResolvedValueOnce(expectedResult);
+ const result = await unsecuredActionsClient.getAll('default');
+ expect(result).toEqual(expectedResult);
+ expect(mockGetAllUnsecured).toHaveBeenCalledWith({
+ esClient: clusterClient.asInternalUser,
+ inMemoryConnectors,
+ kibanaIndices: ['.kibana'],
+ logger,
+ internalSavedObjectsRepository,
+ spaceId: 'default',
+ });
+ });
+
+ test('throws error if getAllUnsecured throws errors', async () => {
+ mockGetAllUnsecured.mockImplementationOnce(() => {
+ throw new Error('failfail');
+ });
+ await expect(
+ unsecuredActionsClient.getAll('customSpace')
+ ).rejects.toThrowErrorMatchingInlineSnapshot(`"failfail"`);
+ expect(mockGetAllUnsecured).toHaveBeenCalledWith({
+ esClient: clusterClient.asInternalUser,
+ inMemoryConnectors,
+ kibanaIndices: ['.kibana'],
+ logger,
+ internalSavedObjectsRepository,
+ spaceId: 'customSpace',
+ });
+ });
+});
+
describe('execute()', () => {
test('throws error when executing action with not allowed requester id', async () => {
await expect(
diff --git a/x-pack/plugins/actions/server/unsecured_actions_client/unsecured_actions_client.ts b/x-pack/plugins/actions/server/unsecured_actions_client/unsecured_actions_client.ts
index 96449380a82cd..8331f6890486c 100644
--- a/x-pack/plugins/actions/server/unsecured_actions_client/unsecured_actions_client.ts
+++ b/x-pack/plugins/actions/server/unsecured_actions_client/unsecured_actions_client.ts
@@ -6,7 +6,7 @@
*/
import { v4 as uuidv4 } from 'uuid';
-import { ISavedObjectsRepository, Logger } from '@kbn/core/server';
+import { IClusterClient, ISavedObjectsRepository, Logger } from '@kbn/core/server';
import {
BulkUnsecuredExecutionEnqueuer,
ExecuteOptions,
@@ -17,8 +17,10 @@ import {
asNotificationExecutionSource,
type RelatedSavedObjects,
} from '../lib';
-import { ActionTypeExecutorResult } from '../types';
+import { ActionTypeExecutorResult, InMemoryConnector } from '../types';
import { asBackgroundTaskExecutionSource } from '../lib/action_execution_source';
+import { ConnectorWithExtraFindData } from '../application/connector/types';
+import { getAllUnsecured } from '../application/connector/methods/get_all/get_all';
// requests from the notification service (for system notification)
const NOTIFICATION_REQUESTER_ID = 'notifications';
@@ -37,8 +39,11 @@ const ALLOWED_REQUESTER_IDS = [
export interface UnsecuredActionsClientOpts {
actionExecutor: ActionExecutorContract;
- internalSavedObjectsRepository: ISavedObjectsRepository;
+ clusterClient: IClusterClient;
executionEnqueuer: BulkUnsecuredExecutionEnqueuer;
+ inMemoryConnectors: InMemoryConnector[];
+ internalSavedObjectsRepository: ISavedObjectsRepository;
+ kibanaIndices: string[];
logger: Logger;
}
@@ -48,6 +53,7 @@ type UnsecuredExecuteOptions = Omit & {
};
export interface IUnsecuredActionsClient {
+ getAll: (spaceId: string) => Promise;
execute: (opts: UnsecuredExecuteOptions) => Promise>;
bulkEnqueueExecution: (
requesterId: string,
@@ -56,17 +62,7 @@ export interface IUnsecuredActionsClient {
}
export class UnsecuredActionsClient {
- private readonly actionExecutor: ActionExecutorContract;
- private readonly internalSavedObjectsRepository: ISavedObjectsRepository;
- private readonly executionEnqueuer: BulkUnsecuredExecutionEnqueuer;
- private readonly logger: Logger;
-
- constructor(params: UnsecuredActionsClientOpts) {
- this.actionExecutor = params.actionExecutor;
- this.executionEnqueuer = params.executionEnqueuer;
- this.internalSavedObjectsRepository = params.internalSavedObjectsRepository;
- this.logger = params.logger;
- }
+ constructor(private readonly opts: UnsecuredActionsClientOpts) {}
public async execute({
requesterId,
@@ -83,14 +79,14 @@ export class UnsecuredActionsClient {
}
if (!relatedSavedObjects) {
- this.logger.warn(
+ this.opts.logger.warn(
`Calling "execute" in UnsecuredActionsClient without any relatedSavedObjects data. Consider including this for traceability.`
);
}
const source = this.getSourceFromRequester(requesterId, id, relatedSavedObjects);
- return this.actionExecutor.executeUnsecured({
+ return this.opts.actionExecutor.executeUnsecured({
actionExecutionId: uuidv4(),
actionId: id,
params,
@@ -123,7 +119,18 @@ export class UnsecuredActionsClient {
...source,
};
});
- return this.executionEnqueuer(this.internalSavedObjectsRepository, actionsToEnqueue);
+ return this.opts.executionEnqueuer(this.opts.internalSavedObjectsRepository, actionsToEnqueue);
+ }
+
+ public async getAll(spaceId: string): Promise {
+ return getAllUnsecured({
+ esClient: this.opts.clusterClient.asInternalUser,
+ inMemoryConnectors: this.opts.inMemoryConnectors,
+ kibanaIndices: this.opts.kibanaIndices,
+ logger: this.opts.logger,
+ internalSavedObjectsRepository: this.opts.internalSavedObjectsRepository,
+ spaceId,
+ });
}
private getSourceFromRequester(
diff --git a/x-pack/plugins/actions/tsconfig.json b/x-pack/plugins/actions/tsconfig.json
index d30c8bdde7d60..aae2d31c7aa09 100644
--- a/x-pack/plugins/actions/tsconfig.json
+++ b/x-pack/plugins/actions/tsconfig.json
@@ -44,7 +44,8 @@
"@kbn/core-logging-server-mocks",
"@kbn/serverless",
"@kbn/actions-types",
- "@kbn/core-test-helpers-kbn-server"
+ "@kbn/core-test-helpers-kbn-server",
+ "@kbn/security-plugin-types-server"
],
"exclude": [
"target/**/*",
diff --git a/x-pack/plugins/observability_solution/apm/public/components/app/service_dashboards/actions/goto_dashboard.tsx b/x-pack/plugins/observability_solution/apm/public/components/app/service_dashboards/actions/goto_dashboard.tsx
index f196077c41a4d..1d1c923e6a597 100644
--- a/x-pack/plugins/observability_solution/apm/public/components/app/service_dashboards/actions/goto_dashboard.tsx
+++ b/x-pack/plugins/observability_solution/apm/public/components/app/service_dashboards/actions/goto_dashboard.tsx
@@ -30,7 +30,7 @@ export function GotoDashboard({
data-test-subj="apmGotoDashboardGoToDashboardButton"
color="text"
size="s"
- iconType={'visGauge'}
+ iconType="visGauge"
href={url}
>
{i18n.translate('xpack.apm.serviceDashboards.contextMenu.goToDashboard', {
diff --git a/x-pack/plugins/observability_solution/apm/public/components/app/service_dashboards/actions/link_dashboard.tsx b/x-pack/plugins/observability_solution/apm/public/components/app/service_dashboards/actions/link_dashboard.tsx
index 0db2654c1d66b..03dc390fc93ce 100644
--- a/x-pack/plugins/observability_solution/apm/public/components/app/service_dashboards/actions/link_dashboard.tsx
+++ b/x-pack/plugins/observability_solution/apm/public/components/app/service_dashboards/actions/link_dashboard.tsx
@@ -29,7 +29,7 @@ export function LinkDashboard({
setIsModalVisible(true)}
>
diff --git a/x-pack/plugins/observability_solution/apm/public/components/app/service_dashboards/actions/save_dashboard_modal.tsx b/x-pack/plugins/observability_solution/apm/public/components/app/service_dashboards/actions/save_dashboard_modal.tsx
index 6872b0d2cc805..406a1b3e43190 100644
--- a/x-pack/plugins/observability_solution/apm/public/components/app/service_dashboards/actions/save_dashboard_modal.tsx
+++ b/x-pack/plugins/observability_solution/apm/public/components/app/service_dashboards/actions/save_dashboard_modal.tsx
@@ -53,29 +53,29 @@ export function SaveDashboardModal({
const { data: allAvailableDashboards, status } = useDashboardFetcher();
const history = useHistory();
- let defaultOption: EuiComboBoxOptionOption | undefined;
+ const [isLoading, setIsLoading] = useState(false);
- const [serviceFiltersEnabled, setserviceFiltersEnabled] = useState(
+ const [serviceFiltersEnabled, setServiceFiltersEnabled] = useState(
(currentDashboard?.serviceEnvironmentFilterEnabled &&
currentDashboard?.serviceNameFilterEnabled) ??
true
);
- if (currentDashboard) {
- const { title, dashboardSavedObjectId } = currentDashboard;
- defaultOption = { label: title, value: dashboardSavedObjectId };
- }
-
- const [selectedDashboard, setSelectedDashboard] = useState(
- defaultOption ? [defaultOption] : []
+ const [selectedDashboard, setSelectedDashboard] = useState<
+ Array>
+ >(
+ currentDashboard
+ ? [
+ {
+ label: currentDashboard.title,
+ value: currentDashboard.dashboardSavedObjectId,
+ },
+ ]
+ : []
);
const isEditMode = !!currentDashboard?.id;
- const reloadCustomDashboards = useCallback(() => {
- onRefresh();
- }, [onRefresh]);
-
const options = allAvailableDashboards?.map(
(dashboardItem: DashboardItem) => ({
label: dashboardItem.attributes.title,
@@ -92,6 +92,7 @@ export function SaveDashboardModal({
const [newDashboard] = selectedDashboard;
try {
if (newDashboard.value) {
+ setIsLoading(true);
await callApmApi('POST /internal/apm/custom-dashboard', {
params: {
query: { customDashboardId: currentDashboard?.id },
@@ -117,7 +118,7 @@ export function SaveDashboardModal({
dashboardId: newDashboard.value,
}),
});
- reloadCustomDashboards();
+ onRefresh();
}
} catch (error) {
console.error(error);
@@ -132,6 +133,7 @@ export function SaveDashboardModal({
text: error.body.message,
});
}
+ setIsLoading(false);
onClose();
},
[
@@ -139,7 +141,7 @@ export function SaveDashboardModal({
notifications.toasts,
serviceFiltersEnabled,
onClose,
- reloadCustomDashboards,
+ onRefresh,
isEditMode,
serviceName,
currentDashboard,
@@ -170,7 +172,7 @@ export function SaveDashboardModal({
}
- onChange={() => setserviceFiltersEnabled(!serviceFiltersEnabled)}
+ onChange={() => setServiceFiltersEnabled(!serviceFiltersEnabled)}
checked={serviceFiltersEnabled}
/>
@@ -220,6 +222,7 @@ export function SaveDashboardModal({
{i18n.translate(
'xpack.apm.serviceDashboards.selectDashboard.cancel',
@@ -231,6 +234,7 @@ export function SaveDashboardModal({
{isEditMode
diff --git a/x-pack/plugins/observability_solution/apm/public/components/app/service_dashboards/actions/unlink_dashboard.tsx b/x-pack/plugins/observability_solution/apm/public/components/app/service_dashboards/actions/unlink_dashboard.tsx
index c43d3d289b767..3e68e8f54ebef 100644
--- a/x-pack/plugins/observability_solution/apm/public/components/app/service_dashboards/actions/unlink_dashboard.tsx
+++ b/x-pack/plugins/observability_solution/apm/public/components/app/service_dashboards/actions/unlink_dashboard.tsx
@@ -85,7 +85,7 @@ export function UnlinkDashboard({
setIsModalVisible(true)}
>
diff --git a/x-pack/plugins/observability_solution/apm/public/components/app/service_dashboards/context_menu.tsx b/x-pack/plugins/observability_solution/apm/public/components/app/service_dashboards/context_menu.tsx
index 2eb48b7f66848..bf7da532054bf 100644
--- a/x-pack/plugins/observability_solution/apm/public/components/app/service_dashboards/context_menu.tsx
+++ b/x-pack/plugins/observability_solution/apm/public/components/app/service_dashboards/context_menu.tsx
@@ -4,7 +4,7 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
-
+import { i18n } from '@kbn/i18n';
import React, { useState } from 'react';
import {
EuiButtonIcon,
@@ -36,7 +36,10 @@ export function ContextMenu({ items }: Props) {
display="base"
size="s"
iconType="boxesVertical"
- aria-label="More"
+ aria-label={i18n.translate(
+ 'xpack.apm.serviceDashboards.contextMenu.moreLabel',
+ { defaultMessage: 'More' }
+ )}
onClick={onButtonClick}
/>
}
@@ -47,8 +50,11 @@ export function ContextMenu({ items }: Props) {
>
(
- {item}
+ items={items.map((item, index) => (
+
+ {' '}
+ {item}
+
))}
/>
diff --git a/x-pack/plugins/observability_solution/apm/public/components/app/service_dashboards/dashboard_selector.tsx b/x-pack/plugins/observability_solution/apm/public/components/app/service_dashboards/dashboard_selector.tsx
index 7dbcbe714a428..5925340151a7d 100644
--- a/x-pack/plugins/observability_solution/apm/public/components/app/service_dashboards/dashboard_selector.tsx
+++ b/x-pack/plugins/observability_solution/apm/public/components/app/service_dashboards/dashboard_selector.tsx
@@ -90,8 +90,8 @@ export function DashboardSelector({
selectedDashboard
? [
{
- value: selectedDashboard?.dashboardSavedObjectId,
- label: selectedDashboard?.title,
+ value: selectedDashboard.dashboardSavedObjectId,
+ label: selectedDashboard.title,
},
]
: []
diff --git a/x-pack/plugins/observability_solution/apm/public/components/app/service_dashboards/index.tsx b/x-pack/plugins/observability_solution/apm/public/components/app/service_dashboards/index.tsx
index 7426ec382b87f..4bfe04df040b3 100644
--- a/x-pack/plugins/observability_solution/apm/public/components/app/service_dashboards/index.tsx
+++ b/x-pack/plugins/observability_solution/apm/public/components/app/service_dashboards/index.tsx
@@ -83,7 +83,7 @@ export function ServiceDashboards() {
);
useEffect(() => {
- const filteredServiceDashbords = (data?.serviceDashboards ?? []).reduce(
+ const filteredServiceDashboards = (data?.serviceDashboards ?? []).reduce(
(
result: MergedServiceDashboard[],
serviceDashboard: SavedApmCustomDashboard
@@ -102,8 +102,8 @@ export function ServiceDashboards() {
[]
);
- setServiceDashboards(filteredServiceDashbords);
- }, [allAvailableDashboards, data?.serviceDashboards]);
+ setServiceDashboards(filteredServiceDashboards);
+ }, [allAvailableDashboards, data]);
const getCreationOptions =
useCallback((): Promise => {
@@ -127,7 +127,6 @@ export function ServiceDashboards() {
timeRange: { from: rangeFrom, to: rangeTo },
query: { query: kuery, language: 'kuery' },
});
- // eslint-disable-next-line react-hooks/exhaustive-deps
}, [
dataView,
serviceName,
@@ -136,6 +135,7 @@ export function ServiceDashboards() {
dashboard,
rangeFrom,
rangeTo,
+ currentDashboard,
]);
const getLocatorParams = useCallback(
diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/server/functions/context.ts b/x-pack/plugins/observability_solution/observability_ai_assistant/server/functions/context.ts
index 422c0287e15d7..7db8f63e273f9 100644
--- a/x-pack/plugins/observability_solution/observability_ai_assistant/server/functions/context.ts
+++ b/x-pack/plugins/observability_solution/observability_ai_assistant/server/functions/context.ts
@@ -216,7 +216,7 @@ async function scoreSuggestions({
const newUserMessageContent =
dedent(`Given the following question, score the documents that are relevant to the question. on a scale from 0 to 7,
- 0 being completely relevant, and 7 being extremely relevant. Information is relevant to the question if it helps in
+ 0 being completely irrelevant, and 7 being extremely relevant. Information is relevant to the question if it helps in
answering the question. Judge it according to the following criteria:
- The document is relevant to the question, and the rest of the conversation
diff --git a/x-pack/test/alerting_api_integration/common/plugins/actions_simulators/server/unsecured_actions_simulation.ts b/x-pack/test/alerting_api_integration/common/plugins/actions_simulators/server/unsecured_actions_simulation.ts
index 675dbe50afd54..536abe2d3526e 100644
--- a/x-pack/test/alerting_api_integration/common/plugins/actions_simulators/server/unsecured_actions_simulation.ts
+++ b/x-pack/test/alerting_api_integration/common/plugins/actions_simulators/server/unsecured_actions_simulation.ts
@@ -94,4 +94,33 @@ export function initPlugin(router: IRouter, coreSetup: CoreSetup,
+ res: KibanaResponseFactory
+ ): Promise> {
+ const [_, { actions }] = await coreSetup.getStartServices();
+ const { body } = req;
+
+ try {
+ const unsecuredActionsClient = actions.getUnsecuredActionsClient();
+ const { spaceId } = body;
+ const result = await unsecuredActionsClient.getAll(spaceId);
+
+ return res.ok({ body: { status: 'success', result } });
+ } catch (err) {
+ return res.ok({ body: { status: 'error', error: `${err}` } });
+ }
+ }
+ );
}
diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/get_all_unsecured_actions.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/get_all_unsecured_actions.ts
new file mode 100644
index 0000000000000..3183f19771f16
--- /dev/null
+++ b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/get_all_unsecured_actions.ts
@@ -0,0 +1,209 @@
+/*
+ * 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 expect from '@kbn/expect';
+import { FtrProviderContext } from '../../../../common/ftr_provider_context';
+import { getUrlPrefix, ObjectRemover } from '../../../common/lib';
+import { Spaces } from '../../scenarios';
+
+// eslint-disable-next-line import/no-default-export
+export default function createUnsecuredActionTests({ getService }: FtrProviderContext) {
+ const supertest = getService('supertest');
+ const kibanaServer = getService('kibanaServer');
+
+ const preconfiguredConnectors = [
+ {
+ id: 'preconfigured-alert-history-es-index',
+ actionTypeId: '.index',
+ name: 'Alert history Elasticsearch index',
+ isPreconfigured: true,
+ isDeprecated: false,
+ isSystemAction: false,
+ referencedByCount: 0,
+ },
+ {
+ id: 'notification-email',
+ actionTypeId: '.email',
+ name: 'Notification Email Connector',
+ isPreconfigured: true,
+ isDeprecated: false,
+ isSystemAction: false,
+ referencedByCount: 0,
+ },
+ {
+ id: 'preconfigured-es-index-action',
+ actionTypeId: '.index',
+ name: 'preconfigured_es_index_action',
+ isPreconfigured: true,
+ isDeprecated: false,
+ isSystemAction: false,
+ referencedByCount: 0,
+ },
+ {
+ id: 'my-deprecated-servicenow',
+ actionTypeId: '.servicenow',
+ name: 'ServiceNow#xyz',
+ isPreconfigured: true,
+ isDeprecated: true,
+ isSystemAction: false,
+ referencedByCount: 0,
+ },
+ {
+ id: 'my-deprecated-servicenow-default',
+ actionTypeId: '.servicenow',
+ name: 'ServiceNow#xyz',
+ isPreconfigured: true,
+ isDeprecated: true,
+ isSystemAction: false,
+ referencedByCount: 0,
+ },
+ {
+ id: 'my-slack1',
+ actionTypeId: '.slack',
+ name: 'Slack#xyz',
+ isPreconfigured: true,
+ isDeprecated: false,
+ isSystemAction: false,
+ referencedByCount: 0,
+ },
+ {
+ id: 'custom-system-abc-connector',
+ actionTypeId: 'system-abc-action-type',
+ name: 'SystemABC',
+ isPreconfigured: true,
+ isDeprecated: false,
+ isSystemAction: false,
+ referencedByCount: 0,
+ },
+ {
+ id: 'preconfigured.test.index-record',
+ actionTypeId: 'test.index-record',
+ name: 'Test:_Preconfigured_Index_Record',
+ isPreconfigured: true,
+ isDeprecated: false,
+ isSystemAction: false,
+ referencedByCount: 0,
+ },
+ {
+ id: 'my-test-email',
+ actionTypeId: '.email',
+ name: 'TestEmail#xyz',
+ isPreconfigured: true,
+ isDeprecated: false,
+ isSystemAction: false,
+ referencedByCount: 0,
+ },
+ ];
+
+ describe('get all unsecured actions', () => {
+ const objectRemover = new ObjectRemover(supertest);
+
+ // need to wait for kibanaServer to settle ...
+ before(() => {
+ kibanaServer.resolveUrl(`/api/get_all_unsecured_actions`);
+ });
+
+ after(() => objectRemover.removeAll());
+
+ it('should successfully get all actions', async () => {
+ // Create a connector
+ const { body: createdConnector1 } = await supertest
+ .post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/connector`)
+ .set('kbn-xsrf', 'foo')
+ .send({
+ name: 'zzz - My action1',
+ connector_type_id: 'test.index-record',
+ config: {
+ unencrypted: `This value shouldn't get encrypted`,
+ },
+ secrets: {
+ encrypted: 'This value should be encrypted',
+ },
+ })
+ .expect(200);
+ objectRemover.add(Spaces.space1.id, createdConnector1.id, 'action', 'actions');
+
+ const { body: createdConnector2 } = await supertest
+ .post(`${getUrlPrefix(Spaces.other.id)}/api/actions/connector`)
+ .set('kbn-xsrf', 'foo')
+ .send({
+ name: 'zzz - My action2',
+ connector_type_id: 'test.index-record',
+ config: {
+ unencrypted: `This value shouldn't get encrypted`,
+ },
+ secrets: {
+ encrypted: 'This value should be encrypted',
+ },
+ })
+ .expect(200);
+ objectRemover.add(Spaces.other.id, createdConnector2.id, 'action', 'actions');
+
+ const space1SpaceResponse = await supertest
+ .post(`/api/get_all_unsecured_actions`)
+ .set('kbn-xsrf', 'xxx')
+ .send({
+ spaceId: Spaces.space1.id,
+ })
+ .expect(200);
+ expect(space1SpaceResponse.body.status).to.eql('success');
+
+ // the custom ssl connectors have dynamic ports, so remove them before
+ // comparing to what we expect
+ const preconfiguredWithSpace1Connector = space1SpaceResponse.body.result.filter(
+ (conn: { id: string }) => !conn.id.startsWith('custom.ssl.')
+ );
+ expect(preconfiguredWithSpace1Connector).to.eql([
+ ...preconfiguredConnectors,
+ {
+ id: createdConnector1.id,
+ isPreconfigured: false,
+ isDeprecated: false,
+ name: 'zzz - My action1',
+ actionTypeId: 'test.index-record',
+ isMissingSecrets: false,
+ isSystemAction: false,
+ config: {
+ unencrypted: `This value shouldn't get encrypted`,
+ },
+ referencedByCount: 0,
+ },
+ ]);
+
+ const otherSpaceResponse = await supertest
+ .post(`/api/get_all_unsecured_actions`)
+ .set('kbn-xsrf', 'xxx')
+ .send({
+ spaceId: Spaces.other.id,
+ })
+ .expect(200);
+ expect(otherSpaceResponse.body.status).to.eql('success');
+
+ // the custom ssl connectors have dynamic ports, so remove them before
+ // comparing to what we expect
+ const preconfiguredWithOtherSpaceConnector = otherSpaceResponse.body.result.filter(
+ (conn: { id: string }) => !conn.id.startsWith('custom.ssl.')
+ );
+ expect(preconfiguredWithOtherSpaceConnector).to.eql([
+ ...preconfiguredConnectors,
+ {
+ id: createdConnector2.id,
+ isPreconfigured: false,
+ isDeprecated: false,
+ name: 'zzz - My action2',
+ actionTypeId: 'test.index-record',
+ isMissingSecrets: false,
+ isSystemAction: false,
+ config: {
+ unencrypted: `This value shouldn't get encrypted`,
+ },
+ referencedByCount: 0,
+ },
+ ]);
+ });
+ });
+}
diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/index.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/index.ts
index 89f1d48285ae2..4f5832debebda 100644
--- a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/index.ts
+++ b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/index.ts
@@ -31,6 +31,7 @@ export default function actionsTests({ loadTestFile, getService }: FtrProviderCo
loadTestFile(require.resolve('./type_not_enabled'));
loadTestFile(require.resolve('./schedule_unsecured_action'));
loadTestFile(require.resolve('./execute_unsecured_action'));
+ loadTestFile(require.resolve('./get_all_unsecured_actions'));
loadTestFile(require.resolve('./check_registered_connector_types'));
loadTestFile(require.resolve('./max_queued_actions_circuit_breaker'));
diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/sourcerer/sourcerer_timeline.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/sourcerer/sourcerer_timeline.cy.ts
index f720d11689d1a..bb83d4cfbe007 100644
--- a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/sourcerer/sourcerer_timeline.cy.ts
+++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/sourcerer/sourcerer_timeline.cy.ts
@@ -10,7 +10,6 @@ import {
DEFAULT_INDEX_PATTERN,
} from '@kbn/security-solution-plugin/common/constants';
-import { deleteTimelines } from '../../../../tasks/api_calls/common';
import { login } from '../../../../tasks/login';
import { visitWithTimeRange } from '../../../../tasks/navigation';
@@ -33,7 +32,7 @@ import {
} from '../../../../tasks/sourcerer';
import { openTimelineUsingToggle } from '../../../../tasks/security_main';
import { SOURCERER } from '../../../../screens/sourcerer';
-import { createTimeline } from '../../../../tasks/api_calls/timelines';
+import { createTimeline, deleteTimelines } from '../../../../tasks/api_calls/timelines';
import { getTimeline, getTimelineModifiedSourcerer } from '../../../../objects/timeline';
import { closeTimeline, openTimelineById } from '../../../../tasks/timeline';
diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/explore/cases/attach_timeline.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/explore/cases/attach_timeline.cy.ts
index 0dfa0620acccb..a56390010b882 100644
--- a/x-pack/test/security_solution_cypress/cypress/e2e/explore/cases/attach_timeline.cy.ts
+++ b/x-pack/test/security_solution_cypress/cypress/e2e/explore/cases/attach_timeline.cy.ts
@@ -16,8 +16,7 @@ import {
import { DESCRIPTION_INPUT, ADD_COMMENT_INPUT } from '../../../screens/create_new_case';
import { getCase1 } from '../../../objects/case';
import { getTimeline } from '../../../objects/timeline';
-import { createTimeline } from '../../../tasks/api_calls/timelines';
-import { deleteTimelines } from '../../../tasks/api_calls/common';
+import { createTimeline, deleteTimelines } from '../../../tasks/api_calls/timelines';
import { createCase, deleteCases } from '../../../tasks/api_calls/cases';
describe('attach timeline to case', { tags: ['@ess', '@serverless'] }, () => {
diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/explore/cases/creation.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/explore/cases/creation.cy.ts
index 9867cfe77bbf9..c38f09cf16878 100644
--- a/x-pack/test/security_solution_cypress/cypress/e2e/explore/cases/creation.cy.ts
+++ b/x-pack/test/security_solution_cypress/cypress/e2e/explore/cases/creation.cy.ts
@@ -39,7 +39,7 @@ import { TIMELINE_QUERY, TIMELINE_TITLE } from '../../../screens/timeline';
import { OVERVIEW_CASE_DESCRIPTION, OVERVIEW_CASE_NAME } from '../../../screens/overview';
import { goToCaseDetails, goToCreateNewCase } from '../../../tasks/all_cases';
-import { createTimeline } from '../../../tasks/api_calls/timelines';
+import { createTimeline, deleteTimelines } from '../../../tasks/api_calls/timelines';
import { openCaseTimeline } from '../../../tasks/case_details';
import {
attachTimeline,
@@ -53,7 +53,6 @@ import { visit, visitWithTimeRange } from '../../../tasks/navigation';
import { CASES_URL, OVERVIEW_URL } from '../../../urls/navigation';
import { CLOUD_SERVERLESS, ELASTICSEARCH_USERNAME } from '../../../env_var_names_constants';
-import { deleteTimelines } from '../../../tasks/api_calls/common';
import { deleteCases } from '../../../tasks/api_calls/cases';
const isCloudServerless = Cypress.env(CLOUD_SERVERLESS);
diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/alerts/ransomware_detection.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/alerts/ransomware_detection.cy.ts
index a225d08780166..a4908a1035f39 100644
--- a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/alerts/ransomware_detection.cy.ts
+++ b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/alerts/ransomware_detection.cy.ts
@@ -14,7 +14,7 @@ import { ALERTS_HISTOGRAM_SERIES, ALERT_RULE_NAME, MESSAGE } from '../../../scre
import { TIMELINE_QUERY, TIMELINE_VIEW_IN_ANALYZER } from '../../../screens/timeline';
import { selectAlertsHistogram } from '../../../tasks/alerts';
import { openTimelineUsingToggle } from '../../../tasks/security_main';
-import { deleteTimelines } from '../../../tasks/api_calls/common';
+import { deleteTimelines } from '../../../tasks/api_calls/timelines';
describe('Ransomware Detection Alerts', { tags: ['@ess', '@serverless'] }, () => {
before(() => {
diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/alerts/ransomware_prevention.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/alerts/ransomware_prevention.cy.ts
index abc00b14c466a..c7078783466e9 100644
--- a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/alerts/ransomware_prevention.cy.ts
+++ b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/alerts/ransomware_prevention.cy.ts
@@ -12,7 +12,7 @@ import { ALERTS_URL } from '../../../urls/navigation';
import { ALERTS_HISTOGRAM_SERIES, ALERT_RULE_NAME, MESSAGE } from '../../../screens/alerts';
import { TIMELINE_VIEW_IN_ANALYZER } from '../../../screens/timeline';
import { selectAlertsHistogram } from '../../../tasks/alerts';
-import { deleteTimelines } from '../../../tasks/api_calls/common';
+import { deleteTimelines } from '../../../tasks/api_calls/timelines';
import { createTimeline } from '../../../tasks/api_calls/timelines';
import { getTimeline } from '../../../objects/timeline';
diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timeline_templates/creation.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timeline_templates/creation.cy.ts
index 2c640bf3d4492..40efaa1f20883 100644
--- a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timeline_templates/creation.cy.ts
+++ b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timeline_templates/creation.cy.ts
@@ -16,7 +16,7 @@ import {
} from '../../../screens/timeline';
import { TIMELINES_DESCRIPTION, TIMELINES_USERNAME } from '../../../screens/timelines';
import { createTimeline } from '../../../tasks/api_calls/timelines';
-import { deleteTimelines } from '../../../tasks/api_calls/common';
+import { deleteTimelines } from '../../../tasks/api_calls/timelines';
import { login } from '../../../tasks/login';
import { visit } from '../../../tasks/navigation';
diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timeline_templates/export.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timeline_templates/export.cy.ts
index caf9271adb19b..2f32c15ac8b86 100644
--- a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timeline_templates/export.cy.ts
+++ b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timeline_templates/export.cy.ts
@@ -14,9 +14,8 @@ import {
} from '../../../objects/timeline';
import { TIMELINE_TEMPLATES_URL } from '../../../urls/navigation';
-import { createTimelineTemplate } from '../../../tasks/api_calls/timelines';
+import { createTimelineTemplate, deleteTimelines } from '../../../tasks/api_calls/timelines';
import { searchByTitle } from '../../../tasks/table_pagination';
-import { deleteTimelines } from '../../../tasks/api_calls/common';
describe('Export timelines', { tags: ['@ess', '@serverless'] }, () => {
beforeEach(() => {
diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timelines/correlation_tab.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timelines/correlation_tab.cy.ts
index 2318a4c4ef452..70f7d82e34629 100644
--- a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timelines/correlation_tab.cy.ts
+++ b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timelines/correlation_tab.cy.ts
@@ -21,7 +21,7 @@ import { addEqlToTimeline, saveTimeline, clearEqlInTimeline } from '../../../tas
import { TIMELINES_URL } from '../../../urls/navigation';
import { EQL_QUERY_VALIDATION_ERROR } from '../../../screens/create_new_rule';
-import { deleteTimelines } from '../../../tasks/api_calls/common';
+import { deleteTimelines } from '../../../tasks/api_calls/timelines';
describe('Correlation tab', { tags: ['@ess', '@serverless'] }, () => {
const eql = 'any where process.name == "zsh"';
diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timelines/creation.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timelines/creation.cy.ts
index 88fbf7a238aeb..b11fb0b8cf88f 100644
--- a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timelines/creation.cy.ts
+++ b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timelines/creation.cy.ts
@@ -23,9 +23,8 @@ import {
} from '../../../screens/timeline';
import { LOADING_INDICATOR } from '../../../screens/security_header';
import { ROWS } from '../../../screens/timelines';
-import { createTimelineTemplate } from '../../../tasks/api_calls/timelines';
+import { createTimelineTemplate, deleteTimelines } from '../../../tasks/api_calls/timelines';
-import { deleteTimelines } from '../../../tasks/api_calls/common';
import { login } from '../../../tasks/login';
import { visit, visitWithTimeRange } from '../../../tasks/navigation';
import { openTimelineUsingToggle } from '../../../tasks/security_main';
diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timelines/esql/search_filter.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timelines/esql/search_filter.cy.ts
index 73bb13b20236b..15263ff5d6b83 100644
--- a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timelines/esql/search_filter.cy.ts
+++ b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timelines/esql/search_filter.cy.ts
@@ -5,7 +5,6 @@
* 2.0.
*/
-import { deleteTimelines } from '../../../../tasks/api_calls/common';
import { GET_LOCAL_DATE_PICKER_START_DATE_POPOVER_BUTTON } from '../../../../screens/date_picker';
import {
setStartDate,
@@ -25,10 +24,11 @@ import {
addFieldToTable,
convertEditorNonBreakingSpaceToSpace,
} from '../../../../tasks/discover';
-import { createNewTimeline, goToEsqlTab, openActiveTimeline } from '../../../../tasks/timeline';
import { login } from '../../../../tasks/login';
import { visitWithTimeRange } from '../../../../tasks/navigation';
import { ALERTS_URL } from '../../../../urls/navigation';
+import { deleteTimelines } from '../../../../tasks/api_calls/timelines';
+import { openActiveTimeline, createNewTimeline, goToEsqlTab } from '../../../../tasks/timeline';
const DEFAULT_DATE = '~ 15 minutes ago';
const INITIAL_START_DATE = 'Jan 18, 2021 @ 20:33:29.186';
diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timelines/export.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timelines/export.cy.ts
index cbcc6fc758963..4c98e9d2f9241 100644
--- a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timelines/export.cy.ts
+++ b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timelines/export.cy.ts
@@ -13,7 +13,7 @@ import {
} from '../../../tasks/timelines';
import { login } from '../../../tasks/login';
import { visit } from '../../../tasks/navigation';
-import { deleteTimelines } from '../../../tasks/api_calls/common';
+import { deleteTimelines } from '../../../tasks/api_calls/timelines';
import { TIMELINES_URL } from '../../../urls/navigation';
import { TOASTER } from '../../../screens/alerts_detection_rules';
diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timelines/notes_tab.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timelines/notes_tab.cy.ts
index dd7c775a32118..c9ff65129bfe1 100644
--- a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timelines/notes_tab.cy.ts
+++ b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timelines/notes_tab.cy.ts
@@ -18,14 +18,12 @@ import {
MARKDOWN_INVESTIGATE_BUTTON,
} from '../../../screens/timeline';
import { MODAL_CONFIRMATION_BTN } from '../../../screens/alerts_detection_rules';
-import { createTimeline } from '../../../tasks/api_calls/timelines';
+import { createTimeline, deleteTimelines } from '../../../tasks/api_calls/timelines';
import { login } from '../../../tasks/login';
import { visitTimeline } from '../../../tasks/navigation';
import { addNotesToTimeline, goToNotesTab } from '../../../tasks/timeline';
-import { deleteTimelines } from '../../../tasks/api_calls/common';
-
const author = Cypress.env('ELASTICSEARCH_USERNAME');
const link = 'https://www.elastic.co/';
diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timelines/open_timeline.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timelines/open_timeline.cy.ts
index 6c1ec9b093df7..1d6b54e8339e6 100644
--- a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timelines/open_timeline.cy.ts
+++ b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timelines/open_timeline.cy.ts
@@ -25,7 +25,7 @@ import {
refreshTimelinesUntilTimeLinePresent,
} from '../../../tasks/timeline';
import { TIMELINES_URL } from '../../../urls/navigation';
-import { deleteTimelines } from '../../../tasks/api_calls/common';
+import { deleteTimelines } from '../../../tasks/api_calls/timelines';
describe('Open timeline modal', { tags: ['@serverless', '@ess'] }, () => {
beforeEach(function () {
diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timelines/overview.cy.tsx b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timelines/overview.cy.tsx
index 16b6c58146915..ec8583c287de5 100644
--- a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timelines/overview.cy.tsx
+++ b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timelines/overview.cy.tsx
@@ -19,10 +19,13 @@ import {
import { login } from '../../../tasks/login';
import { visit } from '../../../tasks/navigation';
-import { createTimeline, favoriteTimeline } from '../../../tasks/api_calls/timelines';
+import {
+ createTimeline,
+ deleteTimelines,
+ favoriteTimeline,
+} from '../../../tasks/api_calls/timelines';
import { TIMELINES_URL } from '../../../urls/navigation';
-import { deleteTimelines } from '../../../tasks/api_calls/common';
describe('timeline overview search', { tags: ['@ess', 'serverless'] }, () => {
beforeEach(() => {
diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timelines/query_tab.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timelines/query_tab.cy.ts
index acd3f4eb2176e..3cd3f1148f471 100644
--- a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timelines/query_tab.cy.ts
+++ b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timelines/query_tab.cy.ts
@@ -14,7 +14,7 @@ import {
TIMELINE_QUERY,
NOTE_CARD_CONTENT,
} from '../../../screens/timeline';
-import { deleteTimelines } from '../../../tasks/api_calls/common';
+import { deleteTimelines } from '../../../tasks/api_calls/timelines';
import { addNoteToTimeline } from '../../../tasks/api_calls/notes';
import { createTimeline } from '../../../tasks/api_calls/timelines';
diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timelines/row_renderers.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timelines/row_renderers.cy.ts
index 4867ba79183f3..09e04c28e4975 100644
--- a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timelines/row_renderers.cy.ts
+++ b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timelines/row_renderers.cy.ts
@@ -16,7 +16,7 @@ import {
TIMELINE_ROW_RENDERERS_SURICATA_LINK_TOOLTIP,
TIMELINE_ROW_RENDERERS_MODAL_CLOSE_BUTTON,
} from '../../../screens/timeline';
-import { deleteTimelines } from '../../../tasks/api_calls/common';
+import { deleteTimelines } from '../../../tasks/api_calls/timelines';
import { waitForWelcomePanelToBeLoaded } from '../../../tasks/common';
import { waitForAllHostsToBeLoaded } from '../../../tasks/hosts/all_hosts';
diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timelines/search_or_filter.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timelines/search_or_filter.cy.ts
index ad72dfd2f6ca3..1c55f606d4a72 100644
--- a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timelines/search_or_filter.cy.ts
+++ b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timelines/search_or_filter.cy.ts
@@ -20,7 +20,7 @@ import {
selectKqlSearchMode,
} from '../../../tasks/timeline';
import { waitForTimelinesPanelToBeLoaded } from '../../../tasks/timelines';
-import { deleteTimelines } from '../../../tasks/api_calls/common';
+import { deleteTimelines } from '../../../tasks/api_calls/timelines';
import { hostsUrl, TIMELINES_URL } from '../../../urls/navigation';
diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timelines/url_state.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timelines/url_state.cy.ts
index ddd65d69c5fed..23403fc7b101d 100644
--- a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timelines/url_state.cy.ts
+++ b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timelines/url_state.cy.ts
@@ -16,7 +16,7 @@ import { getNewRule } from '../../../objects/rule';
import { login } from '../../../tasks/login';
import { visit, visitWithTimeRange } from '../../../tasks/navigation';
import { TIMELINES_URL } from '../../../urls/navigation';
-import { deleteTimelines } from '../../../tasks/api_calls/common';
+import { deleteTimelines } from '../../../tasks/api_calls/timelines';
describe('Open timeline', { tags: ['@serverless', '@ess'] }, () => {
let timelineSavedObjectId: string | null = null;
diff --git a/x-pack/test/security_solution_cypress/cypress/tasks/api_calls/common.ts b/x-pack/test/security_solution_cypress/cypress/tasks/api_calls/common.ts
index 1dde47ebf1ab9..d16c8566312dd 100644
--- a/x-pack/test/security_solution_cypress/cypress/tasks/api_calls/common.ts
+++ b/x-pack/test/security_solution_cypress/cypress/tasks/api_calls/common.ts
@@ -95,27 +95,6 @@ export const deleteEndpointExceptionList = () => {
});
};
-export const deleteTimelines = () => {
- const kibanaIndexUrl = `${Cypress.env('ELASTICSEARCH_URL')}/.kibana_\*`;
- rootRequest({
- method: 'POST',
- url: `${kibanaIndexUrl}/_delete_by_query?conflicts=proceed&refresh`,
- body: {
- query: {
- bool: {
- filter: [
- {
- match: {
- type: 'siem-ui-timeline',
- },
- },
- ],
- },
- },
- },
- });
-};
-
export const getConnectors = () =>
rootRequest({
method: 'GET',
diff --git a/x-pack/test/security_solution_cypress/cypress/tasks/api_calls/timelines.ts b/x-pack/test/security_solution_cypress/cypress/tasks/api_calls/timelines.ts
index 620a105a2b98e..e6c13eb4b5988 100644
--- a/x-pack/test/security_solution_cypress/cypress/tasks/api_calls/timelines.ts
+++ b/x-pack/test/security_solution_cypress/cypress/tasks/api_calls/timelines.ts
@@ -5,7 +5,10 @@
* 2.0.
*/
-import type { TimelineResponse } from '@kbn/security-solution-plugin/common/api/timeline';
+import type {
+ AllTimelinesResponse,
+ TimelineResponse,
+} from '@kbn/security-solution-plugin/common/api/timeline';
import type { CompleteTimeline } from '../../objects/timeline';
import { rootRequest } from './common';
@@ -141,3 +144,22 @@ export const favoriteTimeline = ({
templateTimelineVersion: templateTimelineVersion || null,
},
});
+
+export const getAllTimelines = () =>
+ rootRequest({
+ method: 'GET',
+ url: 'api/timelines?page_size=100&page_index=1&sort_field=updated&sort_order=desc&timeline_type=default',
+ });
+
+export const deleteTimelines = () => {
+ getAllTimelines().then(($timelines) => {
+ const savedObjectIds = $timelines.body.timeline.map((timeline) => timeline.savedObjectId);
+ rootRequest({
+ method: 'DELETE',
+ url: 'api/timeline',
+ body: {
+ savedObjectIds,
+ },
+ });
+ });
+};