mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[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:
parent
b33acd0fe6
commit
75ce1e397a
28 changed files with 574 additions and 271 deletions
|
@ -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(),
|
||||
}),
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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<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>) {
|
||||
const disabledFeature = e.body?.error?.['disabled.feature'];
|
||||
return disabledFeature === 'api_keys';
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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),
|
||||
},
|
||||
|
||||
|
|
|
@ -141,6 +141,7 @@ describe('Security Plugin', () => {
|
|||
"grantAsInternalUser": [Function],
|
||||
"invalidate": [Function],
|
||||
"invalidateAsInternalUser": [Function],
|
||||
"validate": [Function],
|
||||
},
|
||||
"getCurrentUser": [Function],
|
||||
},
|
||||
|
|
|
@ -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<
|
||||
|
|
|
@ -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]),
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
})
|
||||
|
|
|
@ -37,6 +37,7 @@ export function useEnablement() {
|
|||
canEnable: enablement?.canEnable,
|
||||
isEnabled: enablement?.isEnabled,
|
||||
},
|
||||
invalidApiKeyError: enablement ? !Boolean(enablement?.isValidApiKey) : false,
|
||||
error,
|
||||
loading,
|
||||
totalMonitors: total,
|
||||
|
|
|
@ -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',
|
||||
}
|
||||
);
|
|
@ -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',
|
||||
}
|
||||
);
|
|
@ -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 ? (
|
||||
<>
|
||||
<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}
|
||||
<InvalidApiKeyCalloutCallout />
|
||||
<DisabledCallout />
|
||||
<MonitorListContainer
|
||||
isEnabled={isEnabled}
|
||||
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(
|
||||
'xpack.synthetics.monitorManagement.editMonitorError.description',
|
||||
{
|
||||
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',
|
||||
}
|
||||
);
|
||||
|
|
|
@ -114,7 +114,7 @@ export const fetchDisableSynthetics = async (): Promise<void> => {
|
|||
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);
|
||||
};
|
||||
|
||||
|
|
|
@ -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<MonitorManagementList>) => ({
|
||||
...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<MonitorManagementList>, action: PayloadAction<any>) => ({
|
||||
...state,
|
||||
loading: {
|
||||
...state.loading,
|
||||
enablement: false,
|
||||
},
|
||||
error: {
|
||||
...state.error,
|
||||
enablement: null,
|
||||
},
|
||||
enablement: action.payload,
|
||||
})
|
||||
)
|
||||
.addCase(
|
||||
enableSyntheticsFailure,
|
||||
(state: WritableDraft<MonitorManagementList>, action: PayloadAction<Error>) => ({
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
||||
|
|
|
@ -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<SyntheticsServiceApiKey>(
|
||||
const soClient = getEncryptedSOClient(server);
|
||||
const obj = await soClient.getDecryptedAsInternalUser<SyntheticsServiceApiKey>(
|
||||
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,
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -13,11 +13,8 @@ export const getAPIKeySyntheticsRoute: SyntheticsRestApiRouteFactory = (libs) =>
|
|||
path: API_URLS.SYNTHETICS_APIKEY,
|
||||
validate: {},
|
||||
handler: async ({ request, server }): Promise<any> => {
|
||||
const { security } = server;
|
||||
|
||||
const apiKey = await generateAPIKey({
|
||||
request,
|
||||
security,
|
||||
server,
|
||||
uptimePrivileges: true,
|
||||
});
|
||||
|
|
|
@ -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<any> => {
|
||||
handler: async ({ response, server }): Promise<any> => {
|
||||
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<any> => {
|
||||
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<any> => {
|
||||
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) {
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
};
|
|
@ -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);
|
||||
|
|
|
@ -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<SyntheticsServiceApiKey | undefined> => {
|
||||
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,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -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);
|
||||
})
|
||||
|
|
|
@ -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<SyntheticsMonitorWithId[]>();
|
||||
|
||||
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() {
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue