From 3d54923123b37ff0c1d7e51067ac30a20965461b Mon Sep 17 00:00:00 2001 From: Ying Mao Date: Mon, 7 Apr 2025 16:46:02 -0400 Subject: [PATCH] [Response Ops][Reporting] Add health API to inform whether conditions are sufficient for scheduled reports (#216857) Resolves https://github.com/elastic/kibana/issues/216319 ## Summary Adds an internal reporting health API to return whether conditions are sufficient to support scheduled reports. For scheduled reporting, we need for security and API keys to be enabled in Elasticsearch and for a permanent encryption key to be set for the encrypted saved objects plugin. ``` GET kbn:/internal/reporting/_health Response { "has_permanent_encryption_key": true, "is_sufficiently_secure": true } ``` The issue also mentions returning whether a preconfigured email service is configured, but that will be done as part of the main scheduled reporting task. ## To Verify 1. Run kibana and ES with no special flags, both flags should be `true` 2. Run ES with `-E xpack.security.enabled=false`. `is_sufficiently_secure` should be set to `false` 3. Run ES With `-E xpack.security.authc.api_key.enabled=false`. `is_sufficient_secure` should be set to `false` Note that in dev mode, an encryption key is auto-set if not defined in the Kibana yml so `has_permanent_encryption_key` will always return `true` in dev mode. --------- Co-authored-by: Elastic Machine --- .../private/kbn-reporting/common/routes.ts | 1 + .../private/kbn-reporting/common/types.ts | 5 ++ .../plugins/private/reporting/kibana.jsonc | 1 + .../plugins/private/reporting/server/core.ts | 56 +++++++++++++++++-- .../private/reporting/server/routes/index.ts | 2 + .../server/routes/internal/health/health.ts | 44 +++++++++++++++ .../server/routes/internal/health/index.ts | 8 +++ .../create_mock_reportingplugin.ts | 2 + .../plugins/private/reporting/server/types.ts | 2 + .../plugins/private/reporting/tsconfig.json | 1 + 10 files changed, 116 insertions(+), 6 deletions(-) create mode 100644 x-pack/platform/plugins/private/reporting/server/routes/internal/health/health.ts create mode 100644 x-pack/platform/plugins/private/reporting/server/routes/internal/health/index.ts diff --git a/src/platform/packages/private/kbn-reporting/common/routes.ts b/src/platform/packages/private/kbn-reporting/common/routes.ts index dc01746fcc8d..4fb56700cda2 100644 --- a/src/platform/packages/private/kbn-reporting/common/routes.ts +++ b/src/platform/packages/private/kbn-reporting/common/routes.ts @@ -24,6 +24,7 @@ export const INTERNAL_ROUTES = { DELETE_PREFIX: prefixInternalPath + '/jobs/delete', // docId is added to the final path DOWNLOAD_PREFIX: prefixInternalPath + '/jobs/download', // docId is added to the final path }, + HEALTH: prefixInternalPath + '/_health', GENERATE_PREFIX: prefixInternalPath + '/generate', // exportTypeId is added to the final path }; diff --git a/src/platform/packages/private/kbn-reporting/common/types.ts b/src/platform/packages/private/kbn-reporting/common/types.ts index 2b9aaa7b1c9f..a7931e642f9c 100644 --- a/src/platform/packages/private/kbn-reporting/common/types.ts +++ b/src/platform/packages/private/kbn-reporting/common/types.ts @@ -109,6 +109,11 @@ export interface ReportingServerInfo { uuid: string; } +export interface ReportingHealthInfo { + isSufficientlySecure: boolean; + hasPermanentEncryptionKey: boolean; +} + export type IlmPolicyMigrationStatus = 'policy-not-found' | 'indices-not-managed-by-policy' | 'ok'; export interface IlmPolicyStatusResponse { diff --git a/x-pack/platform/plugins/private/reporting/kibana.jsonc b/x-pack/platform/plugins/private/reporting/kibana.jsonc index 8e53ca3a489a..ce6b130018af 100644 --- a/x-pack/platform/plugins/private/reporting/kibana.jsonc +++ b/x-pack/platform/plugins/private/reporting/kibana.jsonc @@ -18,6 +18,7 @@ "requiredPlugins": [ "data", "discover", + "encryptedSavedObjects", "fieldFormats", "home", "management", diff --git a/x-pack/platform/plugins/private/reporting/server/core.ts b/x-pack/platform/plugins/private/reporting/server/core.ts index 117849365de9..bbd3d8d6bb50 100644 --- a/x-pack/platform/plugins/private/reporting/server/core.ts +++ b/x-pack/platform/plugins/private/reporting/server/core.ts @@ -28,7 +28,7 @@ import type { DiscoverServerPluginStart } from '@kbn/discover-plugin/server'; import type { FeaturesPluginSetup } from '@kbn/features-plugin/server'; import type { FieldFormatsStart } from '@kbn/field-formats-plugin/server'; import type { LicensingPluginStart } from '@kbn/licensing-plugin/server'; -import type { ReportingServerInfo } from '@kbn/reporting-common/types'; +import type { ReportingHealthInfo, ReportingServerInfo } from '@kbn/reporting-common/types'; import { CsvSearchSourceExportType, CsvV2ExportType } from '@kbn/reporting-export-types-csv'; import { PdfExportType, PdfV1ExportType } from '@kbn/reporting-export-types-pdf'; import { PngExportType } from '@kbn/reporting-export-types-png'; @@ -46,6 +46,7 @@ import type { UsageCounter } from '@kbn/usage-collection-plugin/server'; import { checkLicense } from '@kbn/reporting-server/check_license'; import { ExportTypesRegistry } from '@kbn/reporting-server/export_types_registry'; +import { EncryptedSavedObjectsPluginSetup } from '@kbn/encrypted-saved-objects-plugin/server'; import type { ReportingSetup } from '.'; import { createConfig } from './config'; import { reportingEventLoggerFactory } from './lib/event_logger/logger'; @@ -56,15 +57,16 @@ import { EventTracker } from './usage'; export interface ReportingInternalSetup { basePath: Pick; - router: ReportingPluginRouter; + docLinks: DocLinksServiceSetup; + encryptedSavedObjects: EncryptedSavedObjectsPluginSetup; features: FeaturesPluginSetup; + logger: Logger; + router: ReportingPluginRouter; security?: SecurityPluginSetup; spaces?: SpacesPluginSetup; - usageCounter?: UsageCounter; - taskManager: TaskManagerSetupContract; - logger: Logger; status: StatusServiceSetup; - docLinks: DocLinksServiceSetup; + taskManager: TaskManagerSetupContract; + usageCounter?: UsageCounter; } export interface ReportingInternalStart { @@ -247,6 +249,32 @@ export class ReportingCore { }; } + public async getHealthInfo(): Promise { + const { encryptedSavedObjects } = this.getPluginSetupDeps(); + const { securityService } = await this.getPluginStartDeps(); + const isSecurityEnabled = await this.isSecurityEnabled(); + + let isSufficientlySecure: boolean; + const apiKeysAreEnabled = (await securityService.authc.apiKeys.areAPIKeysEnabled()) ?? false; + + if (isSecurityEnabled === null) { + isSufficientlySecure = false; + } else { + // if esSecurityIsEnabled = true, then areApiKeysEnabled must be true to be considered secure + // if esSecurityIsEnabled = false, then it does not matter what areApiKeysEnabled is + if (!isSecurityEnabled) { + isSufficientlySecure = false; + } else { + isSufficientlySecure = apiKeysAreEnabled; + } + } + + return { + isSufficientlySecure, + hasPermanentEncryptionKey: encryptedSavedObjects.canEncrypt, + }; + } + /* * Gives synchronous access to the config */ @@ -307,6 +335,22 @@ export class ReportingCore { ); } + public async isSecurityEnabled() { + const { license$ } = (await this.getPluginStartDeps()).licensing; + return await Rx.firstValueFrom( + license$.pipe( + map((license) => { + if (!license || !license?.isAvailable) { + return null; + } + + const { isEnabled } = license.getFeature('security'); + return isEnabled; + }) + ) + ); + } + /* * Gives synchronous access to the setupDeps */ diff --git a/x-pack/platform/plugins/private/reporting/server/routes/index.ts b/x-pack/platform/plugins/private/reporting/server/routes/index.ts index 3473f660f647..f9fbd12802e2 100644 --- a/x-pack/platform/plugins/private/reporting/server/routes/index.ts +++ b/x-pack/platform/plugins/private/reporting/server/routes/index.ts @@ -9,6 +9,7 @@ import type { Logger } from '@kbn/core/server'; import { ReportingCore } from '..'; import { registerDeprecationsRoutes } from './internal/deprecations/deprecations'; import { registerDiagnosticRoutes } from './internal/diagnostic'; +import { registerHealthRoute } from './internal/health'; import { registerGenerationRoutesInternal } from './internal/generate/generate_from_jobparams'; import { registerJobInfoRoutesInternal } from './internal/management/jobs'; import { registerGenerationRoutesPublic } from './public/generate_from_jobparams'; @@ -16,6 +17,7 @@ import { registerJobInfoRoutesPublic } from './public/jobs'; export function registerRoutes(reporting: ReportingCore, logger: Logger) { registerDeprecationsRoutes(reporting, logger); + registerHealthRoute(reporting, logger); registerDiagnosticRoutes(reporting, logger); registerGenerationRoutesInternal(reporting, logger); registerJobInfoRoutesInternal(reporting); diff --git a/x-pack/platform/plugins/private/reporting/server/routes/internal/health/health.ts b/x-pack/platform/plugins/private/reporting/server/routes/internal/health/health.ts new file mode 100644 index 000000000000..e35ea67a22e4 --- /dev/null +++ b/x-pack/platform/plugins/private/reporting/server/routes/internal/health/health.ts @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { Logger } from '@kbn/core/server'; +import { INTERNAL_ROUTES } from '@kbn/reporting-common'; +import type { ReportingCore } from '../../..'; +import { authorizedUserPreRouting } from '../../common'; + +const path = INTERNAL_ROUTES.HEALTH; +export const registerHealthRoute = (reporting: ReportingCore, logger: Logger) => { + const { router } = reporting.getPluginSetupDeps(); + + router.get( + { + path, + security: { + authz: { + enabled: false, + reason: 'This route is opted out from authorization', + }, + }, + validate: false, + options: { access: 'internal' }, + }, + authorizedUserPreRouting(reporting, async (_user, _context, req, res) => { + try { + const healthInfo = await reporting.getHealthInfo(); + return res.ok({ + body: { + has_permanent_encryption_key: healthInfo.hasPermanentEncryptionKey, + is_sufficiently_secure: healthInfo.isSufficientlySecure, + }, + }); + } catch (err) { + logger.error(err); + return res.custom({ statusCode: 500 }); + } + }) + ); +}; diff --git a/x-pack/platform/plugins/private/reporting/server/routes/internal/health/index.ts b/x-pack/platform/plugins/private/reporting/server/routes/internal/health/index.ts new file mode 100644 index 000000000000..a6d0ec84a5bf --- /dev/null +++ b/x-pack/platform/plugins/private/reporting/server/routes/internal/health/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { registerHealthRoute } from './health'; diff --git a/x-pack/platform/plugins/private/reporting/server/test_helpers/create_mock_reportingplugin.ts b/x-pack/platform/plugins/private/reporting/server/test_helpers/create_mock_reportingplugin.ts index c5c0185785f5..95113dca7ad0 100644 --- a/x-pack/platform/plugins/private/reporting/server/test_helpers/create_mock_reportingplugin.ts +++ b/x-pack/platform/plugins/private/reporting/server/test_helpers/create_mock_reportingplugin.ts @@ -19,6 +19,7 @@ import { } from '@kbn/core/server/mocks'; import { dataPluginMock } from '@kbn/data-plugin/server/mocks'; import { discoverPluginMock } from '@kbn/discover-plugin/server/mocks'; +import { encryptedSavedObjectsMock } from '@kbn/encrypted-saved-objects-plugin/server/mocks'; import { featuresPluginMock } from '@kbn/features-plugin/server/mocks'; import { FieldFormatsRegistry } from '@kbn/field-formats-plugin/common'; import { fieldFormatsMock } from '@kbn/field-formats-plugin/common/mocks'; @@ -38,6 +39,7 @@ export const createMockPluginSetup = ( setupMock: Partial> ): ReportingInternalSetup => { return { + encryptedSavedObjects: encryptedSavedObjectsMock.createSetup(), features: featuresPluginMock.createSetup(), basePath: { set: jest.fn() }, router: { get: jest.fn(), post: jest.fn(), put: jest.fn(), delete: jest.fn() }, diff --git a/x-pack/platform/plugins/private/reporting/server/types.ts b/x-pack/platform/plugins/private/reporting/server/types.ts index 221ec83e0b3e..4d4b25177ab1 100644 --- a/x-pack/platform/plugins/private/reporting/server/types.ts +++ b/x-pack/platform/plugins/private/reporting/server/types.ts @@ -31,6 +31,7 @@ import type { UsageCollectionSetup } from '@kbn/usage-collection-plugin/server'; import { ExportTypesRegistry } from '@kbn/reporting-server/export_types_registry'; import type { AuthenticatedUser } from '@kbn/core-security-common'; +import { EncryptedSavedObjectsPluginSetup } from '@kbn/encrypted-saved-objects-plugin/server'; /** * Plugin Setup Contract @@ -48,6 +49,7 @@ export type ReportingUser = { username: AuthenticatedUser['username'] } | false; export type ScrollConfig = ReportingConfigType['csv']['scroll']; export interface ReportingSetupDeps { + encryptedSavedObjects: EncryptedSavedObjectsPluginSetup; features: FeaturesPluginSetup; screenshotMode: ScreenshotModePluginSetup; taskManager: TaskManagerSetupContract; diff --git a/x-pack/platform/plugins/private/reporting/tsconfig.json b/x-pack/platform/plugins/private/reporting/tsconfig.json index f0b105990b7a..bd7fa041328a 100644 --- a/x-pack/platform/plugins/private/reporting/tsconfig.json +++ b/x-pack/platform/plugins/private/reporting/tsconfig.json @@ -14,6 +14,7 @@ "@kbn/screenshot-mode-plugin", "@kbn/share-plugin", "@kbn/ui-actions-plugin", + "@kbn/encrypted-saved-objects-plugin", "@kbn/usage-collection-plugin", "@kbn/field-formats-plugin", "@kbn/features-plugin",