[Synthetics] enable/disable - prevent incorrect keys from being added to the monitor saved object (#140553) (#142238)

* synthetics - enable/disable - prevent incorrect keys from being added to the monitor saved object

* [CI] Auto-commit changed files from 'node scripts/precommit_hook.js --ref HEAD~1..HEAD --fix'

* adjust types

* add exact typing

* [CI] Auto-commit changed files from 'node scripts/precommit_hook.js --ref HEAD~1..HEAD --fix'

* adjust test

* use exact types

* use exact types for editing

* adjust types

* adjust tests

* adjust types

* Update x-pack/test/api_integration/apis/uptime/rest/add_monitor.ts

* adjust normalizers

* Update x-pack/plugins/synthetics/common/runtime_types/monitor_management/monitor_types.ts

* Update x-pack/plugins/synthetics/server/routes/monitor_cruds/add_monitor.ts

* adjust types

* [CI] Auto-commit changed files from 'node scripts/eslint --no-cache --fix'

* adjust jest tests

* adjust types

* adjust api_integration tests

* update tests

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
(cherry picked from commit 27916d316a)

Co-authored-by: Dominique Clarke <dominique.clarke@elastic.co>
This commit is contained in:
Kibana Machine 2022-09-29 10:30:08 -06:00 committed by GitHub
parent b247f298d6
commit c5e9eb8c11
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
37 changed files with 339 additions and 103 deletions

View file

@ -108,6 +108,7 @@ export const DEFAULT_HTTP_SIMPLE_FIELDS: HTTPSimpleFields = {
[ConfigKey.MAX_REDIRECTS]: '0',
[ConfigKey.MONITOR_TYPE]: DataStream.HTTP,
[ConfigKey.FORM_MONITOR_TYPE]: FormMonitorType.HTTP,
[ConfigKey.PORT]: null,
};
export const DEFAULT_HTTP_ADVANCED_FIELDS: HTTPAdvancedFields = {
@ -144,6 +145,7 @@ export const DEFAULT_TCP_SIMPLE_FIELDS: TCPSimpleFields = {
[ConfigKey.HOSTS]: '',
[ConfigKey.MONITOR_TYPE]: DataStream.TCP,
[ConfigKey.FORM_MONITOR_TYPE]: FormMonitorType.TCP,
[ConfigKey.PORT]: null,
};
export const DEFAULT_TCP_ADVANCED_FIELDS: TCPAdvancedFields = {

View file

@ -18,6 +18,7 @@ import {
tlsValueToStringFormatter,
tlsArrayToYamlFormatter,
} from '../tls/formatters';
import { tlsFormatters } from '../tls/formatters';
export type BrowserFormatMap = Record<keyof BrowserFields, Formatter>;
@ -72,9 +73,8 @@ export const browserFormatters: BrowserFormatMap = {
arrayToJsonFormatter(fields[ConfigKey.JOURNEY_FILTERS_TAGS]),
[ConfigKey.THROTTLING_CONFIG]: throttlingFormatter,
[ConfigKey.IGNORE_HTTPS_ERRORS]: null,
[ConfigKey.PROJECT_ID]: null,
[ConfigKey.PLAYWRIGHT_OPTIONS]: null,
[ConfigKey.ORIGINAL_SPACE]: null,
[ConfigKey.TEXT_ASSERTION]: null,
...commonFormatters,
...tlsFormatters,
};

View file

@ -29,7 +29,9 @@ export const commonFormatters: CommonFormatMap = {
[ConfigKey.MONITOR_SOURCE_TYPE]: null,
[ConfigKey.FORM_MONITOR_TYPE]: null,
[ConfigKey.JOURNEY_ID]: null,
[ConfigKey.PROJECT_ID]: null,
[ConfigKey.CUSTOM_HEARTBEAT_ID]: null,
[ConfigKey.ORIGINAL_SPACE]: null,
};
export const arrayToJsonFormatter = (value: string[] = []) =>

View file

@ -41,6 +41,7 @@ export const httpFormatters: HTTPFormatMap = {
[ConfigKey.REQUEST_HEADERS_CHECK]: (fields) =>
objectToJsonFormatter(fields[ConfigKey.REQUEST_HEADERS_CHECK]),
[ConfigKey.REQUEST_METHOD_CHECK]: null,
[ConfigKey.PORT]: null,
...tlsFormatters,
...commonFormatters,
};

View file

@ -19,6 +19,7 @@ export const tcpFormatters: TCPFormatMap = {
[ConfigKey.PROXY_USE_LOCAL_RESOLVER]: null,
[ConfigKey.RESPONSE_RECEIVE_CHECK]: null,
[ConfigKey.REQUEST_SEND_CHECK]: null,
[ConfigKey.PORT]: null,
...tlsFormatters,
...commonFormatters,
};

View file

@ -83,6 +83,8 @@ export const CommonFieldsCodec = t.intersection([
[ConfigKey.MONITOR_SOURCE_TYPE]: SourceTypeCodec,
[ConfigKey.CONFIG_ID]: t.string,
[ConfigKey.JOURNEY_ID]: t.string,
[ConfigKey.PROJECT_ID]: t.string,
[ConfigKey.ORIGINAL_SPACE]: t.string,
[ConfigKey.CUSTOM_HEARTBEAT_ID]: t.string,
}),
]);
@ -94,6 +96,7 @@ export const TCPSimpleFieldsCodec = t.intersection([
t.interface({
[ConfigKey.METADATA]: MetadataCodec,
[ConfigKey.HOSTS]: t.string,
[ConfigKey.PORT]: t.union([t.number, t.null]),
}),
CommonFieldsCodec,
]);
@ -151,6 +154,7 @@ export const HTTPSimpleFieldsCodec = t.intersection([
[ConfigKey.METADATA]: MetadataCodec,
[ConfigKey.MAX_REDIRECTS]: t.string,
[ConfigKey.URLS]: t.string,
[ConfigKey.PORT]: t.union([t.number, t.null]),
}),
CommonFieldsCodec,
]);
@ -217,8 +221,6 @@ export const EncryptedBrowserSimpleFieldsCodec = t.intersection([
}),
t.partial({
[ConfigKey.PLAYWRIGHT_OPTIONS]: t.string,
[ConfigKey.PROJECT_ID]: t.string,
[ConfigKey.ORIGINAL_SPACE]: t.string,
[ConfigKey.TEXT_ASSERTION]: t.string,
}),
]),
@ -241,7 +243,7 @@ export const BrowserSensitiveSimpleFieldsCodec = t.intersection([
CommonFieldsCodec,
]);
export const BrowserAdvancedFieldsCodec = t.interface({
export const EncryptedBrowserAdvancedFieldsCodec = t.interface({
[ConfigKey.SCREENSHOTS]: t.string,
[ConfigKey.JOURNEY_FILTERS_MATCH]: t.string,
[ConfigKey.JOURNEY_FILTERS_TAGS]: t.array(t.string),
@ -263,25 +265,26 @@ export const BrowserSensitiveAdvancedFieldsCodec = t.interface({
[ConfigKey.SYNTHETICS_ARGS]: t.array(t.string),
});
export const BrowserAdvancedsCodec = t.intersection([
BrowserAdvancedFieldsCodec,
export const BrowserAdvancedFieldsCodec = t.intersection([
EncryptedBrowserAdvancedFieldsCodec,
BrowserSensitiveAdvancedFieldsCodec,
]);
export const EncryptedBrowserFieldsCodec = t.intersection([
EncryptedBrowserSimpleFieldsCodec,
BrowserAdvancedFieldsCodec,
EncryptedBrowserAdvancedFieldsCodec,
TLSFieldsCodec,
]);
export const BrowserFieldsCodec = t.intersection([
BrowserSimpleFieldsCodec,
BrowserAdvancedFieldsCodec,
BrowserSensitiveAdvancedFieldsCodec,
TLSCodec,
]);
export type BrowserFields = t.TypeOf<typeof BrowserFieldsCodec>;
export type BrowserSimpleFields = t.TypeOf<typeof BrowserSimpleFieldsCodec>;
export type BrowserAdvancedFields = t.TypeOf<typeof BrowserAdvancedsCodec>;
export type BrowserAdvancedFields = t.TypeOf<typeof BrowserAdvancedFieldsCodec>;
// MonitorFields, represents any possible monitor type
export const MonitorFieldsCodec = t.intersection([

View file

@ -134,6 +134,7 @@ describe('format', () => {
timeout: '16',
type: 'http',
urls: 'sample url',
'url.port': null,
username: '',
});
});
@ -347,6 +348,7 @@ describe('format', () => {
timeout: '16',
type: 'http',
urls: 'sample url',
'url.port': null,
username: '',
});
});

View file

@ -51,7 +51,7 @@ export const MonitorEnabled = ({
const handleEnabledChange = (event: EuiSwitchEvent) => {
const checked = event.target.checked;
updateMonitorEnabledState(monitor, checked);
updateMonitorEnabledState(checked);
};
return (

View file

@ -111,15 +111,7 @@ describe('ActionsPopover', () => {
const enableButton = getByText('Disable monitor');
fireEvent.click(enableButton);
expect(updateMonitorEnabledState).toHaveBeenCalledTimes(1);
expect(updateMonitorEnabledState.mock.calls[0]).toEqual([
{
id: 'somelongstring',
isEnabled: true,
location: { id: 'us_central', isServiceManaged: true },
name: 'Monitor 1',
},
false,
]);
expect(updateMonitorEnabledState.mock.calls[0]).toEqual([false]);
});
it('sets enabled state to true', async () => {
@ -139,14 +131,6 @@ describe('ActionsPopover', () => {
const enableButton = getByText('Enable monitor');
fireEvent.click(enableButton);
expect(updateMonitorEnabledState).toHaveBeenCalledTimes(1);
expect(updateMonitorEnabledState.mock.calls[0]).toEqual([
{
id: 'somelongstring',
isEnabled: false,
location: { id: 'us_central', isServiceManaged: true },
name: 'Monitor 1',
},
true,
]);
expect(updateMonitorEnabledState.mock.calls[0]).toEqual([true]);
});
});

View file

@ -141,7 +141,7 @@ export function ActionsPopover({
icon: 'invert',
onClick: () => {
if (status !== FETCH_STATUS.LOADING)
updateMonitorEnabledState(monitor, !monitor.isEnabled);
updateMonitorEnabledState(!monitor.isEnabled);
},
},
],

View file

@ -9,11 +9,7 @@ import { useKibana } from '@kbn/kibana-react-plugin/public';
import { FETCH_STATUS } from '@kbn/observability-plugin/public';
import React, { useCallback, useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import {
ConfigKey,
EncryptedSyntheticsMonitor,
MonitorOverviewItem,
} from '../components/monitors_page/overview/types';
import { ConfigKey } from '../components/monitors_page/overview/types';
import {
clearMonitorUpsertStatus,
fetchUpsertMonitorAction,
@ -41,11 +37,11 @@ export function useMonitorEnableHandler({
const savedObjEnabledState = upsertStatuses[id]?.enabled;
const [isEnabled, setIsEnabled] = useState<boolean | null>(null);
const updateMonitorEnabledState = useCallback(
(monitor: EncryptedSyntheticsMonitor | MonitorOverviewItem, enabled: boolean) => {
(enabled: boolean) => {
dispatch(
fetchUpsertMonitorAction({
id,
monitor: { ...monitor, [ConfigKey.ENABLED]: enabled },
monitor: { [ConfigKey.ENABLED]: enabled },
})
);
},

View file

@ -10,7 +10,6 @@ import { createAction } from '@reduxjs/toolkit';
import {
EncryptedSyntheticsMonitor,
MonitorManagementListResult,
MonitorOverviewItem,
} from '../../../../../common/runtime_types';
import { createAsyncAction } from '../utils/actions';
@ -23,7 +22,7 @@ export const fetchMonitorListAction = createAsyncAction<
export interface UpsertMonitorRequest {
id: string;
monitor: EncryptedSyntheticsMonitor | MonitorOverviewItem;
monitor: Partial<EncryptedSyntheticsMonitor>;
}
export const fetchUpsertMonitorAction = createAction<UpsertMonitorRequest>('fetchUpsertMonitor');
export const fetchUpsertSuccessAction = createAction<{

View file

@ -11,7 +11,6 @@ import {
FetchMonitorManagementListQueryArgs,
MonitorManagementListResult,
MonitorManagementListResultCodec,
MonitorOverviewItem,
ServiceLocationErrors,
SyntheticsMonitor,
} from '../../../../../common/runtime_types';
@ -55,7 +54,7 @@ export const fetchUpsertMonitor = async ({
monitor,
id,
}: {
monitor: SyntheticsMonitor | EncryptedSyntheticsMonitor | MonitorOverviewItem;
monitor: Partial<SyntheticsMonitor> | Partial<EncryptedSyntheticsMonitor>;
id?: string;
}): Promise<{ attributes: { errors: ServiceLocationErrors } } | SyntheticsMonitor> => {
if (id) {

View file

@ -13,6 +13,8 @@ import {
LocationStatus,
ScheduleUnit,
SourceType,
VerificationMode,
TLSVersion,
} from '../../../../../../common/runtime_types';
/**
@ -338,8 +340,8 @@ function getMonitorDetailsMockSlice() {
'ssl.certificate': '',
'ssl.key': '',
'ssl.key_passphrase': '',
'ssl.verification_mode': 'full',
'ssl.supported_protocols': ['TLSv1.1', 'TLSv1.2', 'TLSv1.3'],
'ssl.verification_mode': VerificationMode.FULL,
'ssl.supported_protocols': ['TLSv1.1', 'TLSv1.2', 'TLSv1.3'] as TLSVersion[],
revision: 1,
updated_at: '2022-07-24T17:15:46.342Z',
},

View file

@ -18,6 +18,7 @@ import {
getNormalizer,
getJsonToJavascriptNormalizer,
} from '../common/normalizers';
import { tlsNormalizers } from '../tls/normalizers';
import { defaultBrowserSimpleFields, defaultBrowserAdvancedFields } from '../contexts';
@ -107,9 +108,8 @@ export const browserNormalizers: BrowserNormalizerMap = {
ConfigKey.JOURNEY_FILTERS_TAGS
),
[ConfigKey.IGNORE_HTTPS_ERRORS]: getBrowserNormalizer(ConfigKey.IGNORE_HTTPS_ERRORS),
[ConfigKey.PROJECT_ID]: getBrowserNormalizer(ConfigKey.PROJECT_ID),
[ConfigKey.PLAYWRIGHT_OPTIONS]: getBrowserNormalizer(ConfigKey.PLAYWRIGHT_OPTIONS),
[ConfigKey.ORIGINAL_SPACE]: getBrowserNormalizer(ConfigKey.ORIGINAL_SPACE),
[ConfigKey.TEXT_ASSERTION]: getBrowserNormalizer(ConfigKey.TEXT_ASSERTION),
...commonNormalizers,
...tlsNormalizers,
};

View file

@ -93,5 +93,7 @@ export const commonNormalizers: CommonNormalizerMap = {
[ConfigKey.MONITOR_SOURCE_TYPE]: getCommonNormalizer(ConfigKey.MONITOR_SOURCE_TYPE),
[ConfigKey.FORM_MONITOR_TYPE]: getCommonNormalizer(ConfigKey.FORM_MONITOR_TYPE),
[ConfigKey.JOURNEY_ID]: getCommonNormalizer(ConfigKey.JOURNEY_ID),
[ConfigKey.PROJECT_ID]: getCommonNormalizer(ConfigKey.PROJECT_ID),
[ConfigKey.CUSTOM_HEARTBEAT_ID]: getCommonNormalizer(ConfigKey.CUSTOM_HEARTBEAT_ID),
[ConfigKey.ORIGINAL_SPACE]: getCommonNormalizer(ConfigKey.ORIGINAL_SPACE),
};

View file

@ -34,6 +34,7 @@ export const getHTTPJsonToJavascriptNormalizer = (key: ConfigKey) => {
export const httpNormalizers: HTTPNormalizerMap = {
[ConfigKey.METADATA]: getHTTPJsonToJavascriptNormalizer(ConfigKey.METADATA),
[ConfigKey.URLS]: getHTTPNormalizer(ConfigKey.URLS),
[ConfigKey.PORT]: getHTTPNormalizer(ConfigKey.PORT),
[ConfigKey.MAX_REDIRECTS]: getHTTPNormalizer(ConfigKey.MAX_REDIRECTS),
[ConfigKey.USERNAME]: getHTTPNormalizer(ConfigKey.USERNAME),
[ConfigKey.PASSWORD]: getHTTPNormalizer(ConfigKey.PASSWORD),

View file

@ -33,6 +33,7 @@ export const getTCPJsonToJavascriptNormalizer = (key: ConfigKey) => {
export const tcpNormalizers: TCPNormalizerMap = {
[ConfigKey.METADATA]: getTCPJsonToJavascriptNormalizer(ConfigKey.METADATA),
[ConfigKey.HOSTS]: getTCPNormalizer(ConfigKey.HOSTS),
[ConfigKey.PORT]: getTCPNormalizer(ConfigKey.PORT),
[ConfigKey.PROXY_URL]: getTCPNormalizer(ConfigKey.PROXY_URL),
[ConfigKey.PROXY_USE_LOCAL_RESOLVER]: getTCPNormalizer(ConfigKey.PROXY_USE_LOCAL_RESOLVER),
[ConfigKey.RESPONSE_RECEIVE_CHECK]: getTCPNormalizer(ConfigKey.RESPONSE_RECEIVE_CHECK),

View file

@ -67,7 +67,7 @@ export const addSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () => ({
const validationResult = validateMonitor(monitorWithDefaults as MonitorFields);
if (!validationResult.valid) {
if (!validationResult.valid || !validationResult.decodedMonitor) {
const { reason: message, details, payload } = validationResult;
return response.badRequest({ body: { message, attributes: { details, ...payload } } });
}
@ -78,8 +78,7 @@ export const addSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () => ({
try {
const { errors, newMonitor } = await syncNewMonitor({
normalizedMonitor: monitorWithDefaults,
monitor,
normalizedMonitor: validationResult.decodedMonitor,
server,
syntheticsMonitorClient,
savedObjectsClient,
@ -140,7 +139,6 @@ export const createNewSavedObjectMonitor = async ({
export const syncNewMonitor = async ({
id,
monitor,
server,
syntheticsMonitorClient,
savedObjectsClient,
@ -150,7 +148,6 @@ export const syncNewMonitor = async ({
spaceId,
}: {
id?: string;
monitor: SyntheticsMonitor;
normalizedMonitor: SyntheticsMonitor;
server: UptimeServerSetup;
syntheticsMonitorClient: SyntheticsMonitorClient;
@ -201,7 +198,7 @@ export const syncNewMonitor = async ({
formatTelemetryEvent({
errors: syncErrors,
monitor: monitorSavedObject,
isInlineScript: Boolean((monitor as MonitorFields)[ConfigKey.SOURCE_INLINE]),
isInlineScript: Boolean((normalizedMonitor as MonitorFields)[ConfigKey.SOURCE_INLINE]),
kibanaVersion: server.kibanaVersion,
})
);

View file

@ -84,13 +84,13 @@ export const editSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () => (
const validationResult = validateMonitor(editedMonitor as MonitorFields);
if (!validationResult.valid) {
if (!validationResult.valid || !validationResult.decodedMonitor) {
const { reason: message, details, payload } = validationResult;
return response.badRequest({ body: { message, attributes: { details, ...payload } } });
}
const monitorWithRevision = {
...editedMonitor,
...validationResult.decodedMonitor,
revision: (previousMonitor.attributes[ConfigKey.REVISION] || 0) + 1,
};
const formattedMonitor = formatSecrets(monitorWithRevision);
@ -102,7 +102,7 @@ export const editSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () => (
syntheticsMonitorClient,
savedObjectsClient,
request,
normalizedMonitor: editedMonitor,
normalizedMonitor: validationResult.decodedMonitor,
monitorWithRevision: formattedMonitor,
spaceId,
});

View file

@ -109,6 +109,7 @@ describe('validateMonitor', () => {
[ConfigKey.METADATA]: testMetaData,
[ConfigKey.HOSTS]: 'https://host1.com',
[ConfigKey.FORM_MONITOR_TYPE]: FormMonitorType.TCP,
[ConfigKey.PORT]: null,
};
testTCPAdvancedFields = {
@ -131,6 +132,7 @@ describe('validateMonitor', () => {
[ConfigKey.MAX_REDIRECTS]: '3',
[ConfigKey.URLS]: 'https://example.com',
[ConfigKey.FORM_MONITOR_TYPE]: FormMonitorType.HTTP,
[ConfigKey.PORT]: null,
};
testHTTPAdvancedFields = {
@ -453,7 +455,8 @@ function getJsonPayload() {
' },' +
' "url": "https://example-url.com",' +
' "isServiceManaged": true' +
' }]' +
' }],' +
' "url.port": null' +
'}';
return JSON.parse(json);

View file

@ -4,6 +4,7 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import * as t from 'io-ts';
import { isLeft } from 'fp-ts/lib/Either';
import { formatErrors } from '@kbn/securitysolution-io-ts-utils';
@ -19,6 +20,7 @@ import {
ICMPSimpleFieldsCodec,
MonitorFields,
TCPFieldsCodec,
SyntheticsMonitor,
} from '../../../common/runtime_types';
type MonitorCodecType =
@ -39,6 +41,7 @@ export interface ValidationResult {
reason: string;
details: string;
payload: object;
decodedMonitor?: SyntheticsMonitor;
}
/**
@ -58,9 +61,10 @@ export function validateMonitor(monitorFields: MonitorFields): ValidationResult
};
}
const codec = monitorTypeToCodecMap[monitorType];
// Cast it to ICMPCodec to satisfy typing. During runtime, correct codec will be used to decode.
const SyntheticsMonitorCodec = monitorTypeToCodecMap[monitorType] as typeof ICMPSimpleFieldsCodec;
if (!codec) {
if (!SyntheticsMonitorCodec) {
return {
valid: false,
reason: `Payload is not a valid monitor object`,
@ -69,8 +73,8 @@ export function validateMonitor(monitorFields: MonitorFields): ValidationResult
};
}
// Cast it to ICMPCodec to satisfy typing. During runtime, correct codec will be used to decode.
const decodedMonitor = (codec as typeof ICMPSimpleFieldsCodec).decode(monitorFields);
const ExactSyntheticsMonitorCodec = t.exact(SyntheticsMonitorCodec);
const decodedMonitor = ExactSyntheticsMonitorCodec.decode(monitorFields);
if (isLeft(decodedMonitor)) {
return {
@ -81,7 +85,13 @@ export function validateMonitor(monitorFields: MonitorFields): ValidationResult
};
}
return { valid: true, reason: '', details: '', payload: monitorFields };
return {
valid: true,
reason: '',
details: '',
payload: monitorFields,
decodedMonitor: decodedMonitor.right,
};
}
export function validateProjectMonitor(monitorFields: ProjectMonitor): ValidationResult {

View file

@ -26,7 +26,7 @@ export const runOnceSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () =
const validationResult = validateMonitor(monitor);
if (!validationResult.valid) {
if (!validationResult.valid || !validationResult.decodedMonitor) {
const { reason: message, details, payload } = validationResult;
return response.badRequest({ body: { message, attributes: { details, ...payload } } });
}
@ -36,7 +36,7 @@ export const runOnceSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () =
const errors = await syntheticsService.runOnceConfigs([
formatHeartbeatRequest({
// making it enabled, even if it's disabled in the UI
monitor: { ...monitor, enabled: true },
monitor: { ...validationResult.decodedMonitor, enabled: true },
monitorId,
runOnce: true,
}),

View file

@ -14,6 +14,7 @@ import {
} from './common';
import { BrowserFields, ConfigKey } from '../../../common/runtime_types/monitor_management';
import { DEFAULT_BROWSER_ADVANCED_FIELDS } from '../../../common/constants/monitor_defaults';
import { tlsFormatters } from './tls';
export type BrowserFormatMap = Record<keyof BrowserFields, Formatter>;
@ -66,10 +67,9 @@ export const browserFormatters: BrowserFormatMap = {
[ConfigKey.JOURNEY_FILTERS_TAGS]: (fields) =>
arrayFormatter(fields[ConfigKey.JOURNEY_FILTERS_TAGS]),
[ConfigKey.IGNORE_HTTPS_ERRORS]: null,
[ConfigKey.PROJECT_ID]: null,
[ConfigKey.PLAYWRIGHT_OPTIONS]: (fields) =>
stringToObjectFormatter(fields[ConfigKey.PLAYWRIGHT_OPTIONS] || ''),
[ConfigKey.ORIGINAL_SPACE]: null,
[ConfigKey.TEXT_ASSERTION]: null,
...commonFormatters,
...tlsFormatters,
};

View file

@ -31,7 +31,9 @@ export const commonFormatters: CommonFormatMap = {
fields[ConfigKey.MONITOR_SOURCE_TYPE] || SourceType.UI,
[ConfigKey.FORM_MONITOR_TYPE]: null,
[ConfigKey.JOURNEY_ID]: null,
[ConfigKey.PROJECT_ID]: null,
[ConfigKey.CUSTOM_HEARTBEAT_ID]: null,
[ConfigKey.ORIGINAL_SPACE]: null,
};
export const arrayFormatter = (value: string[] = []) => (value.length ? value : null);

View file

@ -14,6 +14,7 @@ export type HTTPFormatMap = Record<keyof HTTPFields, Formatter>;
export const httpFormatters: HTTPFormatMap = {
[ConfigKey.METADATA]: (fields) => objectFormatter(fields[ConfigKey.METADATA]),
[ConfigKey.URLS]: null,
[ConfigKey.PORT]: null,
[ConfigKey.MAX_REDIRECTS]: null,
[ConfigKey.USERNAME]: null,
[ConfigKey.PASSWORD]: null,

View file

@ -14,6 +14,7 @@ export type TCPFormatMap = Record<keyof TCPFields, Formatter>;
export const tcpFormatters: TCPFormatMap = {
[ConfigKey.METADATA]: null,
[ConfigKey.HOSTS]: null,
[ConfigKey.PORT]: null,
[ConfigKey.PROXY_URL]: null,
[ConfigKey.PROXY_USE_LOCAL_RESOLVER]: null,
[ConfigKey.RESPONSE_RECEIVE_CHECK]: null,

View file

@ -179,6 +179,7 @@ describe('http normalizers', () => {
timeout: '80',
type: 'http',
urls: 'http://localhost:9200',
'url.port': null,
username: '',
},
unsupportedKeys: ['check.response.body', 'unsupportedKey.nestedUnsupportedKey'],
@ -232,6 +233,7 @@ describe('http normalizers', () => {
timeout: '80',
type: 'http',
urls: 'http://localhost:9200',
'url.port': null,
username: '',
},
unsupportedKeys: [],

View file

@ -8,7 +8,7 @@
import { Locations, LocationStatus, PrivateLocation } from '../../../../common/runtime_types';
import { normalizeProjectMonitors } from '.';
describe('http normalizers', () => {
describe('icmp normalizers', () => {
describe('normalize push monitors', () => {
const projectId = 'test-project-id';
const locations: Locations = [
@ -81,7 +81,7 @@ describe('http normalizers', () => {
},
];
it('properly normalizes http monitors', () => {
it('properly normalizes icmp monitors', () => {
const actual = normalizeProjectMonitors({
locations,
privateLocations,

View file

@ -8,7 +8,7 @@
import { Locations, LocationStatus, PrivateLocation } from '../../../../common/runtime_types';
import { normalizeProjectMonitors } from '.';
describe('http normalizers', () => {
describe('tcp normalizers', () => {
describe('normalize push monitors', () => {
const projectId = 'test-project-id';
const locations: Locations = [
@ -83,7 +83,7 @@ describe('http normalizers', () => {
},
];
it('properly normalizes http monitors', () => {
it('properly normalizes tcp monitors', () => {
const actual = normalizeProjectMonitors({
locations,
privateLocations,
@ -106,6 +106,7 @@ describe('http normalizers', () => {
enabled: true,
form_monitor_type: 'tcp',
hosts: 'smtp.gmail.com:587',
'url.port': null,
journey_id: 'gmail-smtp',
locations: [
{
@ -157,6 +158,7 @@ describe('http normalizers', () => {
enabled: true,
form_monitor_type: 'tcp',
hosts: 'localhost:18278',
'url.port': null,
journey_id: 'always-down',
locations: [
{
@ -221,6 +223,7 @@ describe('http normalizers', () => {
enabled: true,
form_monitor_type: 'tcp',
hosts: 'localhost',
'url.port': null,
journey_id: 'always-down',
locations: [
{

View file

@ -19,7 +19,6 @@ import { deleteMonitorBulk } from '../../routes/monitor_cruds/bulk_cruds/delete_
import { SyntheticsMonitorClient } from '../synthetics_monitor/synthetics_monitor_client';
import { syncEditedMonitorBulk } from '../../routes/monitor_cruds/bulk_cruds/edit_monitor_bulk';
import {
BrowserFields,
ConfigKey,
SyntheticsMonitorWithSecrets,
EncryptedSyntheticsMonitor,
@ -124,16 +123,16 @@ export class ProjectMonitorFormatter {
const existingMonitors = await this.getProjectMonitorsForProject();
this.staleMonitorsMap = await this.getStaleMonitorsMap(existingMonitors);
const normalizedNewMonitors: BrowserFields[] = [];
const normalizedNewMonitors: SyntheticsMonitor[] = [];
const normalizedUpdateMonitors: Array<{
previousMonitor: SavedObjectsFindResult<EncryptedSyntheticsMonitor>;
monitor: BrowserFields;
monitor: SyntheticsMonitor;
}> = [];
for (const monitor of this.monitors) {
const previousMonitor = existingMonitors.find(
(monitorObj) =>
(monitorObj.attributes as BrowserFields)[ConfigKey.JOURNEY_ID] === monitor.id
(monitorObj.attributes as SyntheticsMonitor)[ConfigKey.JOURNEY_ID] === monitor.id
);
const normM = await this.validateProjectMonitor({
@ -154,7 +153,7 @@ export class ProjectMonitorFormatter {
await this.createMonitorsBulk(normalizedNewMonitors);
const { updatedCount } = await this.updateMonitors(normalizedUpdateMonitors);
const { updatedCount } = await this.updateMonitorsBulk(normalizedUpdateMonitors);
if (normalizedUpdateMonitors.length > 0) {
let updateMessage = '';
@ -228,16 +227,16 @@ export class ProjectMonitorFormatter {
}
/* Validates that the normalized monitor is a valid monitor saved object type */
const { valid: isNormalizedMonitorValid } = this.validateMonitor({
const { valid: isNormalizedMonitorValid, decodedMonitor } = this.validateMonitor({
validationResult: validateMonitor(normalizedMonitor as MonitorFields),
monitorId: monitor.id,
});
if (!isNormalizedMonitorValid) {
if (!isNormalizedMonitorValid || !decodedMonitor) {
return null;
}
return normalizedMonitor;
return decodedMonitor;
} catch (e) {
this.server.logger.error(e);
this.failedMonitors.push({
@ -259,7 +258,7 @@ export class ProjectMonitorFormatter {
const staleMonitors: StaleMonitorMap = {};
existingMonitors.forEach((savedObject) => {
const journeyId = (savedObject.attributes as BrowserFields)[ConfigKey.JOURNEY_ID];
const journeyId = (savedObject.attributes as SyntheticsMonitor)[ConfigKey.JOURNEY_ID];
if (journeyId) {
staleMonitors[journeyId] = {
stale: true,
@ -291,7 +290,7 @@ export class ProjectMonitorFormatter {
return hits;
};
private createMonitorsBulk = async (monitors: BrowserFields[]) => {
private createMonitorsBulk = async (monitors: SyntheticsMonitor[]) => {
try {
if (monitors.length > 0) {
const { newMonitors } = await syncNewMonitorBulk({
@ -352,9 +351,9 @@ export class ProjectMonitorFormatter {
);
};
private updateMonitors = async (
private updateMonitorsBulk = async (
monitors: Array<{
monitor: BrowserFields;
monitor: SyntheticsMonitor;
previousMonitor: SavedObjectsFindResult<EncryptedSyntheticsMonitor>;
}>
): Promise<{

View file

@ -140,6 +140,41 @@ export default function ({ getService }: FtrProviderContext) {
expect(apiResponse.status).eql(400);
});
it('omits unknown keys', async () => {
// Delete a required property to make payload invalid
const newMonitor = {
name: 'Sample name',
url: 'https://elastic.co',
unknownKey: 'unknownValue',
type: 'http',
locations: [
{
id: 'eu-west-01',
label: 'Europe West',
geo: {
lat: 33.2343132435,
lon: 73.2342343434,
},
url: 'https://example-url.com',
isServiceManaged: true,
},
],
};
const apiResponse = await supertestAPI
.post(API_URLS.SYNTHETICS_MONITORS)
.set('kbn-xsrf', 'true')
.send(newMonitor)
.expect(200);
const response = await supertestAPI
.get(`${API_URLS.SYNTHETICS_MONITORS}/${apiResponse.body.id}`)
.set('kbn-xsrf', 'true')
.expect(200);
expect(response.body.attributes).not.to.have.keys('unknownkey', 'url');
});
it('can create monitor with API key with proper permissions', async () => {
await supertestAPI
.post('/internal/security/api_key')

View file

@ -96,6 +96,114 @@ export default function ({ getService }: FtrProviderContext) {
icmpProjectMonitors = setUniqueIds(getFixtureJson('project_icmp_monitor'));
});
it('project monitors - handles browser monitors', async () => {
const successfulMonitors = [projectMonitors.monitors[0]];
try {
const messages = await parseStreamApiResponse(
projectMonitorEndpoint,
JSON.stringify(projectMonitors)
);
expect(messages).to.have.length(2);
expect(messages[1].updatedMonitors).eql([]);
expect(messages[1].createdMonitors).eql(successfulMonitors.map((monitor) => monitor.id));
expect(messages[1].failedMonitors).eql([]);
for (const monitor of successfulMonitors) {
const journeyId = monitor.id;
const createdMonitorsResponse = await supertest
.get(API_URLS.SYNTHETICS_MONITORS)
.query({ filter: `${syntheticsMonitorType}.attributes.journey_id: ${journeyId}` })
.set('kbn-xsrf', 'true')
.expect(200);
const decryptedCreatedMonitor = await supertest
.get(`${API_URLS.SYNTHETICS_MONITORS}/${createdMonitorsResponse.body.monitors[0].id}`)
.set('kbn-xsrf', 'true')
.expect(200);
expect(decryptedCreatedMonitor.body.attributes).to.eql({
__ui: {
is_zip_url_tls_enabled: false,
script_source: {
file_name: '',
is_generated_script: false,
},
},
config_id: '',
custom_heartbeat_id: `${journeyId}-test-suite-default`,
enabled: true,
'filter_journeys.match': 'check if title is present',
'filter_journeys.tags': [],
form_monitor_type: 'multistep',
ignore_https_errors: false,
journey_id: journeyId,
locations: [
{
geo: {
lat: 0,
lon: 0,
},
id: 'localhost',
isInvalid: false,
isServiceManaged: true,
label: 'Local Synthetics Service',
status: 'experimental',
url: 'mockDevUrl',
},
],
name: 'check if title is present',
namespace: 'default',
origin: 'project',
original_space: 'default',
playwright_options: '{"headless":true,"chromiumSandbox":false}',
playwright_text_assertion: '',
project_id: 'test-suite',
params: '',
revision: 1,
schedule: {
number: '10',
unit: 'm',
},
screenshots: 'on',
'service.name': '',
'source.zip_url.folder': '',
'source.zip_url.proxy_url': '',
'source.zip_url.url': '',
'source.zip_url.password': '',
'source.zip_url.username': '',
synthetics_args: [],
tags: [],
'throttling.config': '5d/3u/20l',
'throttling.download_speed': '5',
'throttling.is_enabled': true,
'throttling.latency': '20',
'throttling.upload_speed': '3',
'ssl.certificate': '',
'ssl.certificate_authorities': '',
'ssl.supported_protocols': ['TLSv1.1', 'TLSv1.2', 'TLSv1.3'],
'ssl.verification_mode': 'full',
'ssl.key': '',
'ssl.key_passphrase': '',
'source.inline.script': '',
'source.project.content':
'UEsDBBQACAAIAON5qVQAAAAAAAAAAAAAAAAfAAAAZXhhbXBsZXMvdG9kb3MvYmFzaWMuam91cm5leS50c22Q0WrDMAxF3/sVF7MHB0LMXlc6RvcN+wDPVWNviW0sdUsp/fe5SSiD7UFCWFfHujIGlpnkybwxFTZfoY/E3hsaLEtwhs9RPNWKDU12zAOxkXRIbN4tB9d9pFOJdO6EN2HMqQguWN9asFBuQVMmJ7jiWNII9fIXrbabdUYr58l9IhwhQQZCYORCTFFUC31Btj21NRc7Mq4Nds+4bDD/pNVgT9F52Jyr2Fa+g75LAPttg8yErk+S9ELpTmVotlVwnfNCuh2lepl3+JflUmSBJ3uggt1v9INW/lHNLKze9dJe1J3QJK8pSvWkm6aTtCet5puq+x63+AFQSwcIAPQ3VfcAAACcAQAAUEsBAi0DFAAIAAgA43mpVAD0N1X3AAAAnAEAAB8AAAAAAAAAAAAgAKSBAAAAAGV4YW1wbGVzL3RvZG9zL2Jhc2ljLmpvdXJuZXkudHNQSwUGAAAAAAEAAQBNAAAARAEAAAAA',
timeout: null,
type: 'browser',
'url.port': null,
urls: '',
});
}
} finally {
await Promise.all([
successfulMonitors.map((monitor) => {
return deleteMonitor(monitor.id, httpProjectMonitors.project);
}),
]);
}
});
it('project monitors - handles http monitors', async () => {
const kibanaVersion = await kibanaServer.version.get();
const successfulMonitors = [httpProjectMonitors.monitors[1]];
@ -130,7 +238,12 @@ export default function ({ getService }: FtrProviderContext) {
.set('kbn-xsrf', 'true')
.expect(200);
expect(createdMonitorsResponse.body.monitors[0].attributes).to.eql({
const decryptedCreatedMonitor = await supertest
.get(`${API_URLS.SYNTHETICS_MONITORS}/${createdMonitorsResponse.body.monitors[0].id}`)
.set('kbn-xsrf', 'true')
.expect(200);
expect(decryptedCreatedMonitor.body.attributes).to.eql({
__ui: {
is_tls_enabled: false,
},
@ -138,6 +251,16 @@ export default function ({ getService }: FtrProviderContext) {
'check.response.status': ['200'],
config_id: '',
custom_heartbeat_id: `${journeyId}-test-suite-default`,
'check.response.body.negative': [],
'check.response.body.positive': ['Saved', 'saved'],
'check.response.headers': {},
'check.request.body': {
type: 'text',
value: '',
},
'check.request.headers': {
'Content-Type': 'application/x-www-form-urlencoded',
},
enabled: false,
form_monitor_type: 'http',
journey_id: journeyId,
@ -161,6 +284,8 @@ export default function ({ getService }: FtrProviderContext) {
origin: 'project',
original_space: 'default',
project_id: 'test-suite',
username: '',
password: '',
proxy_url: '',
'response.include_body': 'always',
'response.include_headers': false,
@ -174,10 +299,13 @@ export default function ({ getService }: FtrProviderContext) {
'ssl.certificate_authorities': '',
'ssl.supported_protocols': ['TLSv1.1', 'TLSv1.2', 'TLSv1.3'],
'ssl.verification_mode': 'full',
'ssl.key': '',
'ssl.key_passphrase': '',
tags: Array.isArray(monitor.tags) ? monitor.tags : monitor.tags?.split(','),
timeout: '80',
type: 'http',
urls: Array.isArray(monitor.urls) ? monitor.urls?.[0] : monitor.urls,
'url.port': null,
});
}
} finally {
@ -223,12 +351,19 @@ export default function ({ getService }: FtrProviderContext) {
.set('kbn-xsrf', 'true')
.expect(200);
expect(createdMonitorsResponse.body.monitors[0].attributes).to.eql({
const decryptedCreatedMonitor = await supertest
.get(`${API_URLS.SYNTHETICS_MONITORS}/${createdMonitorsResponse.body.monitors[0].id}`)
.set('kbn-xsrf', 'true')
.expect(200);
expect(decryptedCreatedMonitor.body.attributes).to.eql({
__ui: {
is_tls_enabled: false,
},
config_id: '',
custom_heartbeat_id: `${journeyId}-test-suite-default`,
'check.receive': '',
'check.send': '',
enabled: true,
form_monitor_type: 'tcp',
journey_id: journeyId,
@ -263,10 +398,13 @@ export default function ({ getService }: FtrProviderContext) {
'ssl.certificate_authorities': '',
'ssl.supported_protocols': ['TLSv1.1', 'TLSv1.2', 'TLSv1.3'],
'ssl.verification_mode': 'full',
'ssl.key': '',
'ssl.key_passphrase': '',
tags: Array.isArray(monitor.tags) ? monitor.tags : monitor.tags?.split(','),
timeout: '16',
type: 'tcp',
hosts: Array.isArray(monitor.hosts) ? monitor.hosts?.[0] : monitor.hosts,
'url.port': null,
});
}
} finally {
@ -312,7 +450,12 @@ export default function ({ getService }: FtrProviderContext) {
.set('kbn-xsrf', 'true')
.expect(200);
expect(createdMonitorsResponse.body.monitors[0].attributes).to.eql({
const decryptedCreatedMonitor = await supertest
.get(`${API_URLS.SYNTHETICS_MONITORS}/${createdMonitorsResponse.body.monitors[0].id}`)
.set('kbn-xsrf', 'true')
.expect(200);
expect(decryptedCreatedMonitor.body.attributes).to.eql({
config_id: '',
custom_heartbeat_id: `${journeyId}-test-suite-default`,
enabled: true,

View file

@ -16,7 +16,7 @@ import { getFixtureJson } from './helper/get_fixture_json';
import { PrivateLocationTestService } from './services/private_location_test_service';
export default function ({ getService }: FtrProviderContext) {
describe('[PUT] /internal/uptime/service/monitors', function () {
describe('EditMonitor', function () {
this.tags('skipCloud');
const supertest = getService('supertest');
@ -109,6 +109,66 @@ export default function ({ getService }: FtrProviderContext) {
);
});
it('strips unknown keys from monitor edits', async () => {
const newMonitor = httpMonitorJson;
const { id: monitorId, attributes: savedMonitor } = await saveMonitor(
newMonitor as MonitorFields
);
expect(savedMonitor).eql(omit(newMonitor, secretKeys));
const updates: Partial<HTTPFields> = {
[ConfigKey.URLS]: 'https://modified-host.com',
[ConfigKey.NAME]: 'Modified name',
[ConfigKey.LOCATIONS]: [
{
id: 'eu-west-01',
label: 'Europe West',
geo: {
lat: 33.2343132435,
lon: 73.2342343434,
},
url: 'https://example-url.com',
isServiceManaged: true,
},
],
[ConfigKey.REQUEST_HEADERS_CHECK]: {
sampleHeader2: 'sampleValue2',
},
[ConfigKey.METADATA]: {
script_source: {
is_generated_script: false,
file_name: 'test-file.name',
},
},
unknownkey: 'unknownvalue',
} as Partial<HTTPFields>;
const modifiedMonitor = omit(
{
...newMonitor,
...updates,
[ConfigKey.METADATA]: {
...newMonitor[ConfigKey.METADATA],
...updates[ConfigKey.METADATA],
},
},
['unknownkey']
);
const editResponse = await supertest
.put(API_URLS.SYNTHETICS_MONITORS + '/' + monitorId)
.set('kbn-xsrf', 'true')
.send(modifiedMonitor)
.expect(200);
expect(editResponse.body.attributes).eql(
omit({ ...modifiedMonitor, revision: 2 }, secretKeys)
);
expect(editResponse.body.attributes).not.to.have.keys('unknownkey');
});
it('returns 404 if monitor id is not present', async () => {
const invalidMonitorId = 'invalid-id';
const expected404Message = `Monitor id ${invalidMonitorId} not found!`;

View file

@ -23,6 +23,7 @@
"max_redirects": "3",
"password": "test",
"urls": "https://nextjs-test-synthetics.vercel.app/api/users",
"url.port": null,
"proxy_url": "http://proxy.com",
"check.response.body.negative": [],
"check.response.body.positive": [],

View file

@ -1,5 +1,5 @@
{
"type": "tcp",
"type": "icmp",
"locations": [],
"journey_id": "",
"enabled": true,
@ -14,25 +14,8 @@
"tagT2"
],
"timeout": "16",
"__ui": {
"is_tls_enabled": true,
"is_zip_url_tls_enabled": false
},
"hosts": "192.33.22.111:3333",
"proxy_url": "",
"proxy_use_local_resolver": false,
"check.receive": "",
"check.send": "",
"ssl.certificate_authorities": "",
"ssl.certificate": "",
"ssl.key": "",
"ssl.key_passphrase": "",
"ssl.verification_mode": "full",
"ssl.supported_protocols": [
"TLSv1.1",
"TLSv1.2",
"TLSv1.3"
],
"wait": "1",
"name": "Test HTTP Monitor 04",
"namespace": "testnamespace",
"origin": "ui",

View file

@ -15,6 +15,7 @@
"is_zip_url_tls_enabled": false
},
"hosts": "example-host:40",
"url.port": null,
"proxy_url": "",
"proxy_use_local_resolver": false,
"check.receive": "",