mirror of
https://github.com/elastic/kibana.git
synced 2025-04-25 02:09:32 -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(),
|
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(),
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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'
|
| '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),
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -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],
|
||||||
},
|
},
|
||||||
|
|
|
@ -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<
|
||||||
|
|
|
@ -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]),
|
||||||
|
|
|
@ -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,
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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 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',
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
|
@ -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);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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,7 +241,9 @@ export const monitorManagementListReducer = createReducer(initialState, (builder
|
||||||
enablement: true,
|
enablement: true,
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
.addCase(enableSyntheticsSuccess, (state: WritableDraft<MonitorManagementList>) => ({
|
.addCase(
|
||||||
|
enableSyntheticsSuccess,
|
||||||
|
(state: WritableDraft<MonitorManagementList>, action: PayloadAction<any>) => ({
|
||||||
...state,
|
...state,
|
||||||
loading: {
|
loading: {
|
||||||
...state.loading,
|
...state.loading,
|
||||||
|
@ -250,13 +253,9 @@ export const monitorManagementListReducer = createReducer(initialState, (builder
|
||||||
...state.error,
|
...state.error,
|
||||||
enablement: null,
|
enablement: null,
|
||||||
},
|
},
|
||||||
enablement: {
|
enablement: action.payload,
|
||||||
canManageApiKeys: state.enablement?.canManageApiKeys || false,
|
})
|
||||||
canEnable: state.enablement?.canEnable || false,
|
)
|
||||||
areApiKeysEnabled: state.enablement?.areApiKeysEnabled || false,
|
|
||||||
isEnabled: true,
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
.addCase(
|
.addCase(
|
||||||
enableSyntheticsFailure,
|
enableSyntheticsFailure,
|
||||||
(state: WritableDraft<MonitorManagementList>, action: PayloadAction<Error>) => ({
|
(state: WritableDraft<MonitorManagementList>, action: PayloadAction<Error>) => ({
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
};
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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,
|
||||||
});
|
});
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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.
|
* 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);
|
||||||
|
|
|
@ -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;
|
try {
|
||||||
|
const apiKey = await syntheticsServiceAPIKeySavedObject.get(server);
|
||||||
|
|
||||||
const encryptedClient = encryptedSavedObjects.getClient({
|
if (apiKey) {
|
||||||
includedHiddenTypes: [syntheticsServiceApiKey.name],
|
const isValid = await server.security.authc.apiKeys.validate({
|
||||||
|
id: apiKey.id,
|
||||||
|
api_key: apiKey.apiKey,
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
if (isValid) {
|
||||||
const apiKey = await getSyntheticsServiceAPIKey(encryptedClient);
|
const { index } = await checkHasPrivileges(server, apiKey);
|
||||||
if (apiKey) {
|
|
||||||
return 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,55 +135,48 @@ 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),
|
||||||
|
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: {
|
body: {
|
||||||
cluster: [
|
cluster: [
|
||||||
'manage_security',
|
'manage_security',
|
||||||
|
@ -182,9 +186,7 @@ export const getSyntheticsEnablement = async ({
|
||||||
],
|
],
|
||||||
index: serviceApiKeyPrivileges.indices,
|
index: serviceApiKeyPrivileges.indices,
|
||||||
},
|
},
|
||||||
}),
|
});
|
||||||
security.authc.apiKeys.areAPIKeysEnabled(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
const { cluster } = hasPrivileges;
|
const { cluster } = hasPrivileges;
|
||||||
const {
|
const {
|
||||||
|
@ -203,10 +205,8 @@ export const getSyntheticsEnablement = async ({
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
canEnable: canManageApiKeys && hasClusterPermissions && hasIndexPermissions,
|
|
||||||
canManageApiKeys,
|
canManageApiKeys,
|
||||||
isEnabled: Boolean(apiKey),
|
canEnable: canManageApiKeys && hasClusterPermissions && hasIndexPermissions,
|
||||||
areApiKeysEnabled,
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
})
|
})
|
||||||
|
|
|
@ -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,6 +297,7 @@ export class SyntheticsService {
|
||||||
const subject = new Subject<SyntheticsMonitorWithId[]>();
|
const subject = new Subject<SyntheticsMonitorWithId[]>();
|
||||||
|
|
||||||
subject.subscribe(async (monitorConfigs) => {
|
subject.subscribe(async (monitorConfigs) => {
|
||||||
|
try {
|
||||||
const monitors = this.formatConfigs(monitorConfigs);
|
const monitors = this.formatConfigs(monitorConfigs);
|
||||||
|
|
||||||
if (monitors.length === 0) {
|
if (monitors.length === 0) {
|
||||||
|
@ -315,24 +305,24 @@ export class SyntheticsService {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
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(`${monitors.length} monitors will be pushed to synthetics service.`);
|
this.logger.debug(`${monitors.length} monitors will be pushed to synthetics service.`);
|
||||||
|
|
||||||
try {
|
service.syncErrors = await this.apiClient.put({
|
||||||
service.syncErrors = await this.apiClient.put(data);
|
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() {
|
||||||
|
|
|
@ -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', () => {
|
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);
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue