[Synthetics] Validate API keys (#143867)

Co-authored-by: Dominique Clarke <dominique.clarke@elastic.co>
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Fixes https://github.com/elastic/kibana/issues/142875
This commit is contained in:
Shahzad 2022-11-15 16:29:02 +01:00 committed by GitHub
parent b33acd0fe6
commit 75ce1e397a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 574 additions and 271 deletions

View file

@ -14,6 +14,7 @@ export const apiKeysMock = {
areAPIKeysEnabled: jest.fn(), areAPIKeysEnabled: jest.fn(),
create: jest.fn(), create: jest.fn(),
grantAsInternalUser: jest.fn(), grantAsInternalUser: jest.fn(),
validate: jest.fn(),
invalidate: jest.fn(), invalidate: jest.fn(),
invalidateAsInternalUser: jest.fn(), invalidateAsInternalUser: jest.fn(),
}), }),

View file

@ -18,6 +18,7 @@ import { ALL_SPACES_ID } from '../../../common/constants';
import type { SecurityLicense } from '../../../common/licensing'; import type { SecurityLicense } from '../../../common/licensing';
import { licenseMock } from '../../../common/licensing/index.mock'; import { licenseMock } from '../../../common/licensing/index.mock';
import { APIKeys } from './api_keys'; import { APIKeys } from './api_keys';
import { getFakeKibanaRequest } from './fake_kibana_request';
const encodeToBase64 = (str: string) => Buffer.from(str).toString('base64'); 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()', () => { describe('invalidateAsInternalUser()', () => {
it('returns null when security feature is disabled', async () => { it('returns null when security feature is disabled', async () => {
mockLicense.isEnabled.mockReturnValue(false); mockLicense.isEnabled.mockReturnValue(false);

View file

@ -18,6 +18,7 @@ import {
BasicHTTPAuthorizationHeaderCredentials, BasicHTTPAuthorizationHeaderCredentials,
HTTPAuthorizationHeader, HTTPAuthorizationHeader,
} from '../http_authentication'; } from '../http_authentication';
import { getFakeKibanaRequest } from './fake_kibana_request';
/** /**
* Represents the options to create an APIKey class instance that will be * 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. * Class responsible for managing Elasticsearch API keys.
*/ */
@ -335,6 +351,30 @@ export class APIKeys {
return result; return result;
} }
/**
* Tries to validate an API key.
* @param apiKeyPrams ValidateAPIKeyParams.
*/
async validate(apiKeyPrams: ValidateAPIKeyParams): Promise<boolean> {
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<string, any>) { private doesErrorIndicateAPIKeysAreDisabled(e: Record<string, any>) {
const disabledFeature = e.body?.error?.['disabled.feature']; const disabledFeature = e.body?.error?.['disabled.feature'];
return disabledFeature === 'api_keys'; return disabledFeature === 'api_keys';

View file

@ -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<string, string> = {};
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);
}

View file

@ -63,6 +63,7 @@ export interface InternalAuthenticationServiceStart extends AuthenticationServic
| 'areAPIKeysEnabled' | 'areAPIKeysEnabled'
| 'create' | 'create'
| 'invalidate' | 'invalidate'
| 'validate'
| 'grantAsInternalUser' | 'grantAsInternalUser'
| 'invalidateAsInternalUser' | 'invalidateAsInternalUser'
>; >;
@ -81,6 +82,7 @@ export interface AuthenticationServiceStart {
| 'areAPIKeysEnabled' | 'areAPIKeysEnabled'
| 'create' | 'create'
| 'invalidate' | 'invalidate'
| 'validate'
| 'grantAsInternalUser' | 'grantAsInternalUser'
| 'invalidateAsInternalUser' | 'invalidateAsInternalUser'
>; >;
@ -354,6 +356,7 @@ export class AuthenticationService {
create: apiKeys.create.bind(apiKeys), create: apiKeys.create.bind(apiKeys),
grantAsInternalUser: apiKeys.grantAsInternalUser.bind(apiKeys), grantAsInternalUser: apiKeys.grantAsInternalUser.bind(apiKeys),
invalidate: apiKeys.invalidate.bind(apiKeys), invalidate: apiKeys.invalidate.bind(apiKeys),
validate: apiKeys.validate.bind(apiKeys),
invalidateAsInternalUser: apiKeys.invalidateAsInternalUser.bind(apiKeys), invalidateAsInternalUser: apiKeys.invalidateAsInternalUser.bind(apiKeys),
}, },

View file

@ -141,6 +141,7 @@ describe('Security Plugin', () => {
"grantAsInternalUser": [Function], "grantAsInternalUser": [Function],
"invalidate": [Function], "invalidate": [Function],
"invalidateAsInternalUser": [Function], "invalidateAsInternalUser": [Function],
"validate": [Function],
}, },
"getCurrentUser": [Function], "getCurrentUser": [Function],
}, },

View file

@ -40,6 +40,7 @@ export const MonitorManagementEnablementResultCodec = t.type({
canEnable: t.boolean, canEnable: t.boolean,
canManageApiKeys: t.boolean, canManageApiKeys: t.boolean,
areApiKeysEnabled: t.boolean, areApiKeysEnabled: t.boolean,
isValidApiKey: t.boolean,
}); });
export type MonitorManagementEnablementResult = t.TypeOf< export type MonitorManagementEnablementResult = t.TypeOf<

View file

@ -31,6 +31,7 @@ export function useEnablement() {
canEnable: enablement?.canEnable, canEnable: enablement?.canEnable,
isEnabled: enablement?.isEnabled, isEnabled: enablement?.isEnabled,
}, },
invalidApiKeyError: enablement ? !Boolean(enablement?.isValidApiKey) : false,
error, error,
loading, loading,
enableSynthetics: useCallback(() => dispatch(enableSynthetics()), [dispatch]), enableSynthetics: useCallback(() => dispatch(enableSynthetics()), [dispatch]),

View file

@ -58,6 +58,7 @@ export const syntheticsEnablementReducer = createReducer(initialState, (builder)
areApiKeysEnabled: state.enablement?.areApiKeysEnabled ?? false, areApiKeysEnabled: state.enablement?.areApiKeysEnabled ?? false,
canManageApiKeys: state.enablement?.canManageApiKeys ?? false, canManageApiKeys: state.enablement?.canManageApiKeys ?? false,
isEnabled: false, isEnabled: false,
isValidApiKey: true,
}; };
}) })
.addCase(disableSyntheticsFailure, (state, action) => { .addCase(disableSyntheticsFailure, (state, action) => {
@ -75,6 +76,7 @@ export const syntheticsEnablementReducer = createReducer(initialState, (builder)
canEnable: state.enablement?.canEnable ?? false, canEnable: state.enablement?.canEnable ?? false,
areApiKeysEnabled: state.enablement?.areApiKeysEnabled ?? false, areApiKeysEnabled: state.enablement?.areApiKeysEnabled ?? false,
canManageApiKeys: state.enablement?.canManageApiKeys ?? false, canManageApiKeys: state.enablement?.canManageApiKeys ?? false,
isValidApiKey: state.enablement?.isValidApiKey ?? false,
isEnabled: true, isEnabled: true,
}; };
}) })

View file

@ -37,6 +37,7 @@ export function useEnablement() {
canEnable: enablement?.canEnable, canEnable: enablement?.canEnable,
isEnabled: enablement?.isEnabled, isEnabled: enablement?.isEnabled,
}, },
invalidApiKeyError: enablement ? !Boolean(enablement?.isValidApiKey) : false,
error, error,
loading, loading,
totalMonitors: total, totalMonitors: total,

View file

@ -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 (
<>
<EuiCallOut title={CALLOUT_MANAGEMENT_DISABLED} color="warning" iconType="help">
<p>{CALLOUT_MANAGEMENT_DESCRIPTION}</p>
{enablement.canEnable ? (
<EuiButton
fill
color="primary"
onClick={() => {
enableSynthetics();
}}
>
{SYNTHETICS_ENABLE_LABEL}
</EuiButton>
) : (
<p>
{CALLOUT_MANAGEMENT_CONTACT_ADMIN}{' '}
<EuiLink href="#" target="_blank">
{LEARN_MORE_LABEL}
</EuiLink>
</p>
)}
</EuiCallOut>
<EuiSpacer size="s" />
</>
);
};
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',
}
);

View file

@ -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 (
<>
<EuiCallOut title={API_KEY_MISSING} color="warning" iconType="help">
<p>{CALLOUT_MANAGEMENT_DESCRIPTION}</p>
{enablement.canEnable ? (
<EuiButton
fill
color="primary"
onClick={() => {
enableSynthetics();
}}
>
{SYNTHETICS_ENABLE_LABEL}
</EuiButton>
) : (
<p>
{CALLOUT_MANAGEMENT_CONTACT_ADMIN}{' '}
<EuiLink href="#" target="_blank">
{LEARN_MORE_LABEL}
</EuiLink>
</p>
)}
</EuiCallOut>
<EuiSpacer size="s" />
</>
);
};
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',
}
);

View file

@ -8,8 +8,9 @@
import React, { useEffect, useRef, useState } from 'react'; import React, { useEffect, useRef, useState } from 'react';
import { i18n } from '@kbn/i18n'; import { i18n } from '@kbn/i18n';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { EuiCallOut, EuiButton, EuiSpacer, EuiLink } from '@elastic/eui';
import { useTrackPageview } from '@kbn/observability-plugin/public'; 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 { ManageLocationsPortal } from '../../components/monitor_management/manage_locations/manage_locations';
import { monitorManagementListSelector } from '../../state/selectors'; import { monitorManagementListSelector } from '../../state/selectors';
import { useMonitorManagementBreadcrumbs } from './use_monitor_management_breadcrumbs'; import { useMonitorManagementBreadcrumbs } from './use_monitor_management_breadcrumbs';
@ -27,12 +28,7 @@ export const MonitorManagementPage: React.FC = () => {
useMonitorManagementBreadcrumbs(); useMonitorManagementBreadcrumbs();
const [shouldFocusEnablementButton, setShouldFocusEnablementButton] = useState(false); const [shouldFocusEnablementButton, setShouldFocusEnablementButton] = useState(false);
const { const { error: enablementError, enablement, loading: enablementLoading } = useEnablement();
error: enablementError,
enablement,
loading: enablementLoading,
enableSynthetics,
} = useEnablement();
const { loading: locationsLoading } = useLocations(); const { loading: locationsLoading } = useLocations();
const { list: monitorList } = useSelector(monitorManagementListSelector); const { list: monitorList } = useSelector(monitorManagementListSelector);
const { isEnabled } = enablement; const { isEnabled } = enablement;
@ -61,32 +57,8 @@ export const MonitorManagementPage: React.FC = () => {
errorTitle={ERROR_HEADING_LABEL} errorTitle={ERROR_HEADING_LABEL}
errorBody={ERROR_HEADING_BODY} errorBody={ERROR_HEADING_BODY}
> >
{!isEnabled && monitorList.total && monitorList.total > 0 ? ( <InvalidApiKeyCalloutCallout />
<> <DisabledCallout />
<EuiCallOut title={CALLOUT_MANAGEMENT_DISABLED} color="warning" iconType="help">
<p>{CALLOUT_MANAGEMENT_DESCRIPTION}</p>
{enablement.canEnable ? (
<EuiButton
fill
color="primary"
onClick={() => {
enableSynthetics();
}}
>
{SYNTHETICS_ENABLE_LABEL}
</EuiButton>
) : (
<p>
{CALLOUT_MANAGEMENT_CONTACT_ADMIN}{' '}
<EuiLink href="#" target="_blank">
{LEARN_MORE_LABEL}
</EuiLink>
</p>
)}
</EuiCallOut>
<EuiSpacer size="s" />
</>
) : null}
<MonitorListContainer <MonitorListContainer
isEnabled={isEnabled} isEnabled={isEnabled}
pageState={pageState} pageState={pageState}
@ -106,45 +78,9 @@ const LOADING_LABEL = i18n.translate(
} }
); );
const LEARN_MORE_LABEL = i18n.translate(
'xpack.synthetics.monitorManagement.manageMonitorLoadingLabel.callout.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.callout.disabled.adminContact',
{
defaultMessage: 'Please contact your administrator to enable Monitor Management.',
}
);
const CALLOUT_MANAGEMENT_DESCRIPTION = i18n.translate(
'xpack.synthetics.monitorManagement.callout.description.disabled',
{
defaultMessage:
'Monitor Management is currently disabled. To run your monitors on Elastic managed Synthetics service, enable Monitor Management. Your existing monitors are paused.',
}
);
const ERROR_HEADING_BODY = i18n.translate( const ERROR_HEADING_BODY = i18n.translate(
'xpack.synthetics.monitorManagement.editMonitorError.description', 'xpack.synthetics.monitorManagement.editMonitorError.description',
{ {
defaultMessage: 'Monitor Management settings could not be loaded. Please contact Support.', defaultMessage: 'Monitor Management settings could not be loaded. Please contact Support.',
} }
); );
const SYNTHETICS_ENABLE_LABEL = i18n.translate(
'xpack.synthetics.monitorManagement.syntheticsEnableLabel.management',
{
defaultMessage: 'Enable Monitor Management',
}
);

View file

@ -114,7 +114,7 @@ export const fetchDisableSynthetics = async (): Promise<void> => {
return await apiService.delete(API_URLS.SYNTHETICS_ENABLEMENT); return await apiService.delete(API_URLS.SYNTHETICS_ENABLEMENT);
}; };
export const fetchEnableSynthetics = async (): Promise<void> => { export const fetchEnableSynthetics = async (): Promise<MonitorManagementEnablementResult> => {
return await apiService.post(API_URLS.SYNTHETICS_ENABLEMENT); return await apiService.post(API_URLS.SYNTHETICS_ENABLEMENT);
}; };

View file

@ -217,6 +217,7 @@ export const monitorManagementListReducer = createReducer(initialState, (builder
canEnable: state.enablement?.canEnable || false, canEnable: state.enablement?.canEnable || false,
areApiKeysEnabled: state.enablement?.areApiKeysEnabled || false, areApiKeysEnabled: state.enablement?.areApiKeysEnabled || false,
isEnabled: false, isEnabled: false,
isValidApiKey: state.enablement?.isValidApiKey || false,
}, },
})) }))
.addCase( .addCase(
@ -240,23 +241,21 @@ export const monitorManagementListReducer = createReducer(initialState, (builder
enablement: true, enablement: true,
}, },
})) }))
.addCase(enableSyntheticsSuccess, (state: WritableDraft<MonitorManagementList>) => ({ .addCase(
...state, enableSyntheticsSuccess,
loading: { (state: WritableDraft<MonitorManagementList>, action: PayloadAction<any>) => ({
...state.loading, ...state,
enablement: false, loading: {
}, ...state.loading,
error: { enablement: false,
...state.error, },
enablement: null, error: {
}, ...state.error,
enablement: { enablement: null,
canManageApiKeys: state.enablement?.canManageApiKeys || false, },
canEnable: state.enablement?.canEnable || false, enablement: action.payload,
areApiKeysEnabled: state.enablement?.areApiKeysEnabled || false, })
isEnabled: true, )
},
}))
.addCase( .addCase(
enableSyntheticsFailure, enableSyntheticsFailure,
(state: WritableDraft<MonitorManagementList>, action: PayloadAction<Error>) => ({ (state: WritableDraft<MonitorManagementList>, action: PayloadAction<Error>) => ({

View file

@ -11,6 +11,7 @@ import type {
IScopedClusterClient, IScopedClusterClient,
Logger, Logger,
IBasePath, IBasePath,
CoreStart,
} from '@kbn/core/server'; } from '@kbn/core/server';
import type { TelemetryPluginSetup, TelemetryPluginStart } from '@kbn/telemetry-plugin/server'; import type { TelemetryPluginSetup, TelemetryPluginStart } from '@kbn/telemetry-plugin/server';
import { ObservabilityPluginSetup } from '@kbn/observability-plugin/server'; import { ObservabilityPluginSetup } from '@kbn/observability-plugin/server';
@ -65,6 +66,7 @@ export interface UptimeServerSetup {
uptimeEsClient: UptimeEsClient; uptimeEsClient: UptimeEsClient;
basePath: IBasePath; basePath: IBasePath;
isDev?: boolean; isDev?: boolean;
coreStart: CoreStart;
} }
export interface UptimeCorePluginsSetup { export interface UptimeCorePluginsSetup {

View file

@ -28,7 +28,6 @@ import { getJourneyScreenshotBlocks } from './get_journey_screenshot_blocks';
import { getSyntheticsMonitor } from './get_monitor'; import { getSyntheticsMonitor } from './get_monitor';
import { import {
getSyntheticsEnablement, getSyntheticsEnablement,
deleteServiceApiKey,
generateAndSaveServiceAPIKey, generateAndSaveServiceAPIKey,
getAPIKeyForSyntheticsService, getAPIKeyForSyntheticsService,
} from '../../../synthetics_service/get_api_key'; } from '../../../synthetics_service/get_api_key';
@ -57,7 +56,6 @@ export const uptimeRequests = {
getNetworkEvents, getNetworkEvents,
getSyntheticsEnablement, getSyntheticsEnablement,
getAPIKeyForSyntheticsService, getAPIKeyForSyntheticsService,
deleteServiceApiKey,
generateAndSaveServiceAPIKey, generateAndSaveServiceAPIKey,
}; };

View file

@ -11,8 +11,8 @@ import {
SavedObjectsErrorHelpers, SavedObjectsErrorHelpers,
SavedObjectsType, SavedObjectsType,
} from '@kbn/core/server'; } from '@kbn/core/server';
import { EncryptedSavedObjectsClient } from '@kbn/encrypted-saved-objects-plugin/server';
import { SyntheticsServiceApiKey } from '../../../../common/runtime_types/synthetics_service_api_key'; import { SyntheticsServiceApiKey } from '../../../../common/runtime_types/synthetics_service_api_key';
import { UptimeServerSetup } from '../adapters';
export const syntheticsApiKeyID = 'ba997842-b0cf-4429-aa9d-578d9bf0d391'; export const syntheticsApiKeyID = 'ba997842-b0cf-4429-aa9d-578d9bf0d391';
export const syntheticsApiKeyObjectType = 'uptime-synthetics-api-key'; 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 { try {
const obj = await client.getDecryptedAsInternalUser<SyntheticsServiceApiKey>( const soClient = getEncryptedSOClient(server);
const obj = await soClient.getDecryptedAsInternalUser<SyntheticsServiceApiKey>(
syntheticsServiceApiKey.name, syntheticsServiceApiKey.name,
syntheticsApiKeyID syntheticsApiKeyID
); );
@ -64,20 +72,26 @@ export const getSyntheticsServiceAPIKey = async (client: EncryptedSavedObjectsCl
} }
}; };
export const setSyntheticsServiceApiKey = async ( const setSyntheticsServiceApiKey = async (
client: SavedObjectsClientContract, soClient: SavedObjectsClientContract,
apiKey: SyntheticsServiceApiKey apiKey: SyntheticsServiceApiKey
) => { ) => {
await client.create(syntheticsServiceApiKey.name, apiKey, { await soClient.create(syntheticsServiceApiKey.name, apiKey, {
id: syntheticsApiKeyID, id: syntheticsApiKeyID,
overwrite: true, overwrite: true,
}); });
}; };
export const deleteSyntheticsServiceApiKey = async (client: SavedObjectsClientContract) => { const deleteSyntheticsServiceApiKey = async (soClient: SavedObjectsClientContract) => {
try { try {
return await client.delete(syntheticsServiceApiKey.name, syntheticsApiKeyID); return await soClient.delete(syntheticsServiceApiKey.name, syntheticsApiKeyID);
} catch (e) { } catch (e) {
throw e; throw e;
} }
}; };
export const syntheticsServiceAPIKeySavedObject = {
get: getSyntheticsServiceAPIKey,
set: setSyntheticsServiceApiKey,
delete: deleteSyntheticsServiceApiKey,
};

View file

@ -121,6 +121,7 @@ export class Plugin implements PluginType {
); );
if (this.server) { if (this.server) {
this.server.coreStart = coreStart;
this.server.security = pluginsStart.security; this.server.security = pluginsStart.security;
this.server.fleet = pluginsStart.fleet; this.server.fleet = pluginsStart.fleet;
this.server.encryptedSavedObjects = pluginsStart.encryptedSavedObjects; this.server.encryptedSavedObjects = pluginsStart.encryptedSavedObjects;

View file

@ -13,11 +13,8 @@ export const getAPIKeySyntheticsRoute: SyntheticsRestApiRouteFactory = (libs) =>
path: API_URLS.SYNTHETICS_APIKEY, path: API_URLS.SYNTHETICS_APIKEY,
validate: {}, validate: {},
handler: async ({ request, server }): Promise<any> => { handler: async ({ request, server }): Promise<any> => {
const { security } = server;
const apiKey = await generateAPIKey({ const apiKey = await generateAPIKey({
request, request,
security,
server, server,
uptimePrivileges: true, uptimePrivileges: true,
}); });

View file

@ -4,22 +4,25 @@
* 2.0; you may not use this file except in compliance with the Elastic License * 2.0; you may not use this file except in compliance with the Elastic License
* 2.0. * 2.0.
*/ */
import { syntheticsServiceAPIKeySavedObject } from '../../legacy_uptime/lib/saved_objects/service_api_key';
import { import {
SyntheticsRestApiRouteFactory, SyntheticsRestApiRouteFactory,
UMRestApiRouteFactory, UMRestApiRouteFactory,
} from '../../legacy_uptime/routes/types'; } from '../../legacy_uptime/routes/types';
import { API_URLS } from '../../../common/constants'; 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) => ({ export const getSyntheticsEnablementRoute: UMRestApiRouteFactory = (libs) => ({
method: 'GET', method: 'GET',
path: API_URLS.SYNTHETICS_ENABLEMENT, path: API_URLS.SYNTHETICS_ENABLEMENT,
validate: {}, validate: {},
handler: async ({ request, response, server }): Promise<any> => { handler: async ({ response, server }): Promise<any> => {
try { try {
return response.ok({ return response.ok({
body: await libs.requests.getSyntheticsEnablement({ body: await libs.requests.getSyntheticsEnablement({
request,
server, server,
}), }),
}); });
@ -38,25 +41,21 @@ export const disableSyntheticsRoute: SyntheticsRestApiRouteFactory = (libs) => (
response, response,
request, request,
server, server,
savedObjectsClient,
syntheticsMonitorClient, syntheticsMonitorClient,
savedObjectsClient,
}): Promise<any> => { }): Promise<any> => {
const { security } = server; const { security } = server;
const { syntheticsService } = syntheticsMonitorClient; const { syntheticsService } = syntheticsMonitorClient;
try { try {
const { canEnable } = await libs.requests.getSyntheticsEnablement({ request, server }); const { canEnable } = await libs.requests.getSyntheticsEnablement({ server });
if (!canEnable) { if (!canEnable) {
return response.forbidden(); return response.forbidden();
} }
await syntheticsService.deleteAllConfigs(); await syntheticsService.deleteAllConfigs();
const apiKey = await libs.requests.getAPIKeyForSyntheticsService({ const { apiKey } = await libs.requests.getAPIKeyForSyntheticsService({
server, server,
}); });
await libs.requests.deleteServiceApiKey({ await syntheticsServiceAPIKeySavedObject.delete(savedObjectsClient);
request,
server,
savedObjectsClient,
});
await security.authc.apiKeys?.invalidate(request, { ids: [apiKey?.id || ''] }); await security.authc.apiKeys?.invalidate(request, { ids: [apiKey?.id || ''] });
return response.ok({}); return response.ok({});
} catch (e) { } catch (e) {
@ -71,15 +70,18 @@ export const enableSyntheticsRoute: UMRestApiRouteFactory = (libs) => ({
path: API_URLS.SYNTHETICS_ENABLEMENT, path: API_URLS.SYNTHETICS_ENABLEMENT,
validate: {}, validate: {},
handler: async ({ request, response, server }): Promise<any> => { handler: async ({ request, response, server }): Promise<any> => {
const { authSavedObjectsClient, logger, security } = server; const { authSavedObjectsClient, logger } = server;
try { try {
await libs.requests.generateAndSaveServiceAPIKey({ await generateAndSaveServiceAPIKey({
request, request,
authSavedObjectsClient, authSavedObjectsClient,
security,
server, server,
}); });
return response.ok({}); return response.ok({
body: await libs.requests.getSyntheticsEnablement({
server,
}),
});
} catch (e) { } catch (e) {
logger.error(e); logger.error(e);
if (e instanceof SyntheticsForbiddenError) { if (e instanceof SyntheticsForbiddenError) {

View file

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

View file

@ -5,7 +5,7 @@
* 2.0. * 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 { encryptedSavedObjectsMock } from '@kbn/encrypted-saved-objects-plugin/server/mocks';
import { securityMock } from '@kbn/security-plugin/server/mocks'; import { securityMock } from '@kbn/security-plugin/server/mocks';
import { coreMock } from '@kbn/core/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 { KibanaRequest } from '@kbn/core/server';
import { UptimeServerSetup } from '../legacy_uptime/lib/adapters'; import { UptimeServerSetup } from '../legacy_uptime/lib/adapters';
import { getUptimeESMockClient } from '../legacy_uptime/lib/requests/test_helpers'; 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 () { describe('getAPIKeyTest', function () {
const core = coreMock.createStart(); const core = coreMock.createStart();
@ -20,7 +23,20 @@ describe('getAPIKeyTest', function () {
const encryptedSavedObjects = encryptedSavedObjectsMock.createStart(); const encryptedSavedObjects = encryptedSavedObjectsMock.createStart();
const request = {} as KibanaRequest; 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 = { const server = {
logger,
security, security,
encryptedSavedObjects, encryptedSavedObjects,
savedObjectsClient: core.savedObjects.getScopedClient(request), savedObjectsClient: core.savedObjects.getScopedClient(request),
@ -28,6 +44,7 @@ describe('getAPIKeyTest', function () {
} as unknown as UptimeServerSetup; } as unknown as UptimeServerSetup;
security.authc.apiKeys.areAPIKeysEnabled = jest.fn().mockReturnValue(true); security.authc.apiKeys.areAPIKeysEnabled = jest.fn().mockReturnValue(true);
security.authc.apiKeys.validate = jest.fn().mockReturnValue(true);
security.authc.apiKeys.create = jest.fn().mockReturnValue({ security.authc.apiKeys.create = jest.fn().mockReturnValue({
id: 'test', id: 'test',
name: 'service-api-key', name: 'service-api-key',
@ -47,7 +64,10 @@ describe('getAPIKeyTest', function () {
server, 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(encryptedSavedObjects.getClient).toHaveBeenCalledTimes(1);
expect(getObject).toHaveBeenCalledTimes(1); expect(getObject).toHaveBeenCalledTimes(1);

View file

@ -10,22 +10,19 @@ import type {
} from '@elastic/elasticsearch/lib/api/types'; } from '@elastic/elasticsearch/lib/api/types';
import { KibanaRequest, SavedObjectsClientContract } from '@kbn/core/server'; 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 { ALL_SPACES_ID } from '@kbn/security-plugin/common/constants';
import { import { syntheticsServiceAPIKeySavedObject } from '../legacy_uptime/lib/saved_objects/service_api_key';
deleteSyntheticsServiceApiKey,
getSyntheticsServiceAPIKey,
setSyntheticsServiceApiKey,
syntheticsServiceApiKey,
} from '../legacy_uptime/lib/saved_objects/service_api_key';
import { SyntheticsServiceApiKey } from '../../common/runtime_types/synthetics_service_api_key'; import { SyntheticsServiceApiKey } from '../../common/runtime_types/synthetics_service_api_key';
import { UptimeServerSetup } from '../legacy_uptime/lib/adapters'; import { UptimeServerSetup } from '../legacy_uptime/lib/adapters';
import { checkHasPrivileges } from './authentication/check_has_privilege';
export const syntheticsIndex = 'synthetics-*';
export const serviceApiKeyPrivileges = { export const serviceApiKeyPrivileges = {
cluster: ['monitor', 'read_ilm', 'read_pipeline'] as SecurityClusterPrivilege[], cluster: ['monitor', 'read_ilm', 'read_pipeline'] as SecurityClusterPrivilege[],
indices: [ indices: [
{ {
names: ['synthetics-*'], names: [syntheticsIndex],
privileges: [ privileges: [
'view_index_metadata', 'view_index_metadata',
'create_doc', 'create_doc',
@ -41,47 +38,61 @@ export const getAPIKeyForSyntheticsService = async ({
server, server,
}: { }: {
server: UptimeServerSetup; server: UptimeServerSetup;
}): Promise<SyntheticsServiceApiKey | undefined> => { }): Promise<{ apiKey?: SyntheticsServiceApiKey; isValid: boolean }> => {
const { encryptedSavedObjects } = server;
const encryptedClient = encryptedSavedObjects.getClient({
includedHiddenTypes: [syntheticsServiceApiKey.name],
});
try { try {
const apiKey = await getSyntheticsServiceAPIKey(encryptedClient); const apiKey = await syntheticsServiceAPIKeySavedObject.get(server);
if (apiKey) { 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) { } catch (err) {
// TODO: figure out how to handle decryption errors server.logger.error(err);
} }
return { isValid: false };
}; };
export const generateAPIKey = async ({ export const generateAPIKey = async ({
server, server,
security,
request, request,
uptimePrivileges = false, uptimePrivileges = false,
}: { }: {
server: UptimeServerSetup; server: UptimeServerSetup;
request?: KibanaRequest; request: KibanaRequest;
security: SecurityPluginStart;
uptimePrivileges?: boolean; uptimePrivileges?: boolean;
}) => { }) => {
const { security } = server;
const isApiKeysEnabled = await security.authc.apiKeys?.areAPIKeysEnabled(); const isApiKeysEnabled = await security.authc.apiKeys?.areAPIKeysEnabled();
if (!isApiKeysEnabled) { if (!isApiKeysEnabled) {
throw new Error('Please enable API keys in kibana to use synthetics service.'); 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) { if (uptimePrivileges) {
return security.authc.apiKeys?.create(request, { return security.authc.apiKeys?.create(request, {
name: 'uptime-api-key', name: 'synthetics-api-key (required for project monitors)',
kibana_role_descriptors: { kibana_role_descriptors: {
uptime_save: { uptime_save: {
elasticsearch: {}, elasticsearch: {},
@ -105,13 +116,13 @@ export const generateAPIKey = async ({
}); });
} }
const { canEnable } = await getSyntheticsEnablement({ request, server }); const { canEnable } = await hasEnablePermissions(server);
if (!canEnable) { if (!canEnable) {
throw new SyntheticsForbiddenError(); throw new SyntheticsForbiddenError();
} }
return security.authc.apiKeys?.create(request, { return security.authc.apiKeys?.create(request, {
name: 'synthetics-api-key', name: 'synthetics-api-key (required for monitor management)',
role_descriptors: { role_descriptors: {
synthetics_writer: serviceApiKeyPrivileges, synthetics_writer: serviceApiKeyPrivileges,
}, },
@ -124,68 +135,59 @@ export const generateAPIKey = async ({
export const generateAndSaveServiceAPIKey = async ({ export const generateAndSaveServiceAPIKey = async ({
server, server,
security,
request, request,
authSavedObjectsClient, authSavedObjectsClient,
}: { }: {
server: UptimeServerSetup; server: UptimeServerSetup;
request?: KibanaRequest; request: KibanaRequest;
security: SecurityPluginStart;
// authSavedObject is needed for write operations // authSavedObject is needed for write operations
authSavedObjectsClient?: SavedObjectsClientContract; authSavedObjectsClient?: SavedObjectsClientContract;
}) => { }) => {
const apiKeyResult = await generateAPIKey({ server, request, security }); const apiKeyResult = await generateAPIKey({ server, request });
if (apiKeyResult) { if (apiKeyResult) {
const { id, name, api_key: apiKey } = apiKeyResult; const { id, name, api_key: apiKey } = apiKeyResult;
const apiKeyObject = { id, name, apiKey }; const apiKeyObject = { id, name, apiKey };
if (authSavedObjectsClient) { if (authSavedObjectsClient) {
// discard decoded key and rest of the keys // discard decoded key and rest of the keys
await setSyntheticsServiceApiKey(authSavedObjectsClient, apiKeyObject); await syntheticsServiceAPIKeySavedObject.set(authSavedObjectsClient, apiKeyObject);
} }
return apiKeyObject; return apiKeyObject;
} }
}; };
export const deleteServiceApiKey = async ({ export const getSyntheticsEnablement = async ({ server }: { server: UptimeServerSetup }) => {
request, const { security } = server;
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],
});
const [apiKey, hasPrivileges, areApiKeysEnabled] = await Promise.all([ const [apiKey, hasPrivileges, areApiKeysEnabled] = await Promise.all([
getSyntheticsServiceAPIKey(encryptedClient), getAPIKeyForSyntheticsService({ server }),
uptimeEsClient.baseESClient.security.hasPrivileges({ hasEnablePermissions(server),
body: {
cluster: [
'manage_security',
'manage_api_key',
'manage_own_api_key',
...serviceApiKeyPrivileges.cluster,
],
index: serviceApiKeyPrivileges.indices,
},
}),
security.authc.apiKeys.areAPIKeysEnabled(), 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 { cluster } = hasPrivileges;
const { const {
manage_security: manageSecurity, manage_security: manageSecurity,
@ -203,10 +205,8 @@ export const getSyntheticsEnablement = async ({
); );
return { return {
canEnable: canManageApiKeys && hasClusterPermissions && hasIndexPermissions,
canManageApiKeys, canManageApiKeys,
isEnabled: Boolean(apiKey), canEnable: canManageApiKeys && hasClusterPermissions && hasIndexPermissions,
areApiKeysEnabled,
}; };
}; };

View file

@ -189,6 +189,9 @@ export class ServiceAPIClient {
}), }),
catchError((err: AxiosError<{ reason: string; status: number }>) => { catchError((err: AxiosError<{ reason: string; status: number }>) => {
pushErrors.push({ locationId: id, error: err.response?.data! }); 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); this.logger.error(err);
sendErrorTelemetryEvents(this.logger, this.server.telemetry, { sendErrorTelemetryEvents(this.logger, this.server.telemetry, {
reason: err.response?.data?.reason, reason: err.response?.data?.reason,
@ -199,9 +202,6 @@ export class ServiceAPIClient {
url, url,
stackVersion: this.server.stackVersion, 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 // we don't want to throw an unhandled exception here
return of(true); return of(true);
}) })

View file

@ -7,35 +7,33 @@
/* eslint-disable max-classes-per-file */ /* eslint-disable max-classes-per-file */
import { SavedObject } from '@kbn/core/server'; import { Logger, SavedObject } from '@kbn/core/server';
import { Logger } from '@kbn/core/server';
import { import {
ConcreteTaskInstance, ConcreteTaskInstance,
TaskInstance,
TaskManagerSetupContract, TaskManagerSetupContract,
TaskManagerStartContract, TaskManagerStartContract,
TaskInstance,
} from '@kbn/task-manager-plugin/server'; } from '@kbn/task-manager-plugin/server';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
import { sendErrorTelemetryEvents } from '../routes/telemetry/monitor_upgrade_sender'; import { sendErrorTelemetryEvents } from '../routes/telemetry/monitor_upgrade_sender';
import { UptimeServerSetup } from '../legacy_uptime/lib/adapters'; import { UptimeServerSetup } from '../legacy_uptime/lib/adapters';
import { installSyntheticsIndexTemplates } from '../routes/synthetics_service/install_index_templates'; 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 { getAPIKeyForSyntheticsService } from './get_api_key';
import { syntheticsMonitorType } from '../legacy_uptime/lib/saved_objects/synthetics_monitor'; import { syntheticsMonitorType } from '../legacy_uptime/lib/saved_objects/synthetics_monitor';
import { getEsHosts } from './get_es_hosts'; import { getEsHosts } from './get_es_hosts';
import { ServiceConfig } from '../../common/config'; import { ServiceConfig } from '../../common/config';
import { ServiceAPIClient } from './service_api_client'; import { ServiceAPIClient } from './service_api_client';
import { formatMonitorConfig, formatHeartbeatRequest } from './formatters/format_configs'; import { formatHeartbeatRequest, formatMonitorConfig } from './formatters/format_configs';
import { import {
ConfigKey, ConfigKey,
HeartbeatConfig,
MonitorFields, MonitorFields,
ServiceLocationErrors,
ServiceLocations, ServiceLocations,
SyntheticsMonitor, SyntheticsMonitor,
ThrottlingOptions,
SyntheticsMonitorWithId, SyntheticsMonitorWithId,
ServiceLocationErrors,
SyntheticsMonitorWithSecrets, SyntheticsMonitorWithSecrets,
HeartbeatConfig, ThrottlingOptions,
} from '../../common/runtime_types'; } from '../../common/runtime_types';
import { getServiceLocations } from './get_service_locations'; import { getServiceLocations } from './get_service_locations';
@ -54,8 +52,6 @@ export class SyntheticsService {
private readonly config: ServiceConfig; private readonly config: ServiceConfig;
private readonly esHosts: string[]; private readonly esHosts: string[];
private apiKey: SyntheticsServiceApiKey | undefined;
public locations: ServiceLocations; public locations: ServiceLocations;
public throttling: ThrottlingOptions | undefined; public throttling: ThrottlingOptions | undefined;
@ -67,6 +63,8 @@ export class SyntheticsService {
public syncErrors?: ServiceLocationErrors | null = []; public syncErrors?: ServiceLocationErrors | null = [];
public invalidApiKeyError?: boolean;
constructor(server: UptimeServerSetup) { constructor(server: UptimeServerSetup) {
this.logger = server.logger; this.logger = server.logger;
this.server = server; this.server = server;
@ -178,7 +176,7 @@ export class SyntheticsService {
status: e.status, status: e.status,
stackVersion: service.server.stackVersion, stackVersion: service.server.stackVersion,
}); });
throw e; service.logger.error(e);
} }
return { state }; return { state };
@ -235,17 +233,19 @@ export class SyntheticsService {
} }
async getApiKey() { async getApiKey() {
try { const { apiKey, isValid } = await getAPIKeyForSyntheticsService({ server: this.server });
this.apiKey = await getAPIKeyForSyntheticsService({ server: this.server }); if (!isValid) {
} catch (err) { throw new Error(
this.logger.error(err); 'API key is not valid. Cannot push monitor configuration to synthetics public testing locations'
throw err; );
} }
return this.apiKey; return apiKey;
} }
async getOutput(apiKey: SyntheticsServiceApiKey) { async getOutput() {
const apiKey = await this.getApiKey();
return { return {
hosts: this.esHosts, hosts: this.esHosts,
api_key: `${apiKey?.id}:${apiKey?.apiKey}`, api_key: `${apiKey?.id}:${apiKey?.apiKey}`,
@ -255,21 +255,15 @@ export class SyntheticsService {
async addConfig(config: HeartbeatConfig | HeartbeatConfig[]) { async addConfig(config: HeartbeatConfig | HeartbeatConfig[]) {
const monitors = this.formatConfigs(Array.isArray(config) ? config : [config]); const monitors = this.formatConfigs(Array.isArray(config) ? config : [config]);
this.apiKey = await this.getApiKey(); const output = await this.getOutput();
if (!this.apiKey) {
return null;
}
const data = {
monitors,
output: await this.getOutput(this.apiKey),
};
this.logger.debug(`1 monitor will be pushed to synthetics service.`); this.logger.debug(`1 monitor will be pushed to synthetics service.`);
try { try {
this.syncErrors = await this.apiClient.post(data); this.syncErrors = await this.apiClient.post({
monitors,
output,
});
return this.syncErrors; return this.syncErrors;
} catch (e) { } catch (e) {
this.logger.error(e); this.logger.error(e);
@ -282,15 +276,10 @@ export class SyntheticsService {
Array.isArray(monitorConfig) ? monitorConfig : [monitorConfig] Array.isArray(monitorConfig) ? monitorConfig : [monitorConfig]
); );
this.apiKey = await this.getApiKey(); const output = await this.getOutput();
if (!this.apiKey) {
return null;
}
const data = { const data = {
monitors, monitors,
output: await this.getOutput(this.apiKey), output,
isEdit: true, isEdit: true,
}; };
@ -308,31 +297,32 @@ export class SyntheticsService {
const subject = new Subject<SyntheticsMonitorWithId[]>(); const subject = new Subject<SyntheticsMonitorWithId[]>();
subject.subscribe(async (monitorConfigs) => { 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 { 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) { } 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); this.logger.error(e);
throw e;
} }
}); });
@ -345,19 +335,13 @@ export class SyntheticsService {
return; return;
} }
this.apiKey = await this.getApiKey(); const output = await this.getOutput();
if (!this.apiKey) {
return null;
}
const data = {
monitors,
output: await this.getOutput(this.apiKey),
};
try { try {
return await this.apiClient.runOnce(data); return await this.apiClient.runOnce({
monitors,
output,
});
} catch (e) { } catch (e) {
this.logger.error(e); this.logger.error(e);
throw e; throw e;
@ -365,21 +349,13 @@ export class SyntheticsService {
} }
async deleteConfigs(configs: SyntheticsMonitorWithId[]) { async deleteConfigs(configs: SyntheticsMonitorWithId[]) {
this.apiKey = await this.getApiKey(); const output = await this.getOutput();
if (!this.apiKey) {
return null;
}
const data = { const data = {
output,
monitors: this.formatConfigs(configs), monitors: this.formatConfigs(configs),
output: await this.getOutput(this.apiKey),
}; };
const result = await this.apiClient.delete(data); return await this.apiClient.delete(data);
if (this.syncErrors && this.syncErrors?.length > 0) {
await this.pushConfigs();
}
return result;
} }
async deleteAllConfigs() { async deleteAllConfigs() {

View file

@ -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<string, string> = {};
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);
}

View file

@ -23,7 +23,7 @@ export default function ({ getService }: FtrProviderContext) {
describe('[GET] - /internal/uptime/service/enablement', () => { describe('[GET] - /internal/uptime/service/enablement', () => {
['manage_security', 'manage_api_key', 'manage_own_api_key'].forEach((privilege) => { ['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 username = 'admin';
const roleName = `synthetics_admin-${privilege}`; const roleName = `synthetics_admin-${privilege}`;
const password = `${username}-password`; const password = `${username}-password`;
@ -60,6 +60,7 @@ export default function ({ getService }: FtrProviderContext) {
canManageApiKeys: true, canManageApiKeys: true,
canEnable: true, canEnable: true,
isEnabled: false, isEnabled: false,
isValidApiKey: false,
}); });
} finally { } finally {
await security.user.delete(username); await security.user.delete(username);
@ -102,6 +103,7 @@ export default function ({ getService }: FtrProviderContext) {
canManageApiKeys: false, canManageApiKeys: false,
canEnable: false, canEnable: false,
isEnabled: false, isEnabled: false,
isValidApiKey: false,
}); });
} finally { } finally {
await security.role.delete(roleName); await security.role.delete(roleName);
@ -153,6 +155,7 @@ export default function ({ getService }: FtrProviderContext) {
canManageApiKeys: true, canManageApiKeys: true,
canEnable: true, canEnable: true,
isEnabled: true, isEnabled: true,
isValidApiKey: true,
}); });
} finally { } finally {
await supertest await supertest
@ -203,6 +206,7 @@ export default function ({ getService }: FtrProviderContext) {
canManageApiKeys: false, canManageApiKeys: false,
canEnable: false, canEnable: false,
isEnabled: false, isEnabled: false,
isValidApiKey: false,
}); });
} finally { } finally {
await security.user.delete(username); await security.user.delete(username);
@ -259,6 +263,7 @@ export default function ({ getService }: FtrProviderContext) {
canManageApiKeys: true, canManageApiKeys: true,
canEnable: true, canEnable: true,
isEnabled: false, isEnabled: false,
isValidApiKey: false,
}); });
} finally { } finally {
await security.user.delete(username); await security.user.delete(username);
@ -308,6 +313,7 @@ export default function ({ getService }: FtrProviderContext) {
canManageApiKeys: false, canManageApiKeys: false,
canEnable: false, canEnable: false,
isEnabled: true, isEnabled: true,
isValidApiKey: true,
}); });
} finally { } finally {
await supertestWithAuth await supertestWithAuth
@ -370,6 +376,7 @@ export default function ({ getService }: FtrProviderContext) {
canManageApiKeys: true, canManageApiKeys: true,
canEnable: true, canEnable: true,
isEnabled: false, isEnabled: false,
isValidApiKey: false,
}); });
// can disable synthetics in non default space when enabled in default space // can disable synthetics in non default space when enabled in default space
@ -394,6 +401,7 @@ export default function ({ getService }: FtrProviderContext) {
canManageApiKeys: true, canManageApiKeys: true,
canEnable: true, canEnable: true,
isEnabled: false, isEnabled: false,
isValidApiKey: false,
}); });
} finally { } finally {
await security.user.delete(username); await security.user.delete(username);