From 988795d1ff45cd966967d19cc9d976cbb1b609aa Mon Sep 17 00:00:00 2001 From: Kevin Lacabane Date: Wed, 3 Jul 2024 11:21:02 +0200 Subject: [PATCH] [eem] improve enablement error handling (#187215) --- .../entity_manager/common/errors.ts | 2 + .../lib/entities/find_entity_definition.ts | 3 +- .../server/lib/entities/types.ts | 12 ++ .../server/routes/enablement/check.ts | 79 +++++++++---- .../server/routes/enablement/disable.ts | 43 +++---- .../server/routes/enablement/enable.ts | 107 +++++++++--------- 6 files changed, 150 insertions(+), 96 deletions(-) create mode 100644 x-pack/plugins/observability_solution/entity_manager/server/lib/entities/types.ts diff --git a/x-pack/plugins/observability_solution/entity_manager/common/errors.ts b/x-pack/plugins/observability_solution/entity_manager/common/errors.ts index ebf5670db2aaf..27e9406771da5 100644 --- a/x-pack/plugins/observability_solution/entity_manager/common/errors.ts +++ b/x-pack/plugins/observability_solution/entity_manager/common/errors.ts @@ -9,3 +9,5 @@ export const ERROR_API_KEY_NOT_FOUND = 'api_key_not_found'; export const ERROR_API_KEY_NOT_VALID = 'api_key_not_valid'; export const ERROR_USER_NOT_AUTHORIZED = 'user_not_authorized'; export const ERROR_API_KEY_SERVICE_DISABLED = 'api_key_service_disabled'; +export const ERROR_PARTIAL_BUILTIN_INSTALLATION = 'partial_builtin_installation'; +export const ERROR_DEFINITION_STOPPED = 'error_definition_stopped'; diff --git a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/find_entity_definition.ts b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/find_entity_definition.ts index 8351142333f9b..fbca1362491a5 100644 --- a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/find_entity_definition.ts +++ b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/find_entity_definition.ts @@ -14,6 +14,7 @@ import { generateLatestIngestPipelineId } from './ingest_pipeline/generate_lates import { generateHistoryTransformId } from './transform/generate_history_transform_id'; import { generateLatestTransformId } from './transform/generate_latest_transform_id'; import { BUILT_IN_ID_PREFIX } from './built_in'; +import { EntityDefinitionWithState } from './types'; export async function findEntityDefinitions({ soClient, @@ -29,7 +30,7 @@ export async function findEntityDefinitions({ id?: string; page?: number; perPage?: number; -}): Promise> { +}): Promise { const filter = compact([ typeof builtIn === 'boolean' ? `${SO_ENTITY_DEFINITION_TYPE}.attributes.id:(${BUILT_IN_ID_PREFIX}*)` diff --git a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/types.ts b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/types.ts new file mode 100644 index 0000000000000..1f3498a9354a5 --- /dev/null +++ b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/types.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EntityDefinition } from '@kbn/entities-schema'; + +export type EntityDefinitionWithState = EntityDefinition & { + state: { installed: boolean; running: boolean }; +}; diff --git a/x-pack/plugins/observability_solution/entity_manager/server/routes/enablement/check.ts b/x-pack/plugins/observability_solution/entity_manager/server/routes/enablement/check.ts index 97f621daf750d..251c3ccfefada 100644 --- a/x-pack/plugins/observability_solution/entity_manager/server/routes/enablement/check.ts +++ b/x-pack/plugins/observability_solution/entity_manager/server/routes/enablement/check.ts @@ -11,13 +11,19 @@ import { SetupRouteOptions } from '../types'; import { ENTITY_INTERNAL_API_PREFIX } from '../../../common/constants_entities'; import { ManagedEntityEnabledResponse } from '../../../common/types_api'; import { checkIfEntityDiscoveryAPIKeyIsValid, readEntityDiscoveryAPIKey } from '../../lib/auth'; -import { ERROR_API_KEY_NOT_FOUND, ERROR_API_KEY_NOT_VALID } from '../../../common/errors'; +import { + ERROR_API_KEY_NOT_FOUND, + ERROR_API_KEY_NOT_VALID, + ERROR_DEFINITION_STOPPED, + ERROR_PARTIAL_BUILTIN_INSTALLATION, +} from '../../../common/errors'; import { findEntityDefinitions } from '../../lib/entities/find_entity_definition'; import { builtInDefinitions } from '../../lib/entities/built_in'; export function checkEntityDiscoveryEnabledRoute({ router, server, + logger, }: SetupRouteOptions) { router.get( { @@ -25,37 +31,60 @@ export function checkEntityDiscoveryEnabledRoute { - server.logger.debug('reading entity discovery API key from saved object'); - const apiKey = await readEntityDiscoveryAPIKey(server); + try { + logger.debug('reading entity discovery API key from saved object'); + const apiKey = await readEntityDiscoveryAPIKey(server); - if (apiKey === undefined) { - return res.ok({ body: { enabled: false, reason: ERROR_API_KEY_NOT_FOUND } }); - } + if (apiKey === undefined) { + return res.ok({ body: { enabled: false, reason: ERROR_API_KEY_NOT_FOUND } }); + } - server.logger.debug('validating existing entity discovery API key'); - const isValid = await checkIfEntityDiscoveryAPIKeyIsValid(server, apiKey); + logger.debug('validating existing entity discovery API key'); + const isValid = await checkIfEntityDiscoveryAPIKeyIsValid(server, apiKey); - if (!isValid) { - return res.ok({ body: { enabled: false, reason: ERROR_API_KEY_NOT_VALID } }); - } + if (!isValid) { + return res.ok({ body: { enabled: false, reason: ERROR_API_KEY_NOT_VALID } }); + } + + const fakeRequest = getFakeKibanaRequest({ id: apiKey.id, api_key: apiKey.apiKey }); + const soClient = server.core.savedObjects.getScopedClient(fakeRequest); + const esClient = server.core.elasticsearch.client.asScoped(fakeRequest).asCurrentUser; - const fakeRequest = getFakeKibanaRequest({ id: apiKey.id, api_key: apiKey.apiKey }); - const soClient = server.core.savedObjects.getScopedClient(fakeRequest); - const esClient = server.core.elasticsearch.client.asScoped(fakeRequest).asCurrentUser; + const entityDiscoveryState = await Promise.all( + builtInDefinitions.map(async (builtInDefinition) => { + const definitions = await findEntityDefinitions({ + esClient, + soClient, + id: builtInDefinition.id, + }); - const entityDiscoveryEnabled = await Promise.all( - builtInDefinitions.map(async (builtInDefinition) => { - const [definition] = await findEntityDefinitions({ - esClient, - soClient, - id: builtInDefinition.id, - }); + return definitions[0]; + }) + ).then((results) => + results.reduce( + (state, definition) => { + return { + installed: Boolean(state.installed && definition?.state.installed), + running: Boolean(state.running && definition?.state.running), + }; + }, + { installed: true, running: true } + ) + ); - return definition && definition.state.installed && definition.state.running; - }) - ).then((results) => results.every(Boolean)); + if (!entityDiscoveryState.installed) { + return res.ok({ body: { enabled: false, reason: ERROR_PARTIAL_BUILTIN_INSTALLATION } }); + } - return res.ok({ body: { enabled: entityDiscoveryEnabled } }); + if (!entityDiscoveryState.running) { + return res.ok({ body: { enabled: false, reason: ERROR_DEFINITION_STOPPED } }); + } + + return res.ok({ body: { enabled: true } }); + } catch (err) { + logger.error(err); + return res.customError({ statusCode: 500, body: err }); + } } ); } diff --git a/x-pack/plugins/observability_solution/entity_manager/server/routes/enablement/disable.ts b/x-pack/plugins/observability_solution/entity_manager/server/routes/enablement/disable.ts index e1312d0dff08f..33a1b60dab293 100644 --- a/x-pack/plugins/observability_solution/entity_manager/server/routes/enablement/disable.ts +++ b/x-pack/plugins/observability_solution/entity_manager/server/routes/enablement/disable.ts @@ -29,32 +29,37 @@ export function disableEntityDiscoveryRoute({ validate: false, }, async (context, req, res) => { - server.logger.debug('reading entity discovery API key from saved object'); - const apiKey = await readEntityDiscoveryAPIKey(server); + try { + server.logger.debug('reading entity discovery API key from saved object'); + const apiKey = await readEntityDiscoveryAPIKey(server); - if (apiKey === undefined) { - return res.ok({ body: { success: false, reason: ERROR_API_KEY_NOT_FOUND } }); - } + if (apiKey === undefined) { + return res.ok({ body: { success: false, reason: ERROR_API_KEY_NOT_FOUND } }); + } - server.logger.debug('validating existing entity discovery API key'); - const isValid = await checkIfEntityDiscoveryAPIKeyIsValid(server, apiKey); + server.logger.debug('validating existing entity discovery API key'); + const isValid = await checkIfEntityDiscoveryAPIKeyIsValid(server, apiKey); - if (!isValid) { - return res.ok({ body: { success: false, reason: ERROR_API_KEY_NOT_VALID } }); - } + if (!isValid) { + return res.ok({ body: { success: false, reason: ERROR_API_KEY_NOT_VALID } }); + } - const fakeRequest = getFakeKibanaRequest({ id: apiKey.id, api_key: apiKey.apiKey }); - const soClient = server.core.savedObjects.getScopedClient(fakeRequest); - const esClient = server.core.elasticsearch.client.asScoped(fakeRequest).asCurrentUser; + const fakeRequest = getFakeKibanaRequest({ id: apiKey.id, api_key: apiKey.apiKey }); + const soClient = server.core.savedObjects.getScopedClient(fakeRequest); + const esClient = server.core.elasticsearch.client.asScoped(fakeRequest).asCurrentUser; - await uninstallBuiltInEntityDefinitions({ soClient, esClient, logger }); + await uninstallBuiltInEntityDefinitions({ soClient, esClient, logger }); - await deleteEntityDiscoveryAPIKey((await context.core).savedObjects.client); - await server.security.authc.apiKeys.invalidateAsInternalUser({ - ids: [apiKey.id], - }); + await deleteEntityDiscoveryAPIKey((await context.core).savedObjects.client); + await server.security.authc.apiKeys.invalidateAsInternalUser({ + ids: [apiKey.id], + }); - return res.ok(); + return res.ok({ body: { success: true } }); + } catch (err) { + logger.error(err); + return res.customError({ statusCode: 500, body: err }); + } } ); } diff --git a/x-pack/plugins/observability_solution/entity_manager/server/routes/enablement/enable.ts b/x-pack/plugins/observability_solution/entity_manager/server/routes/enablement/enable.ts index 82f682fc82a61..392ec48c29b23 100644 --- a/x-pack/plugins/observability_solution/entity_manager/server/routes/enablement/enable.ts +++ b/x-pack/plugins/observability_solution/entity_manager/server/routes/enablement/enable.ts @@ -35,69 +35,74 @@ export function enableEntityDiscoveryRoute({ validate: false, }, async (context, req, res) => { - const apiKeysEnabled = await checkIfAPIKeysAreEnabled(server); - if (!apiKeysEnabled) { - return res.ok({ - body: { - success: false, - reason: ERROR_API_KEY_SERVICE_DISABLED, - message: - 'API key service is not enabled; try configuring `xpack.security.authc.api_key.enabled` in your elasticsearch config', - }, - }); - } + try { + const apiKeysEnabled = await checkIfAPIKeysAreEnabled(server); + if (!apiKeysEnabled) { + return res.ok({ + body: { + success: false, + reason: ERROR_API_KEY_SERVICE_DISABLED, + message: + 'API key service is not enabled; try configuring `xpack.security.authc.api_key.enabled` in your elasticsearch config', + }, + }); + } - const esClient = (await context.core).elasticsearch.client.asCurrentUser; - const canEnable = await canEnableEntityDiscovery(esClient); - if (!canEnable) { - return res.ok({ - body: { - success: false, - reason: ERROR_USER_NOT_AUTHORIZED, - message: - 'Current Kibana user does not have the required permissions to enable entity discovery', - }, - }); - } + const esClient = (await context.core).elasticsearch.client.asCurrentUser; + const canEnable = await canEnableEntityDiscovery(esClient); + if (!canEnable) { + return res.ok({ + body: { + success: false, + reason: ERROR_USER_NOT_AUTHORIZED, + message: + 'Current Kibana user does not have the required permissions to enable entity discovery', + }, + }); + } - const soClient = (await context.core).savedObjects.getClient({ - includedHiddenTypes: [EntityDiscoveryApiKeyType.name], - }); + const soClient = (await context.core).savedObjects.getClient({ + includedHiddenTypes: [EntityDiscoveryApiKeyType.name], + }); - const existingApiKey = await readEntityDiscoveryAPIKey(server); + const existingApiKey = await readEntityDiscoveryAPIKey(server); - if (existingApiKey !== undefined) { - const isValid = await checkIfEntityDiscoveryAPIKeyIsValid(server, existingApiKey); + if (existingApiKey !== undefined) { + const isValid = await checkIfEntityDiscoveryAPIKeyIsValid(server, existingApiKey); - if (!isValid) { - await deleteEntityDiscoveryAPIKey(soClient); - await server.security.authc.apiKeys.invalidateAsInternalUser({ - ids: [existingApiKey.id], - }); + if (!isValid) { + await deleteEntityDiscoveryAPIKey(soClient); + await server.security.authc.apiKeys.invalidateAsInternalUser({ + ids: [existingApiKey.id], + }); + } } - } - const apiKey = await generateEntityDiscoveryAPIKey(server, req); + const apiKey = await generateEntityDiscoveryAPIKey(server, req); - if (apiKey === undefined) { - throw new Error('could not generate entity discovery API key'); - } + if (apiKey === undefined) { + throw new Error('could not generate entity discovery API key'); + } - await saveEntityDiscoveryAPIKey(soClient, apiKey); + await saveEntityDiscoveryAPIKey(soClient, apiKey); - const fakeRequest = getFakeKibanaRequest({ id: apiKey.id, api_key: apiKey.apiKey }); - const scopedSoClient = server.core.savedObjects.getScopedClient(fakeRequest); - const scopedEsClient = server.core.elasticsearch.client.asScoped(fakeRequest).asCurrentUser; + const fakeRequest = getFakeKibanaRequest({ id: apiKey.id, api_key: apiKey.apiKey }); + const scopedSoClient = server.core.savedObjects.getScopedClient(fakeRequest); + const scopedEsClient = server.core.elasticsearch.client.asScoped(fakeRequest).asCurrentUser; - await installBuiltInEntityDefinitions({ - logger, - builtInDefinitions, - spaceId: 'default', - esClient: scopedEsClient, - soClient: scopedSoClient, - }); + await installBuiltInEntityDefinitions({ + logger, + builtInDefinitions, + spaceId: 'default', + esClient: scopedEsClient, + soClient: scopedSoClient, + }); - return res.ok({ body: { success: true } }); + return res.ok({ body: { success: true } }); + } catch (err) { + logger.error(err); + return res.customError({ statusCode: 500, body: err }); + } } ); }