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, + }, + }); + }); +};