From 75ce1e397a03908938fb78a94593a3024571400c Mon Sep 17 00:00:00 2001 From: Shahzad Date: Tue, 15 Nov 2022 16:29:02 +0100 Subject: [PATCH] [Synthetics] Validate API keys (#143867) Co-authored-by: Dominique Clarke Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Fixes https://github.com/elastic/kibana/issues/142875 --- .../authentication/api_keys/api_keys.mock.ts | 1 + .../authentication/api_keys/api_keys.test.ts | 46 ++++++ .../authentication/api_keys/api_keys.ts | 40 +++++ .../api_keys/fake_kibana_request.ts | 32 ++++ .../authentication/authentication_service.ts | 3 + x-pack/plugins/security/server/plugin.test.ts | 1 + .../runtime_types/monitor_management/state.ts | 1 + .../apps/synthetics/hooks/use_enablement.ts | 1 + .../state/synthetics_enablement/index.ts | 2 + .../hooks/use_enablement.ts | 1 + .../monitor_management/disabled_callout.tsx | 87 +++++++++++ .../invalid_api_key_callout.tsx | 80 ++++++++++ .../monitor_management/monitor_management.tsx | 74 +-------- .../state/api/monitor_management.ts | 2 +- .../state/reducers/monitor_management.ts | 33 ++--- .../lib/adapters/framework/adapter_types.ts | 2 + .../legacy_uptime/lib/requests/index.ts | 2 - .../lib/saved_objects/service_api_key.ts | 30 +++- x-pack/plugins/synthetics/server/plugin.ts | 1 + .../routes/monitor_cruds/get_api_key.ts | 3 - .../routes/synthetics_service/enablement.ts | 32 ++-- .../authentication/check_has_privilege.ts | 24 +++ .../synthetics_service/get_api_key.test.ts | 24 ++- .../server/synthetics_service/get_api_key.ts | 140 +++++++++--------- .../synthetics_service/service_api_client.ts | 6 +- .../synthetics_service/synthetics_service.ts | 136 +++++++---------- .../utils/fake_kibana_request.ts | 31 ++++ .../apis/synthetics/synthetics_enablement.ts | 10 +- 28 files changed, 574 insertions(+), 271 deletions(-) create mode 100644 x-pack/plugins/security/server/authentication/api_keys/fake_kibana_request.ts create mode 100644 x-pack/plugins/synthetics/public/legacy_uptime/pages/monitor_management/disabled_callout.tsx create mode 100644 x-pack/plugins/synthetics/public/legacy_uptime/pages/monitor_management/invalid_api_key_callout.tsx create mode 100644 x-pack/plugins/synthetics/server/synthetics_service/authentication/check_has_privilege.ts create mode 100644 x-pack/plugins/synthetics/server/synthetics_service/utils/fake_kibana_request.ts diff --git a/x-pack/plugins/security/server/authentication/api_keys/api_keys.mock.ts b/x-pack/plugins/security/server/authentication/api_keys/api_keys.mock.ts index e82efeb5168d..0c18119ad205 100644 --- a/x-pack/plugins/security/server/authentication/api_keys/api_keys.mock.ts +++ b/x-pack/plugins/security/server/authentication/api_keys/api_keys.mock.ts @@ -14,6 +14,7 @@ export const apiKeysMock = { areAPIKeysEnabled: jest.fn(), create: jest.fn(), grantAsInternalUser: jest.fn(), + validate: jest.fn(), invalidate: jest.fn(), invalidateAsInternalUser: jest.fn(), }), diff --git a/x-pack/plugins/security/server/authentication/api_keys/api_keys.test.ts b/x-pack/plugins/security/server/authentication/api_keys/api_keys.test.ts index 2aa318acff59..8c757cd9cfc0 100644 --- a/x-pack/plugins/security/server/authentication/api_keys/api_keys.test.ts +++ b/x-pack/plugins/security/server/authentication/api_keys/api_keys.test.ts @@ -18,6 +18,7 @@ import { ALL_SPACES_ID } from '../../../common/constants'; import type { SecurityLicense } from '../../../common/licensing'; import { licenseMock } from '../../../common/licensing/index.mock'; import { APIKeys } from './api_keys'; +import { getFakeKibanaRequest } from './fake_kibana_request'; const encodeToBase64 = (str: string) => Buffer.from(str).toString('base64'); @@ -407,6 +408,51 @@ describe('API Keys', () => { }); }); + describe('validate()', () => { + it('returns false when security feature is disabled', async () => { + mockLicense.isEnabled.mockReturnValue(false); + const result = await apiKeys.validate({ + id: '123', + api_key: 'abc123', + }); + expect(result).toEqual(false); + expect(mockClusterClient.asScoped).not.toHaveBeenCalled(); + }); + + it('calls callCluster with proper parameters', async () => { + mockLicense.isEnabled.mockReturnValue(true); + const params = { + id: '123', + api_key: 'abc123', + }; + const result = await apiKeys.validate(params); + expect(result).toEqual(true); + + const fakeRequest = getFakeKibanaRequest(params); + + const { id, uuid, ...restFake } = fakeRequest; + + expect(mockClusterClient.asScoped).toHaveBeenCalledWith(expect.objectContaining(restFake)); + expect( + mockClusterClient.asScoped().asCurrentUser.security.authenticate + ).toHaveBeenCalledWith(); + }); + + it('returns false if cannot authenticate with the API key', async () => { + mockLicense.isEnabled.mockReturnValue(true); + mockScopedClusterClient.asCurrentUser.security.authenticate.mockRejectedValue(new Error()); + const params = { id: '123', api_key: 'abc123' }; + + await expect(apiKeys.validate(params)).resolves.toEqual(false); + + const { id, uuid, ...restFake } = getFakeKibanaRequest(params); + expect(mockClusterClient.asScoped).toHaveBeenCalledWith(expect.objectContaining(restFake)); + expect( + mockClusterClient.asScoped().asCurrentUser.security.authenticate + ).toHaveBeenCalledTimes(1); + }); + }); + describe('invalidateAsInternalUser()', () => { it('returns null when security feature is disabled', async () => { mockLicense.isEnabled.mockReturnValue(false); diff --git a/x-pack/plugins/security/server/authentication/api_keys/api_keys.ts b/x-pack/plugins/security/server/authentication/api_keys/api_keys.ts index 0eef6fac7403..2f662554159e 100644 --- a/x-pack/plugins/security/server/authentication/api_keys/api_keys.ts +++ b/x-pack/plugins/security/server/authentication/api_keys/api_keys.ts @@ -18,6 +18,7 @@ import { BasicHTTPAuthorizationHeaderCredentials, HTTPAuthorizationHeader, } from '../http_authentication'; +import { getFakeKibanaRequest } from './fake_kibana_request'; /** * Represents the options to create an APIKey class instance that will be @@ -139,6 +140,21 @@ export interface InvalidateAPIKeyResult { }>; } +/** + * Represents the parameters for validating API Key credentials. + */ +export interface ValidateAPIKeyParams { + /** + * Unique id for this API key + */ + id: string; + + /** + * Generated API Key (secret) + */ + api_key: string; +} + /** * Class responsible for managing Elasticsearch API keys. */ @@ -335,6 +351,30 @@ export class APIKeys { return result; } + /** + * Tries to validate an API key. + * @param apiKeyPrams ValidateAPIKeyParams. + */ + async validate(apiKeyPrams: ValidateAPIKeyParams): Promise { + if (!this.license.isEnabled()) { + return false; + } + + const fakeRequest = getFakeKibanaRequest(apiKeyPrams); + + this.logger.debug(`Trying to validate an API key`); + + try { + await this.clusterClient.asScoped(fakeRequest).asCurrentUser.security.authenticate(); + this.logger.debug(`API key was validated successfully`); + return true; + } catch (e) { + this.logger.info(`Failed to validate API key: ${e.message}`); + } + + return false; + } + private doesErrorIndicateAPIKeysAreDisabled(e: Record) { const disabledFeature = e.body?.error?.['disabled.feature']; return disabledFeature === 'api_keys'; diff --git a/x-pack/plugins/security/server/authentication/api_keys/fake_kibana_request.ts b/x-pack/plugins/security/server/authentication/api_keys/fake_kibana_request.ts new file mode 100644 index 000000000000..a78accb416e6 --- /dev/null +++ b/x-pack/plugins/security/server/authentication/api_keys/fake_kibana_request.ts @@ -0,0 +1,32 @@ +/* + * 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 { Request } from '@hapi/hapi'; + +import { CoreKibanaRequest } from '@kbn/core/server'; + +export function getFakeKibanaRequest(apiKey: { id: string; api_key: string }) { + const requestHeaders: Record = {}; + + requestHeaders.authorization = `ApiKey ${Buffer.from(`${apiKey.id}:${apiKey.api_key}`).toString( + 'base64' + )}`; + + return CoreKibanaRequest.from({ + headers: requestHeaders, + path: '/', + route: { settings: {} }, + url: { + href: '/', + }, + raw: { + req: { + url: '/', + }, + }, + } as unknown as Request); +} diff --git a/x-pack/plugins/security/server/authentication/authentication_service.ts b/x-pack/plugins/security/server/authentication/authentication_service.ts index c22ac5fceecb..d0ffb92bf6c4 100644 --- a/x-pack/plugins/security/server/authentication/authentication_service.ts +++ b/x-pack/plugins/security/server/authentication/authentication_service.ts @@ -63,6 +63,7 @@ export interface InternalAuthenticationServiceStart extends AuthenticationServic | 'areAPIKeysEnabled' | 'create' | 'invalidate' + | 'validate' | 'grantAsInternalUser' | 'invalidateAsInternalUser' >; @@ -81,6 +82,7 @@ export interface AuthenticationServiceStart { | 'areAPIKeysEnabled' | 'create' | 'invalidate' + | 'validate' | 'grantAsInternalUser' | 'invalidateAsInternalUser' >; @@ -354,6 +356,7 @@ export class AuthenticationService { create: apiKeys.create.bind(apiKeys), grantAsInternalUser: apiKeys.grantAsInternalUser.bind(apiKeys), invalidate: apiKeys.invalidate.bind(apiKeys), + validate: apiKeys.validate.bind(apiKeys), invalidateAsInternalUser: apiKeys.invalidateAsInternalUser.bind(apiKeys), }, diff --git a/x-pack/plugins/security/server/plugin.test.ts b/x-pack/plugins/security/server/plugin.test.ts index 10e3b99484f5..0871db0bbde7 100644 --- a/x-pack/plugins/security/server/plugin.test.ts +++ b/x-pack/plugins/security/server/plugin.test.ts @@ -141,6 +141,7 @@ describe('Security Plugin', () => { "grantAsInternalUser": [Function], "invalidate": [Function], "invalidateAsInternalUser": [Function], + "validate": [Function], }, "getCurrentUser": [Function], }, diff --git a/x-pack/plugins/synthetics/common/runtime_types/monitor_management/state.ts b/x-pack/plugins/synthetics/common/runtime_types/monitor_management/state.ts index 6711c1ad4d3e..d997c554eef0 100644 --- a/x-pack/plugins/synthetics/common/runtime_types/monitor_management/state.ts +++ b/x-pack/plugins/synthetics/common/runtime_types/monitor_management/state.ts @@ -40,6 +40,7 @@ export const MonitorManagementEnablementResultCodec = t.type({ canEnable: t.boolean, canManageApiKeys: t.boolean, areApiKeysEnabled: t.boolean, + isValidApiKey: t.boolean, }); export type MonitorManagementEnablementResult = t.TypeOf< diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/hooks/use_enablement.ts b/x-pack/plugins/synthetics/public/apps/synthetics/hooks/use_enablement.ts index 74a430240b61..a89d323b6372 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/hooks/use_enablement.ts +++ b/x-pack/plugins/synthetics/public/apps/synthetics/hooks/use_enablement.ts @@ -31,6 +31,7 @@ export function useEnablement() { canEnable: enablement?.canEnable, isEnabled: enablement?.isEnabled, }, + invalidApiKeyError: enablement ? !Boolean(enablement?.isValidApiKey) : false, error, loading, enableSynthetics: useCallback(() => dispatch(enableSynthetics()), [dispatch]), diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/state/synthetics_enablement/index.ts b/x-pack/plugins/synthetics/public/apps/synthetics/state/synthetics_enablement/index.ts index 3bf9ff69bf00..06ec4dd3b26b 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/state/synthetics_enablement/index.ts +++ b/x-pack/plugins/synthetics/public/apps/synthetics/state/synthetics_enablement/index.ts @@ -58,6 +58,7 @@ export const syntheticsEnablementReducer = createReducer(initialState, (builder) areApiKeysEnabled: state.enablement?.areApiKeysEnabled ?? false, canManageApiKeys: state.enablement?.canManageApiKeys ?? false, isEnabled: false, + isValidApiKey: true, }; }) .addCase(disableSyntheticsFailure, (state, action) => { @@ -75,6 +76,7 @@ export const syntheticsEnablementReducer = createReducer(initialState, (builder) canEnable: state.enablement?.canEnable ?? false, areApiKeysEnabled: state.enablement?.areApiKeysEnabled ?? false, canManageApiKeys: state.enablement?.canManageApiKeys ?? false, + isValidApiKey: state.enablement?.isValidApiKey ?? false, isEnabled: true, }; }) diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor_management/hooks/use_enablement.ts b/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor_management/hooks/use_enablement.ts index f376a8bc9023..e9ebd839f47f 100644 --- a/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor_management/hooks/use_enablement.ts +++ b/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor_management/hooks/use_enablement.ts @@ -37,6 +37,7 @@ export function useEnablement() { canEnable: enablement?.canEnable, isEnabled: enablement?.isEnabled, }, + invalidApiKeyError: enablement ? !Boolean(enablement?.isValidApiKey) : false, error, loading, totalMonitors: total, diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/pages/monitor_management/disabled_callout.tsx b/x-pack/plugins/synthetics/public/legacy_uptime/pages/monitor_management/disabled_callout.tsx new file mode 100644 index 000000000000..48e5564621a1 --- /dev/null +++ b/x-pack/plugins/synthetics/public/legacy_uptime/pages/monitor_management/disabled_callout.tsx @@ -0,0 +1,87 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiButton, EuiCallOut, EuiLink, EuiSpacer } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { useSelector } from 'react-redux'; +import { monitorManagementListSelector } from '../../state/selectors'; +import { useEnablement } from '../../components/monitor_management/hooks/use_enablement'; + +export const DisabledCallout = () => { + const { enablement, enableSynthetics } = useEnablement(); + const { list: monitorList } = useSelector(monitorManagementListSelector); + + const showDisableCallout = !enablement.isEnabled && monitorList.total && monitorList.total > 0; + + if (!showDisableCallout) { + return null; + } + + return ( + <> + +

{CALLOUT_MANAGEMENT_DESCRIPTION}

+ {enablement.canEnable ? ( + { + enableSynthetics(); + }} + > + {SYNTHETICS_ENABLE_LABEL} + + ) : ( +

+ {CALLOUT_MANAGEMENT_CONTACT_ADMIN}{' '} + + {LEARN_MORE_LABEL} + +

+ )} +
+ + + ); +}; + +const LEARN_MORE_LABEL = i18n.translate( + 'xpack.synthetics.monitorManagement.manageMonitorLoadingLabel.disabledCallout.learnMore', + { + defaultMessage: 'Learn more', + } +); + +const CALLOUT_MANAGEMENT_DISABLED = i18n.translate( + 'xpack.synthetics.monitorManagement.callout.disabled', + { + defaultMessage: 'Monitor Management is disabled', + } +); + +const CALLOUT_MANAGEMENT_CONTACT_ADMIN = i18n.translate( + 'xpack.synthetics.monitorManagement.disabledCallout.adminContact', + { + defaultMessage: 'Contact your administrator to enable Monitor Management.', + } +); + +const CALLOUT_MANAGEMENT_DESCRIPTION = i18n.translate( + 'xpack.synthetics.monitorManagement.disabledCallout.description.disabled', + { + defaultMessage: + 'Monitor Management is currently disabled and your existing monitors are paused. You can enable Monitor Management to run your monitors.', + } +); + +const SYNTHETICS_ENABLE_LABEL = i18n.translate( + 'xpack.synthetics.monitorManagement.syntheticsEnableLabel.management', + { + defaultMessage: 'Enable Monitor Management', + } +); diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/pages/monitor_management/invalid_api_key_callout.tsx b/x-pack/plugins/synthetics/public/legacy_uptime/pages/monitor_management/invalid_api_key_callout.tsx new file mode 100644 index 000000000000..aa8b3112955c --- /dev/null +++ b/x-pack/plugins/synthetics/public/legacy_uptime/pages/monitor_management/invalid_api_key_callout.tsx @@ -0,0 +1,80 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiButton, EuiCallOut, EuiLink, EuiSpacer } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { useEnablement } from '../../components/monitor_management/hooks/use_enablement'; + +export const InvalidApiKeyCalloutCallout = () => { + const { enablement, enableSynthetics, invalidApiKeyError } = useEnablement(); + + if (!invalidApiKeyError || !enablement.isEnabled) { + return null; + } + + return ( + <> + +

{CALLOUT_MANAGEMENT_DESCRIPTION}

+ {enablement.canEnable ? ( + { + enableSynthetics(); + }} + > + {SYNTHETICS_ENABLE_LABEL} + + ) : ( +

+ {CALLOUT_MANAGEMENT_CONTACT_ADMIN}{' '} + + {LEARN_MORE_LABEL} + +

+ )} +
+ + + ); +}; + +const LEARN_MORE_LABEL = i18n.translate( + 'xpack.synthetics.monitorManagement.manageMonitorLoadingLabel.callout.invalidKey', + { + defaultMessage: 'Learn more', + } +); + +const API_KEY_MISSING = i18n.translate('xpack.synthetics.monitorManagement.callout.apiKeyMissing', { + defaultMessage: 'Monitor Management is currently disabled because of missing API key', +}); + +const CALLOUT_MANAGEMENT_CONTACT_ADMIN = i18n.translate( + 'xpack.synthetics.monitorManagement.callout.disabledCallout.invalidKey', + { + defaultMessage: 'Contact your administrator to enable Monitor Management.', + } +); + +const CALLOUT_MANAGEMENT_DESCRIPTION = i18n.translate( + 'xpack.synthetics.monitorManagement.callout.description.invalidKey', + { + defaultMessage: + `Monitor Management is currently disabled. To run your monitors in one of Elastic's global managed testing locations,` + + 'you need to re-enable monitor management.', + } +); + +const SYNTHETICS_ENABLE_LABEL = i18n.translate( + 'xpack.synthetics.monitorManagement.syntheticsEnableLabel.invalidKey', + { + defaultMessage: 'Enable monitor management', + } +); diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/pages/monitor_management/monitor_management.tsx b/x-pack/plugins/synthetics/public/legacy_uptime/pages/monitor_management/monitor_management.tsx index 852819977e15..c7ff463cca95 100644 --- a/x-pack/plugins/synthetics/public/legacy_uptime/pages/monitor_management/monitor_management.tsx +++ b/x-pack/plugins/synthetics/public/legacy_uptime/pages/monitor_management/monitor_management.tsx @@ -8,8 +8,9 @@ import React, { useEffect, useRef, useState } from 'react'; import { i18n } from '@kbn/i18n'; import { useSelector } from 'react-redux'; -import { EuiCallOut, EuiButton, EuiSpacer, EuiLink } from '@elastic/eui'; import { useTrackPageview } from '@kbn/observability-plugin/public'; +import { InvalidApiKeyCalloutCallout } from './invalid_api_key_callout'; +import { DisabledCallout } from './disabled_callout'; import { ManageLocationsPortal } from '../../components/monitor_management/manage_locations/manage_locations'; import { monitorManagementListSelector } from '../../state/selectors'; import { useMonitorManagementBreadcrumbs } from './use_monitor_management_breadcrumbs'; @@ -27,12 +28,7 @@ export const MonitorManagementPage: React.FC = () => { useMonitorManagementBreadcrumbs(); const [shouldFocusEnablementButton, setShouldFocusEnablementButton] = useState(false); - const { - error: enablementError, - enablement, - loading: enablementLoading, - enableSynthetics, - } = useEnablement(); + const { error: enablementError, enablement, loading: enablementLoading } = useEnablement(); const { loading: locationsLoading } = useLocations(); const { list: monitorList } = useSelector(monitorManagementListSelector); const { isEnabled } = enablement; @@ -61,32 +57,8 @@ export const MonitorManagementPage: React.FC = () => { errorTitle={ERROR_HEADING_LABEL} errorBody={ERROR_HEADING_BODY} > - {!isEnabled && monitorList.total && monitorList.total > 0 ? ( - <> - -

{CALLOUT_MANAGEMENT_DESCRIPTION}

- {enablement.canEnable ? ( - { - enableSynthetics(); - }} - > - {SYNTHETICS_ENABLE_LABEL} - - ) : ( -

- {CALLOUT_MANAGEMENT_CONTACT_ADMIN}{' '} - - {LEARN_MORE_LABEL} - -

- )} -
- - - ) : null} + + => { return await apiService.delete(API_URLS.SYNTHETICS_ENABLEMENT); }; -export const fetchEnableSynthetics = async (): Promise => { +export const fetchEnableSynthetics = async (): Promise => { return await apiService.post(API_URLS.SYNTHETICS_ENABLEMENT); }; diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/state/reducers/monitor_management.ts b/x-pack/plugins/synthetics/public/legacy_uptime/state/reducers/monitor_management.ts index 7a3ae1f22cdc..ef9f3239625f 100644 --- a/x-pack/plugins/synthetics/public/legacy_uptime/state/reducers/monitor_management.ts +++ b/x-pack/plugins/synthetics/public/legacy_uptime/state/reducers/monitor_management.ts @@ -217,6 +217,7 @@ export const monitorManagementListReducer = createReducer(initialState, (builder canEnable: state.enablement?.canEnable || false, areApiKeysEnabled: state.enablement?.areApiKeysEnabled || false, isEnabled: false, + isValidApiKey: state.enablement?.isValidApiKey || false, }, })) .addCase( @@ -240,23 +241,21 @@ export const monitorManagementListReducer = createReducer(initialState, (builder enablement: true, }, })) - .addCase(enableSyntheticsSuccess, (state: WritableDraft) => ({ - ...state, - loading: { - ...state.loading, - enablement: false, - }, - error: { - ...state.error, - enablement: null, - }, - enablement: { - canManageApiKeys: state.enablement?.canManageApiKeys || false, - canEnable: state.enablement?.canEnable || false, - areApiKeysEnabled: state.enablement?.areApiKeysEnabled || false, - isEnabled: true, - }, - })) + .addCase( + enableSyntheticsSuccess, + (state: WritableDraft, action: PayloadAction) => ({ + ...state, + loading: { + ...state.loading, + enablement: false, + }, + error: { + ...state.error, + enablement: null, + }, + enablement: action.payload, + }) + ) .addCase( enableSyntheticsFailure, (state: WritableDraft, action: PayloadAction) => ({ diff --git a/x-pack/plugins/synthetics/server/legacy_uptime/lib/adapters/framework/adapter_types.ts b/x-pack/plugins/synthetics/server/legacy_uptime/lib/adapters/framework/adapter_types.ts index 07c247dc4451..fc088e2097dd 100644 --- a/x-pack/plugins/synthetics/server/legacy_uptime/lib/adapters/framework/adapter_types.ts +++ b/x-pack/plugins/synthetics/server/legacy_uptime/lib/adapters/framework/adapter_types.ts @@ -11,6 +11,7 @@ import type { IScopedClusterClient, Logger, IBasePath, + CoreStart, } from '@kbn/core/server'; import type { TelemetryPluginSetup, TelemetryPluginStart } from '@kbn/telemetry-plugin/server'; import { ObservabilityPluginSetup } from '@kbn/observability-plugin/server'; @@ -65,6 +66,7 @@ export interface UptimeServerSetup { uptimeEsClient: UptimeEsClient; basePath: IBasePath; isDev?: boolean; + coreStart: CoreStart; } export interface UptimeCorePluginsSetup { diff --git a/x-pack/plugins/synthetics/server/legacy_uptime/lib/requests/index.ts b/x-pack/plugins/synthetics/server/legacy_uptime/lib/requests/index.ts index 664e0cb0c618..5dceb0c570bc 100644 --- a/x-pack/plugins/synthetics/server/legacy_uptime/lib/requests/index.ts +++ b/x-pack/plugins/synthetics/server/legacy_uptime/lib/requests/index.ts @@ -28,7 +28,6 @@ import { getJourneyScreenshotBlocks } from './get_journey_screenshot_blocks'; import { getSyntheticsMonitor } from './get_monitor'; import { getSyntheticsEnablement, - deleteServiceApiKey, generateAndSaveServiceAPIKey, getAPIKeyForSyntheticsService, } from '../../../synthetics_service/get_api_key'; @@ -57,7 +56,6 @@ export const uptimeRequests = { getNetworkEvents, getSyntheticsEnablement, getAPIKeyForSyntheticsService, - deleteServiceApiKey, generateAndSaveServiceAPIKey, }; diff --git a/x-pack/plugins/synthetics/server/legacy_uptime/lib/saved_objects/service_api_key.ts b/x-pack/plugins/synthetics/server/legacy_uptime/lib/saved_objects/service_api_key.ts index 9f21aed14e32..adab53c9d426 100644 --- a/x-pack/plugins/synthetics/server/legacy_uptime/lib/saved_objects/service_api_key.ts +++ b/x-pack/plugins/synthetics/server/legacy_uptime/lib/saved_objects/service_api_key.ts @@ -11,8 +11,8 @@ import { SavedObjectsErrorHelpers, SavedObjectsType, } from '@kbn/core/server'; -import { EncryptedSavedObjectsClient } from '@kbn/encrypted-saved-objects-plugin/server'; import { SyntheticsServiceApiKey } from '../../../../common/runtime_types/synthetics_service_api_key'; +import { UptimeServerSetup } from '../adapters'; export const syntheticsApiKeyID = 'ba997842-b0cf-4429-aa9d-578d9bf0d391'; export const syntheticsApiKeyObjectType = 'uptime-synthetics-api-key'; @@ -49,9 +49,17 @@ export const syntheticsServiceApiKey: SavedObjectsType = { }, }; -export const getSyntheticsServiceAPIKey = async (client: EncryptedSavedObjectsClient) => { +const getEncryptedSOClient = (server: UptimeServerSetup) => { + const encryptedClient = server.encryptedSavedObjects.getClient({ + includedHiddenTypes: [syntheticsServiceApiKey.name], + }); + return encryptedClient; +}; + +const getSyntheticsServiceAPIKey = async (server: UptimeServerSetup) => { try { - const obj = await client.getDecryptedAsInternalUser( + const soClient = getEncryptedSOClient(server); + const obj = await soClient.getDecryptedAsInternalUser( syntheticsServiceApiKey.name, syntheticsApiKeyID ); @@ -64,20 +72,26 @@ export const getSyntheticsServiceAPIKey = async (client: EncryptedSavedObjectsCl } }; -export const setSyntheticsServiceApiKey = async ( - client: SavedObjectsClientContract, +const setSyntheticsServiceApiKey = async ( + soClient: SavedObjectsClientContract, apiKey: SyntheticsServiceApiKey ) => { - await client.create(syntheticsServiceApiKey.name, apiKey, { + await soClient.create(syntheticsServiceApiKey.name, apiKey, { id: syntheticsApiKeyID, overwrite: true, }); }; -export const deleteSyntheticsServiceApiKey = async (client: SavedObjectsClientContract) => { +const deleteSyntheticsServiceApiKey = async (soClient: SavedObjectsClientContract) => { try { - return await client.delete(syntheticsServiceApiKey.name, syntheticsApiKeyID); + return await soClient.delete(syntheticsServiceApiKey.name, syntheticsApiKeyID); } catch (e) { throw e; } }; + +export const syntheticsServiceAPIKeySavedObject = { + get: getSyntheticsServiceAPIKey, + set: setSyntheticsServiceApiKey, + delete: deleteSyntheticsServiceApiKey, +}; diff --git a/x-pack/plugins/synthetics/server/plugin.ts b/x-pack/plugins/synthetics/server/plugin.ts index 8871b387eb62..dc6a30033273 100644 --- a/x-pack/plugins/synthetics/server/plugin.ts +++ b/x-pack/plugins/synthetics/server/plugin.ts @@ -121,6 +121,7 @@ export class Plugin implements PluginType { ); if (this.server) { + this.server.coreStart = coreStart; this.server.security = pluginsStart.security; this.server.fleet = pluginsStart.fleet; this.server.encryptedSavedObjects = pluginsStart.encryptedSavedObjects; diff --git a/x-pack/plugins/synthetics/server/routes/monitor_cruds/get_api_key.ts b/x-pack/plugins/synthetics/server/routes/monitor_cruds/get_api_key.ts index 0436b8ec0620..d4650505d372 100644 --- a/x-pack/plugins/synthetics/server/routes/monitor_cruds/get_api_key.ts +++ b/x-pack/plugins/synthetics/server/routes/monitor_cruds/get_api_key.ts @@ -13,11 +13,8 @@ export const getAPIKeySyntheticsRoute: SyntheticsRestApiRouteFactory = (libs) => path: API_URLS.SYNTHETICS_APIKEY, validate: {}, handler: async ({ request, server }): Promise => { - const { security } = server; - const apiKey = await generateAPIKey({ request, - security, server, uptimePrivileges: true, }); diff --git a/x-pack/plugins/synthetics/server/routes/synthetics_service/enablement.ts b/x-pack/plugins/synthetics/server/routes/synthetics_service/enablement.ts index 6d60827216e4..defd82c41572 100644 --- a/x-pack/plugins/synthetics/server/routes/synthetics_service/enablement.ts +++ b/x-pack/plugins/synthetics/server/routes/synthetics_service/enablement.ts @@ -4,22 +4,25 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import { syntheticsServiceAPIKeySavedObject } from '../../legacy_uptime/lib/saved_objects/service_api_key'; import { SyntheticsRestApiRouteFactory, UMRestApiRouteFactory, } from '../../legacy_uptime/routes/types'; import { API_URLS } from '../../../common/constants'; -import { SyntheticsForbiddenError } from '../../synthetics_service/get_api_key'; +import { + generateAndSaveServiceAPIKey, + SyntheticsForbiddenError, +} from '../../synthetics_service/get_api_key'; export const getSyntheticsEnablementRoute: UMRestApiRouteFactory = (libs) => ({ method: 'GET', path: API_URLS.SYNTHETICS_ENABLEMENT, validate: {}, - handler: async ({ request, response, server }): Promise => { + handler: async ({ response, server }): Promise => { try { return response.ok({ body: await libs.requests.getSyntheticsEnablement({ - request, server, }), }); @@ -38,25 +41,21 @@ export const disableSyntheticsRoute: SyntheticsRestApiRouteFactory = (libs) => ( response, request, server, - savedObjectsClient, syntheticsMonitorClient, + savedObjectsClient, }): Promise => { const { security } = server; const { syntheticsService } = syntheticsMonitorClient; try { - const { canEnable } = await libs.requests.getSyntheticsEnablement({ request, server }); + const { canEnable } = await libs.requests.getSyntheticsEnablement({ server }); if (!canEnable) { return response.forbidden(); } await syntheticsService.deleteAllConfigs(); - const apiKey = await libs.requests.getAPIKeyForSyntheticsService({ + const { apiKey } = await libs.requests.getAPIKeyForSyntheticsService({ server, }); - await libs.requests.deleteServiceApiKey({ - request, - server, - savedObjectsClient, - }); + await syntheticsServiceAPIKeySavedObject.delete(savedObjectsClient); await security.authc.apiKeys?.invalidate(request, { ids: [apiKey?.id || ''] }); return response.ok({}); } catch (e) { @@ -71,15 +70,18 @@ export const enableSyntheticsRoute: UMRestApiRouteFactory = (libs) => ({ path: API_URLS.SYNTHETICS_ENABLEMENT, validate: {}, handler: async ({ request, response, server }): Promise => { - const { authSavedObjectsClient, logger, security } = server; + const { authSavedObjectsClient, logger } = server; try { - await libs.requests.generateAndSaveServiceAPIKey({ + await generateAndSaveServiceAPIKey({ request, authSavedObjectsClient, - security, server, }); - return response.ok({}); + return response.ok({ + body: await libs.requests.getSyntheticsEnablement({ + server, + }), + }); } catch (e) { logger.error(e); if (e instanceof SyntheticsForbiddenError) { diff --git a/x-pack/plugins/synthetics/server/synthetics_service/authentication/check_has_privilege.ts b/x-pack/plugins/synthetics/server/synthetics_service/authentication/check_has_privilege.ts new file mode 100644 index 000000000000..56b7ce8b79c6 --- /dev/null +++ b/x-pack/plugins/synthetics/server/synthetics_service/authentication/check_has_privilege.ts @@ -0,0 +1,24 @@ +/* + * 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 { UptimeServerSetup } from '../../legacy_uptime/lib/adapters'; +import { getFakeKibanaRequest } from '../utils/fake_kibana_request'; +import { serviceApiKeyPrivileges } from '../get_api_key'; + +export const checkHasPrivileges = async ( + server: UptimeServerSetup, + apiKey: { id: string; apiKey: string } +) => { + return await server.coreStart.elasticsearch.client + .asScoped(getFakeKibanaRequest({ id: apiKey.id, api_key: apiKey.apiKey })) + .asCurrentUser.security.hasPrivileges({ + body: { + index: serviceApiKeyPrivileges.indices, + cluster: serviceApiKeyPrivileges.cluster, + }, + }); +}; diff --git a/x-pack/plugins/synthetics/server/synthetics_service/get_api_key.test.ts b/x-pack/plugins/synthetics/server/synthetics_service/get_api_key.test.ts index 5bd8d0595103..4b15f4da4351 100644 --- a/x-pack/plugins/synthetics/server/synthetics_service/get_api_key.test.ts +++ b/x-pack/plugins/synthetics/server/synthetics_service/get_api_key.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { getAPIKeyForSyntheticsService } from './get_api_key'; +import { getAPIKeyForSyntheticsService, syntheticsIndex } from './get_api_key'; import { encryptedSavedObjectsMock } from '@kbn/encrypted-saved-objects-plugin/server/mocks'; import { securityMock } from '@kbn/security-plugin/server/mocks'; import { coreMock } from '@kbn/core/server/mocks'; @@ -13,6 +13,9 @@ import { syntheticsServiceApiKey } from '../legacy_uptime/lib/saved_objects/serv import { KibanaRequest } from '@kbn/core/server'; import { UptimeServerSetup } from '../legacy_uptime/lib/adapters'; import { getUptimeESMockClient } from '../legacy_uptime/lib/requests/test_helpers'; +import { loggerMock } from '@kbn/logging-mocks'; + +import * as authUtils from './authentication/check_has_privilege'; describe('getAPIKeyTest', function () { const core = coreMock.createStart(); @@ -20,7 +23,20 @@ describe('getAPIKeyTest', function () { const encryptedSavedObjects = encryptedSavedObjectsMock.createStart(); const request = {} as KibanaRequest; + const logger = loggerMock.create(); + + jest.spyOn(authUtils, 'checkHasPrivileges').mockResolvedValue({ + index: { + [syntheticsIndex]: { + auto_configure: true, + create_doc: true, + view_index_metadata: true, + }, + }, + } as any); + const server = { + logger, security, encryptedSavedObjects, savedObjectsClient: core.savedObjects.getScopedClient(request), @@ -28,6 +44,7 @@ describe('getAPIKeyTest', function () { } as unknown as UptimeServerSetup; security.authc.apiKeys.areAPIKeysEnabled = jest.fn().mockReturnValue(true); + security.authc.apiKeys.validate = jest.fn().mockReturnValue(true); security.authc.apiKeys.create = jest.fn().mockReturnValue({ id: 'test', name: 'service-api-key', @@ -47,7 +64,10 @@ describe('getAPIKeyTest', function () { server, }); - expect(apiKey).toEqual({ apiKey: 'qwerty', id: 'test', name: 'service-api-key' }); + expect(apiKey).toEqual({ + apiKey: { apiKey: 'qwerty', id: 'test', name: 'service-api-key' }, + isValid: true, + }); expect(encryptedSavedObjects.getClient).toHaveBeenCalledTimes(1); expect(getObject).toHaveBeenCalledTimes(1); diff --git a/x-pack/plugins/synthetics/server/synthetics_service/get_api_key.ts b/x-pack/plugins/synthetics/server/synthetics_service/get_api_key.ts index 5984bcb3b8ac..435ae4ba2a12 100644 --- a/x-pack/plugins/synthetics/server/synthetics_service/get_api_key.ts +++ b/x-pack/plugins/synthetics/server/synthetics_service/get_api_key.ts @@ -10,22 +10,19 @@ import type { } from '@elastic/elasticsearch/lib/api/types'; import { KibanaRequest, SavedObjectsClientContract } from '@kbn/core/server'; -import { SecurityPluginStart } from '@kbn/security-plugin/server'; import { ALL_SPACES_ID } from '@kbn/security-plugin/common/constants'; -import { - deleteSyntheticsServiceApiKey, - getSyntheticsServiceAPIKey, - setSyntheticsServiceApiKey, - syntheticsServiceApiKey, -} from '../legacy_uptime/lib/saved_objects/service_api_key'; +import { syntheticsServiceAPIKeySavedObject } from '../legacy_uptime/lib/saved_objects/service_api_key'; import { SyntheticsServiceApiKey } from '../../common/runtime_types/synthetics_service_api_key'; import { UptimeServerSetup } from '../legacy_uptime/lib/adapters'; +import { checkHasPrivileges } from './authentication/check_has_privilege'; + +export const syntheticsIndex = 'synthetics-*'; export const serviceApiKeyPrivileges = { cluster: ['monitor', 'read_ilm', 'read_pipeline'] as SecurityClusterPrivilege[], indices: [ { - names: ['synthetics-*'], + names: [syntheticsIndex], privileges: [ 'view_index_metadata', 'create_doc', @@ -41,47 +38,61 @@ export const getAPIKeyForSyntheticsService = async ({ server, }: { server: UptimeServerSetup; -}): Promise => { - const { encryptedSavedObjects } = server; - - const encryptedClient = encryptedSavedObjects.getClient({ - includedHiddenTypes: [syntheticsServiceApiKey.name], - }); - +}): Promise<{ apiKey?: SyntheticsServiceApiKey; isValid: boolean }> => { try { - const apiKey = await getSyntheticsServiceAPIKey(encryptedClient); + const apiKey = await syntheticsServiceAPIKeySavedObject.get(server); + if (apiKey) { - return apiKey; + const isValid = await server.security.authc.apiKeys.validate({ + id: apiKey.id, + api_key: apiKey.apiKey, + }); + + if (isValid) { + const { index } = await checkHasPrivileges(server, apiKey); + + const indexPermissions = index[syntheticsIndex]; + + const hasPermissions = + indexPermissions.auto_configure && + indexPermissions.create_doc && + indexPermissions.view_index_metadata; + + if (!hasPermissions) { + return { isValid: false, apiKey }; + } + } else { + server.logger.info('Synthetics api is no longer valid'); + } + + return { apiKey, isValid }; } } catch (err) { - // TODO: figure out how to handle decryption errors + server.logger.error(err); } + + return { isValid: false }; }; export const generateAPIKey = async ({ server, - security, request, uptimePrivileges = false, }: { server: UptimeServerSetup; - request?: KibanaRequest; - security: SecurityPluginStart; + request: KibanaRequest; uptimePrivileges?: boolean; }) => { + const { security } = server; const isApiKeysEnabled = await security.authc.apiKeys?.areAPIKeysEnabled(); if (!isApiKeysEnabled) { throw new Error('Please enable API keys in kibana to use synthetics service.'); } - if (!request) { - throw new Error('User authorization is needed for api key generation'); - } - if (uptimePrivileges) { return security.authc.apiKeys?.create(request, { - name: 'uptime-api-key', + name: 'synthetics-api-key (required for project monitors)', kibana_role_descriptors: { uptime_save: { elasticsearch: {}, @@ -105,13 +116,13 @@ export const generateAPIKey = async ({ }); } - const { canEnable } = await getSyntheticsEnablement({ request, server }); + const { canEnable } = await hasEnablePermissions(server); if (!canEnable) { throw new SyntheticsForbiddenError(); } return security.authc.apiKeys?.create(request, { - name: 'synthetics-api-key', + name: 'synthetics-api-key (required for monitor management)', role_descriptors: { synthetics_writer: serviceApiKeyPrivileges, }, @@ -124,68 +135,59 @@ export const generateAPIKey = async ({ export const generateAndSaveServiceAPIKey = async ({ server, - security, request, authSavedObjectsClient, }: { server: UptimeServerSetup; - request?: KibanaRequest; - security: SecurityPluginStart; + request: KibanaRequest; // authSavedObject is needed for write operations authSavedObjectsClient?: SavedObjectsClientContract; }) => { - const apiKeyResult = await generateAPIKey({ server, request, security }); + const apiKeyResult = await generateAPIKey({ server, request }); if (apiKeyResult) { const { id, name, api_key: apiKey } = apiKeyResult; const apiKeyObject = { id, name, apiKey }; if (authSavedObjectsClient) { // discard decoded key and rest of the keys - await setSyntheticsServiceApiKey(authSavedObjectsClient, apiKeyObject); + await syntheticsServiceAPIKeySavedObject.set(authSavedObjectsClient, apiKeyObject); } return apiKeyObject; } }; -export const deleteServiceApiKey = async ({ - request, - server, - savedObjectsClient, -}: { - server: UptimeServerSetup; - request?: KibanaRequest; - savedObjectsClient: SavedObjectsClientContract; -}) => { - await deleteSyntheticsServiceApiKey(savedObjectsClient); -}; - -export const getSyntheticsEnablement = async ({ - request, - server: { uptimeEsClient, security, encryptedSavedObjects }, -}: { - server: UptimeServerSetup; - request?: KibanaRequest; -}) => { - const encryptedClient = encryptedSavedObjects.getClient({ - includedHiddenTypes: [syntheticsServiceApiKey.name], - }); +export const getSyntheticsEnablement = async ({ server }: { server: UptimeServerSetup }) => { + const { security } = server; const [apiKey, hasPrivileges, areApiKeysEnabled] = await Promise.all([ - getSyntheticsServiceAPIKey(encryptedClient), - uptimeEsClient.baseESClient.security.hasPrivileges({ - body: { - cluster: [ - 'manage_security', - 'manage_api_key', - 'manage_own_api_key', - ...serviceApiKeyPrivileges.cluster, - ], - index: serviceApiKeyPrivileges.indices, - }, - }), + getAPIKeyForSyntheticsService({ server }), + hasEnablePermissions(server), security.authc.apiKeys.areAPIKeysEnabled(), ]); + const { canEnable, canManageApiKeys } = hasPrivileges; + return { + canEnable, + canManageApiKeys, + isEnabled: Boolean(apiKey?.apiKey), + isValidApiKey: apiKey?.isValid, + areApiKeysEnabled, + }; +}; + +const hasEnablePermissions = async ({ uptimeEsClient }: UptimeServerSetup) => { + const hasPrivileges = await uptimeEsClient.baseESClient.security.hasPrivileges({ + body: { + cluster: [ + 'manage_security', + 'manage_api_key', + 'manage_own_api_key', + ...serviceApiKeyPrivileges.cluster, + ], + index: serviceApiKeyPrivileges.indices, + }, + }); + const { cluster } = hasPrivileges; const { manage_security: manageSecurity, @@ -203,10 +205,8 @@ export const getSyntheticsEnablement = async ({ ); return { - canEnable: canManageApiKeys && hasClusterPermissions && hasIndexPermissions, canManageApiKeys, - isEnabled: Boolean(apiKey), - areApiKeysEnabled, + canEnable: canManageApiKeys && hasClusterPermissions && hasIndexPermissions, }; }; diff --git a/x-pack/plugins/synthetics/server/synthetics_service/service_api_client.ts b/x-pack/plugins/synthetics/server/synthetics_service/service_api_client.ts index 7597797be848..5dceb3802c5f 100644 --- a/x-pack/plugins/synthetics/server/synthetics_service/service_api_client.ts +++ b/x-pack/plugins/synthetics/server/synthetics_service/service_api_client.ts @@ -189,6 +189,9 @@ export class ServiceAPIClient { }), catchError((err: AxiosError<{ reason: string; status: number }>) => { pushErrors.push({ locationId: id, error: err.response?.data! }); + const reason = err.response?.data?.reason ?? ''; + + err.message = `Failed to call service location ${url} with method ${method} with ${allMonitors.length} monitors: ${err.message}, ${reason}`; this.logger.error(err); sendErrorTelemetryEvents(this.logger, this.server.telemetry, { reason: err.response?.data?.reason, @@ -199,9 +202,6 @@ export class ServiceAPIClient { url, stackVersion: this.server.stackVersion, }); - if (err.response?.data?.reason) { - this.logger.error(err.response?.data?.reason); - } // we don't want to throw an unhandled exception here return of(true); }) diff --git a/x-pack/plugins/synthetics/server/synthetics_service/synthetics_service.ts b/x-pack/plugins/synthetics/server/synthetics_service/synthetics_service.ts index 3136fd6cab07..57a174f18316 100644 --- a/x-pack/plugins/synthetics/server/synthetics_service/synthetics_service.ts +++ b/x-pack/plugins/synthetics/server/synthetics_service/synthetics_service.ts @@ -7,35 +7,33 @@ /* eslint-disable max-classes-per-file */ -import { SavedObject } from '@kbn/core/server'; -import { Logger } from '@kbn/core/server'; +import { Logger, SavedObject } from '@kbn/core/server'; import { ConcreteTaskInstance, + TaskInstance, TaskManagerSetupContract, TaskManagerStartContract, - TaskInstance, } from '@kbn/task-manager-plugin/server'; import { Subject } from 'rxjs'; import { sendErrorTelemetryEvents } from '../routes/telemetry/monitor_upgrade_sender'; import { UptimeServerSetup } from '../legacy_uptime/lib/adapters'; import { installSyntheticsIndexTemplates } from '../routes/synthetics_service/install_index_templates'; -import { SyntheticsServiceApiKey } from '../../common/runtime_types/synthetics_service_api_key'; import { getAPIKeyForSyntheticsService } from './get_api_key'; import { syntheticsMonitorType } from '../legacy_uptime/lib/saved_objects/synthetics_monitor'; import { getEsHosts } from './get_es_hosts'; import { ServiceConfig } from '../../common/config'; import { ServiceAPIClient } from './service_api_client'; -import { formatMonitorConfig, formatHeartbeatRequest } from './formatters/format_configs'; +import { formatHeartbeatRequest, formatMonitorConfig } from './formatters/format_configs'; import { ConfigKey, + HeartbeatConfig, MonitorFields, + ServiceLocationErrors, ServiceLocations, SyntheticsMonitor, - ThrottlingOptions, SyntheticsMonitorWithId, - ServiceLocationErrors, SyntheticsMonitorWithSecrets, - HeartbeatConfig, + ThrottlingOptions, } from '../../common/runtime_types'; import { getServiceLocations } from './get_service_locations'; @@ -54,8 +52,6 @@ export class SyntheticsService { private readonly config: ServiceConfig; private readonly esHosts: string[]; - private apiKey: SyntheticsServiceApiKey | undefined; - public locations: ServiceLocations; public throttling: ThrottlingOptions | undefined; @@ -67,6 +63,8 @@ export class SyntheticsService { public syncErrors?: ServiceLocationErrors | null = []; + public invalidApiKeyError?: boolean; + constructor(server: UptimeServerSetup) { this.logger = server.logger; this.server = server; @@ -178,7 +176,7 @@ export class SyntheticsService { status: e.status, stackVersion: service.server.stackVersion, }); - throw e; + service.logger.error(e); } return { state }; @@ -235,17 +233,19 @@ export class SyntheticsService { } async getApiKey() { - try { - this.apiKey = await getAPIKeyForSyntheticsService({ server: this.server }); - } catch (err) { - this.logger.error(err); - throw err; + const { apiKey, isValid } = await getAPIKeyForSyntheticsService({ server: this.server }); + if (!isValid) { + throw new Error( + 'API key is not valid. Cannot push monitor configuration to synthetics public testing locations' + ); } - return this.apiKey; + return apiKey; } - async getOutput(apiKey: SyntheticsServiceApiKey) { + async getOutput() { + const apiKey = await this.getApiKey(); + return { hosts: this.esHosts, api_key: `${apiKey?.id}:${apiKey?.apiKey}`, @@ -255,21 +255,15 @@ export class SyntheticsService { async addConfig(config: HeartbeatConfig | HeartbeatConfig[]) { const monitors = this.formatConfigs(Array.isArray(config) ? config : [config]); - this.apiKey = await this.getApiKey(); - - if (!this.apiKey) { - return null; - } - - const data = { - monitors, - output: await this.getOutput(this.apiKey), - }; + const output = await this.getOutput(); this.logger.debug(`1 monitor will be pushed to synthetics service.`); try { - this.syncErrors = await this.apiClient.post(data); + this.syncErrors = await this.apiClient.post({ + monitors, + output, + }); return this.syncErrors; } catch (e) { this.logger.error(e); @@ -282,15 +276,10 @@ export class SyntheticsService { Array.isArray(monitorConfig) ? monitorConfig : [monitorConfig] ); - this.apiKey = await this.getApiKey(); - - if (!this.apiKey) { - return null; - } - + const output = await this.getOutput(); const data = { monitors, - output: await this.getOutput(this.apiKey), + output, isEdit: true, }; @@ -308,31 +297,32 @@ export class SyntheticsService { const subject = new Subject(); subject.subscribe(async (monitorConfigs) => { - const monitors = this.formatConfigs(monitorConfigs); - - if (monitors.length === 0) { - this.logger.debug('No monitor found which can be pushed to service.'); - return null; - } - - this.apiKey = await this.getApiKey(); - - if (!this.apiKey) { - return null; - } - - const data = { - monitors, - output: await this.getOutput(this.apiKey), - }; - - this.logger.debug(`${monitors.length} monitors will be pushed to synthetics service.`); - try { - service.syncErrors = await this.apiClient.put(data); + const monitors = this.formatConfigs(monitorConfigs); + + if (monitors.length === 0) { + this.logger.debug('No monitor found which can be pushed to service.'); + return null; + } + + const output = await this.getOutput(); + + this.logger.debug(`${monitors.length} monitors will be pushed to synthetics service.`); + + service.syncErrors = await this.apiClient.put({ + monitors, + output, + }); } catch (e) { + sendErrorTelemetryEvents(service.logger, service.server.telemetry, { + reason: 'Failed to push configs to service', + message: e?.message, + type: 'pushConfigsError', + code: e?.code, + status: e.status, + stackVersion: service.server.stackVersion, + }); this.logger.error(e); - throw e; } }); @@ -345,19 +335,13 @@ export class SyntheticsService { return; } - this.apiKey = await this.getApiKey(); - - if (!this.apiKey) { - return null; - } - - const data = { - monitors, - output: await this.getOutput(this.apiKey), - }; + const output = await this.getOutput(); try { - return await this.apiClient.runOnce(data); + return await this.apiClient.runOnce({ + monitors, + output, + }); } catch (e) { this.logger.error(e); throw e; @@ -365,21 +349,13 @@ export class SyntheticsService { } async deleteConfigs(configs: SyntheticsMonitorWithId[]) { - this.apiKey = await this.getApiKey(); - - if (!this.apiKey) { - return null; - } + const output = await this.getOutput(); const data = { + output, monitors: this.formatConfigs(configs), - output: await this.getOutput(this.apiKey), }; - const result = await this.apiClient.delete(data); - if (this.syncErrors && this.syncErrors?.length > 0) { - await this.pushConfigs(); - } - return result; + return await this.apiClient.delete(data); } async deleteAllConfigs() { diff --git a/x-pack/plugins/synthetics/server/synthetics_service/utils/fake_kibana_request.ts b/x-pack/plugins/synthetics/server/synthetics_service/utils/fake_kibana_request.ts new file mode 100644 index 000000000000..3ea92edcbc62 --- /dev/null +++ b/x-pack/plugins/synthetics/server/synthetics_service/utils/fake_kibana_request.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Request } from '@hapi/hapi'; +import { CoreKibanaRequest } from '@kbn/core/server'; + +export function getFakeKibanaRequest(apiKey: { id: string; api_key: string }) { + const requestHeaders: Record = {}; + + requestHeaders.authorization = `ApiKey ${Buffer.from(`${apiKey.id}:${apiKey.api_key}`).toString( + 'base64' + )}`; + + return CoreKibanaRequest.from({ + headers: requestHeaders, + path: '/', + route: { settings: {} }, + url: { + href: '/', + }, + raw: { + req: { + url: '/', + }, + }, + } as unknown as Request); +} diff --git a/x-pack/test/api_integration/apis/synthetics/synthetics_enablement.ts b/x-pack/test/api_integration/apis/synthetics/synthetics_enablement.ts index b3566f64574b..dac32af21106 100644 --- a/x-pack/test/api_integration/apis/synthetics/synthetics_enablement.ts +++ b/x-pack/test/api_integration/apis/synthetics/synthetics_enablement.ts @@ -23,7 +23,7 @@ export default function ({ getService }: FtrProviderContext) { describe('[GET] - /internal/uptime/service/enablement', () => { ['manage_security', 'manage_api_key', 'manage_own_api_key'].forEach((privilege) => { - it(`returns response for an admin with priviledge ${privilege}`, async () => { + it(`returns response for an admin with privilege ${privilege}`, async () => { const username = 'admin'; const roleName = `synthetics_admin-${privilege}`; const password = `${username}-password`; @@ -60,6 +60,7 @@ export default function ({ getService }: FtrProviderContext) { canManageApiKeys: true, canEnable: true, isEnabled: false, + isValidApiKey: false, }); } finally { await security.user.delete(username); @@ -102,6 +103,7 @@ export default function ({ getService }: FtrProviderContext) { canManageApiKeys: false, canEnable: false, isEnabled: false, + isValidApiKey: false, }); } finally { await security.role.delete(roleName); @@ -153,6 +155,7 @@ export default function ({ getService }: FtrProviderContext) { canManageApiKeys: true, canEnable: true, isEnabled: true, + isValidApiKey: true, }); } finally { await supertest @@ -203,6 +206,7 @@ export default function ({ getService }: FtrProviderContext) { canManageApiKeys: false, canEnable: false, isEnabled: false, + isValidApiKey: false, }); } finally { await security.user.delete(username); @@ -259,6 +263,7 @@ export default function ({ getService }: FtrProviderContext) { canManageApiKeys: true, canEnable: true, isEnabled: false, + isValidApiKey: false, }); } finally { await security.user.delete(username); @@ -308,6 +313,7 @@ export default function ({ getService }: FtrProviderContext) { canManageApiKeys: false, canEnable: false, isEnabled: true, + isValidApiKey: true, }); } finally { await supertestWithAuth @@ -370,6 +376,7 @@ export default function ({ getService }: FtrProviderContext) { canManageApiKeys: true, canEnable: true, isEnabled: false, + isValidApiKey: false, }); // can disable synthetics in non default space when enabled in default space @@ -394,6 +401,7 @@ export default function ({ getService }: FtrProviderContext) { canManageApiKeys: true, canEnable: true, isEnabled: false, + isValidApiKey: false, }); } finally { await security.user.delete(username);